Admin page

Toggle account registration
pull/2/head
Armored Dragon 2023-09-20 19:41:57 -05:00
parent c9d13320d5
commit e60f58a975
12 changed files with 326 additions and 13 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/node_modules
/data
*.code-workspace
.vscode/settings.json

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

9
backend/settings.js Normal file
View File

@ -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 };

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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%;

View File

@ -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%;

View File

@ -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);
}
}

39
frontend/views/admin.ejs Normal file
View File

@ -0,0 +1,39 @@
<!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/admin.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | Administration</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<div class="container">
<div class="settings-group">
<h1>Accounts</h1>
<div class="settings-row">
<div class="label">Account registration</div>
<div class="button-group">
<div class="spinner hidden">
<div>↺</div>
</div>
<% if (!settings.registration_enabled ) { %>
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', true, this.id)" class="bad"><div>Disabled</div></button>
<% } else { %>
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', false, this.id)" class="good"><div>Enabled</div></button>
<%}%>
</div>
</div>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/admin.js"></script>

2
yab.js
View File

@ -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);