diff --git a/.gitignore b/.gitignore index 8a8997f..9b818b4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /node_modules /data *.code-workspace +.vscode/settings.json diff --git a/backend/core/form_validation.js b/backend/core/form_validation.js index e2d1ca3..6599105 100644 --- a/backend/core/form_validation.js +++ b/backend/core/form_validation.js @@ -1,11 +1,7 @@ const { PrismaClient } = require("@prisma/client"); const prisma = new PrismaClient(); const core = require("./core"); - -// Check if user registration is allowed via the settings -function registration() { - return true; -} +const settings = require("../settings"); async function userRegistration(username, password) { if (!username) return { success: false, message: "No username provided" }; @@ -29,4 +25,4 @@ async function userLogin(username, password) { return { success: true, data: existing_user.data }; } -module.exports = { registration, userRegistration, userLogin }; +module.exports = { userRegistration, userLogin }; diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js index 43743dd..ae5cf82 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -1,12 +1,16 @@ const validate = require("./form_validation"); const core = require("./core"); +const settings = require("../settings"); async function registerUser(username, password) { - const registration_allowed = validate.registration(); // Check if user registration is allowed + const registration_allowed = await settings.userRegistrationAllowed(); // Check if user registration is allowed const form_valid = await validate.userRegistration(username, password); // Check form for errors + const is_setup_complete = await settings.setupComplete(); + let role = is_setup_complete ? "ADMIN" : null; + // Register the user in the database - if (registration_allowed && form_valid.success) return await core.registerUser(username, password); + if (registration_allowed && form_valid.success) return await core.registerUser(username, password, { role: role }); // Something went wrong! return { success: false, message: form_valid.message }; @@ -19,4 +23,15 @@ async function loginUser(username, password) { return { success: true, data: { username: user.data.username, id: user.data.id, password: user.data.password } }; } -module.exports = { registerUser, loginUser }; +async function getUser({ id, username } = {}) { + let user; + if (id) user = await core.getUser({ id: id }); + else if (username) user = await core.getUser({ username: username }); + + // Make sure we only get important identifier and nothing sensitive! + if (user.success) return { success: true, data: { username: user.data.username, id: user.data.id, role: user.data.role } }; + + return { success: false, message: "No user found" }; +} + +module.exports = { registerUser, loginUser, getUser }; diff --git a/backend/page_scripts.js b/backend/page_scripts.js index bcbf8b4..e16b32c 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -1,11 +1,10 @@ const internal = require("./core/internal_api"); const bcrypt = require("bcrypt"); -const persistent_setting = require("node-persist"); -persistent_setting.init({ dir: "data/" }); +const settings = require("./settings"); async function index(request, response) { // Check if the master admin has been created - const is_setup_complete = (await persistent_setting.getItem("SETUP_COMPLETE")) || false; + const is_setup_complete = (await settings.setupComplete()) || false; if (!is_setup_complete) return response.redirect("/register"); response.render("index.ejs", { website_name: process.env.WEBSITE_NAME }); @@ -17,6 +16,13 @@ function register(request, response) { function login(request, response) { response.render("login.ejs", { website_name: process.env.WEBSITE_NAME }); } +async function admin(request, response) { + const reg_allowed = await settings.userRegistrationAllowed(); + response.render("admin.ejs", { + website_name: process.env.WEBSITE_NAME, + settings: { registration_enabled: reg_allowed }, + }); +} async function registerPost(request, response) { const hashedPassword = await bcrypt.hash(request.body.password, 10); // Hash the password for security :^) @@ -31,4 +37,15 @@ async function loginPost(request, response) { request.session.user = { username: login.data.username, id: login.data.id }; response.json({ success: true }); } -module.exports = { index, register, login, registerPost, loginPost }; + +async function settingPost(request, response) { + const user = await internal.getUser({ id: request.session.user.id }); + + if (!user.success) return response.json({ success: false, message: user.message }); + if (user.data.role !== "ADMIN") return response.json({ success: false, message: "User is not permitted" }); + + if (request.body.setting_name === "ACCOUNT_REGISTRATION") settings.setUserRegistrationAllowed(request.body.value); + + response.json({ success: true }); +} +module.exports = { index, register, login, admin, registerPost, loginPost, settingPost }; diff --git a/backend/settings.js b/backend/settings.js new file mode 100644 index 0000000..e4d77a7 --- /dev/null +++ b/backend/settings.js @@ -0,0 +1,9 @@ +const persistent_setting = require("node-persist"); +persistent_setting.init({ dir: "data/" }); + +const setupComplete = async () => (await persistent_setting.getItem("SETUP_COMPLETE")) || false; +const userRegistrationAllowed = async () => (await persistent_setting.getItem("REGISTRATION_ALLOWED")) == "true"; + +const setUserRegistrationAllowed = (new_value) => persistent_setting.setItem("REGISTRATION_ALLOWED", String(new_value)); + +module.exports = { setupComplete, userRegistrationAllowed, setUserRegistrationAllowed }; diff --git a/frontend/public/css/admin.css b/frontend/public/css/admin.css new file mode 100644 index 0000000..ea562f1 --- /dev/null +++ b/frontend/public/css/admin.css @@ -0,0 +1,133 @@ +body { + margin: 0; + background-color: #111; + color: white; +} + +.header { + background-color: #222; + width: 100%; + height: 50px; + display: flex; + flex-direction: row; + padding: 0 10px; + box-sizing: border-box; + margin-bottom: 1rem; +} +.header .left { + margin: auto auto auto 0; + font-size: 20px; + height: 100%; +} +.header .left a { + width: inherit; +} +.header .right { + margin: auto 0 auto auto; + height: 100%; + display: flex; + flex-direction: row; +} +.header .right a:hover, +.header .right a:focus { + background-color: #333; +} +.header a { + height: 100%; + width: 130px; + margin: auto 0; + display: flex; + text-decoration: none; + transition: background-color ease-in-out 0.1s; +} +.header a div { + margin: auto; + color: white; +} + +button { + background-color: #1c478a; + border: 0; + border-radius: 5px; + color: white; + cursor: pointer; +} + +button:hover, +button:focus { + background-color: #122d57; +} + +button.good { + background-color: #015b01; +} + +button.bad { + background-color: #8e0000; +} + +.page { + width: 1000px; + min-height: 10px; + margin: 0 auto; +} +.page .horizontal-button-container { + display: flex; + flex-direction: row; +} +.page .horizontal-button-container button { + width: 100px; + min-height: 30px; +} + +.hidden { + display: none !important; +} + +@media screen and (max-width: 1010px) { + .page { + width: 100%; + } +} +.container { + background-color: #222; + min-height: 10px; + padding: 10px 20px; + box-sizing: border-box; +} +.container .settings-group .settings-row { + display: flex; + flex-direction: row; + padding: 4px; + box-sizing: border-box; +} +.container .settings-group .settings-row .label { + margin: auto auto auto 0; +} +.container .settings-group .settings-row .button-group { + display: flex; + flex-direction: row; + margin: auto 0 auto auto; +} +.container .settings-group .settings-row .button-group .spinner { + margin: auto 10px auto auto; + animation: spin 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; +} +.container .settings-group .settings-row .button-group button { + height: 30px; + padding: 0px 20px; +} +.container .settings-group .settings-row:nth-child(even) { + background-color: rgba(0, 0, 0, 0.2); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(-360deg); + } +} \ No newline at end of file diff --git a/frontend/public/css/admin.scss b/frontend/public/css/admin.scss new file mode 100644 index 0000000..58efe6a --- /dev/null +++ b/frontend/public/css/admin.scss @@ -0,0 +1,47 @@ +@use "theme"; + +.container { + background-color: theme.$header-color; + min-height: 10px; + padding: 10px 20px; + box-sizing: border-box; + + .settings-group { + .settings-row { + display: flex; + flex-direction: row; + padding: 4px; + box-sizing: border-box; + .label { + margin: auto auto auto 0; + } + + .button-group { + display: flex; + flex-direction: row; + margin: auto 0 auto auto; + .spinner { + margin: auto 10px auto auto; + animation: spin 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; + } + button { + height: 30px; + padding: 0px 20px; + } + } + } + .settings-row:nth-child(even) { + background-color: #00000033; + } + } +} +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(-360deg); + } +} diff --git a/frontend/public/css/theme.css b/frontend/public/css/theme.css index dc92d1a..e227aaa 100644 --- a/frontend/public/css/theme.css +++ b/frontend/public/css/theme.css @@ -58,6 +58,14 @@ button:focus { background-color: #122d57; } +button.good { + background-color: #015b01; +} + +button.bad { + background-color: #8e0000; +} + .page { width: 1000px; min-height: 10px; @@ -72,6 +80,10 @@ button:focus { min-height: 30px; } +.hidden { + display: none !important; +} + @media screen and (max-width: 1010px) { .page { width: 100%; diff --git a/frontend/public/css/theme.scss b/frontend/public/css/theme.scss index 8e84edb..96b164e 100644 --- a/frontend/public/css/theme.scss +++ b/frontend/public/css/theme.scss @@ -65,6 +65,13 @@ button:focus { background-color: #122d57; } +button.good { + background-color: #015b01; +} +button.bad { + background-color: #8e0000; +} + .page { width: 1000px; min-height: 10px; @@ -81,6 +88,9 @@ button:focus { } } +.hidden { + display: none !important; +} @media screen and (max-width: 1010px) { .page { width: 100%; diff --git a/frontend/public/js/admin.js b/frontend/public/js/admin.js new file mode 100644 index 0000000..fb1d100 --- /dev/null +++ b/frontend/public/js/admin.js @@ -0,0 +1,32 @@ +async function toggleState(setting_name, new_value, element_id) { + // Show spinner + qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden"); + + // Send request + const form = new FormData(); + form.append("setting_name", setting_name); + form.append("value", new_value); + + const response = await request("/setting", "POST", form); + + // Check response for errors + if (response.body.success) { + qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden"); + + // Update visual to reflect current setting + // Class + const add_class = new_value ? "good" : "bad"; + const remove_class = new_value ? "bad" : "good"; + qs(`#${element_id}`).classList.remove(remove_class); + qs(`#${element_id}`).classList.add(add_class); + + // Text + const new_text = new_value ? "Enabled" : "Disabled"; + qs(`#${element_id}`).children[0].innerText = new_text; + + // Function + const add_function = new_value ? "toggleState('ACCOUNT_REGISTRATION', false, this.id)" : "toggleState('ACCOUNT_REGISTRATION', true, this.id)"; + qs(`#${element_id}`).removeAttribute("onclick"); + qs(`#${element_id}`).setAttribute("onclick", add_function); + } +} diff --git a/frontend/views/admin.ejs b/frontend/views/admin.ejs new file mode 100644 index 0000000..3d9b026 --- /dev/null +++ b/frontend/views/admin.ejs @@ -0,0 +1,39 @@ + + + + + + + + + <%= website_name %> | Administration + + + <%- include("partials/header.ejs", {selected: 'home'}) %> + +
+
+
+

Accounts

+
+
Account registration
+
+ + <% if (!settings.registration_enabled ) { %> + + <% } else { %> + + <%}%> +
+
+
+
+
+ + <%- include("partials/footer.ejs") %> + + + + diff --git a/yab.js b/yab.js index 762c88b..ebf2453 100644 --- a/yab.js +++ b/yab.js @@ -42,6 +42,7 @@ app.get("/register", checkNotAuthenticated, page_scripts.register); app.post("/register", checkNotAuthenticated, page_scripts.registerPost); // Account Required Endpoints +app.post("/setting", checkAuthenticated, page_scripts.settingPost); // app.get("/blog/new", checkAuthenticated, page_scripts.blogNew); // app.post("/blog", checkAuthenticated, page_scripts.postBlog); // app.delete("/logout", page_scripts.logout); @@ -51,6 +52,7 @@ app.post("/register", checkNotAuthenticated, page_scripts.registerPost); // Endpoints app.get("/", page_scripts.index); +app.get("/admin", checkAuthenticated, page_scripts.admin); // app.get("/blog", page_scripts.blogList); // app.get("/blog/:id", page_scripts.blogSingle); // app.get("/projects", page_scripts.projectList);