From dab1ac413945956059f0531afca1844a890ec6e6 Mon Sep 17 00:00:00 2001 From: Pouria Ezzati Date: Wed, 20 Nov 2024 19:02:02 +0330 Subject: [PATCH] add create admin page and prompt it when a kutt instance is ran for the first time --- server/handlers/auth.handler.js | 34 +++++++++++++++++-- server/handlers/renders.handler.js | 27 ++++++++++++++- server/handlers/validators.handler.js | 14 ++++++++ server/queries/user.queries.js | 19 +++++++++++ server/routes/auth.routes.js | 8 +++++ server/routes/renders.routes.js | 5 +++ server/views/create_admin.hbs | 3 ++ server/views/partials/auth/form.hbs | 5 ++- server/views/partials/auth/form_admin.hbs | 40 +++++++++++++++++++++++ static/css/styles.css | 9 +++++ 10 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 server/views/create_admin.hbs create mode 100644 server/views/partials/auth/form_admin.hbs diff --git a/server/handlers/auth.handler.js b/server/handlers/auth.handler.js index 074bf28..68301de 100644 --- a/server/handlers/auth.handler.js +++ b/server/handlers/auth.handler.js @@ -4,6 +4,7 @@ const { v4: uuid } = require("uuid"); const bcrypt = require("bcryptjs"); const nanoid = require("nanoid"); +const { ROLES } = require("../consts"); const query = require("../queries"); const utils = require("../utils"); const redis = require("../redis"); @@ -26,13 +27,12 @@ function authenticate(type, error, isStrict, redirect) { (user && isStrict && !user.verified) || (user && user.banned)) ) { - const path = user.banned ? "/logout" : "/login"; if (redirect === "page") { - res.redirect(path); + res.redirect("/logout"); return; } if (redirect === "header") { - res.setHeader("HX-Redirect", path); + res.setHeader("HX-Redirect", "/logout"); res.send("NOT_AUTHENTICATED"); return; } @@ -125,6 +125,33 @@ async function signup(req, res) { return res.status(201).send({ message: "A verification email has been sent." }); } +async function createAdminUser(req, res) { + const isThereAUser = await query.user.findAny(); + if (isThereAUser) { + throw new CustomError("Can not create the admin user because a user already exists.", 400); + } + + const salt = await bcrypt.genSalt(12); + const password = await bcrypt.hash(req.body.password, salt); + + const user = await query.user.add({ + email: req.body.email, + password, + role: ROLES.ADMIN, + verified: true + }); + + const token = utils.signToken(user); + + if (req.isHTML) { + utils.setToken(res, token); + res.render("partials/auth/welcome"); + return; + } + + return res.status(201).send({ token }); +} + function login(req, res) { const token = utils.signToken(req.user); @@ -382,6 +409,7 @@ module.exports = { changeEmailRequest, changePassword, cooldown, + createAdminUser, featureAccess, featureAccessPage, generateApiKey, diff --git a/server/handlers/renders.handler.js b/server/handlers/renders.handler.js index b96087d..dccfc6a 100644 --- a/server/handlers/renders.handler.js +++ b/server/handlers/renders.handler.js @@ -3,16 +3,29 @@ const utils = require("../utils"); const env = require("../env"); async function homepage(req, res) { + const isThereAUser = await query.user.findAny(); + if (!isThereAUser) { + res.redirect("/create-admin"); + return; + } + res.render("homepage", { title: "Modern open source URL shortener", }); } -function login(req, res) { +async function login(req, res) { if (req.user) { res.redirect("/"); return; } + + const isThereAUser = await query.user.findAny(); + if (!isThereAUser) { + res.redirect("/create-admin"); + return; + } + res.render("login", { title: "Log in or sign up" }); @@ -25,6 +38,17 @@ function logout(req, res) { }); } +async function createAdmin(req, res) { + const isThereAUser = await query.user.findAny(); + if (isThereAUser) { + res.redirect("/login"); + return; + } + res.render("create_admin", { + title: "Create admin account" + }); +} + function notFound(req, res) { res.render("404", { title: "404 - Not found" @@ -266,6 +290,7 @@ module.exports = { confirmLinkDelete, confirmUserBan, confirmUserDelete, + createAdmin, createUser, getReportEmail, getSupportEmail, diff --git a/server/handlers/validators.handler.js b/server/handlers/validators.handler.js index 6b391d0..73b3c21 100644 --- a/server/handlers/validators.handler.js +++ b/server/handlers/validators.handler.js @@ -417,6 +417,19 @@ const login = [ .withMessage("Email length must be max 255.") ]; +const createAdmin = [ + body("password", "Password is not valid.") + .exists({ checkFalsy: true, checkNull: true }) + .isLength({ min: 8, max: 64 }) + .withMessage("Password length must be between 8 and 64."), + body("email", "Email is not valid.") + .exists({ checkFalsy: true, checkNull: true }) + .trim() + .isEmail() + .isLength({ min: 0, max: 255 }) + .withMessage("Email length must be max 255.") +]; + const changePassword = [ body("currentpassword", "Password is not valid.") .exists({ checkFalsy: true, checkNull: true }) @@ -593,6 +606,7 @@ module.exports = { changePassword, checkUser, cooldown, + createAdmin, createLink, createUser, deleteLink, diff --git a/server/queries/user.queries.js b/server/queries/user.queries.js index 52e0667..3c79560 100644 --- a/server/queries/user.queries.js +++ b/server/queries/user.queries.js @@ -38,6 +38,8 @@ async function add(params, user) { const data = { email: params.email, password: params.password, + ...(params.role && { role: params.role }), + ...(params.verified !== undefined && { verified: params.verified }), verification_token: uuid(), verification_expires: utils.dateToUTC(addMinutes(new Date(), 60)) }; @@ -216,10 +218,27 @@ async function create(params) { return user; } +// check if there exists a user +async function findAny() { + if (env.REDIS_ENABLED) { + const anyuser = await redis.client.get("any-user"); + if (anyuser) return true; + } + + const anyuser = await knex("users").select("id").first(); + + if (env.REDIS_ENABLED && anyuser) { + redis.client.set("any-user", JSON.stringify(anyuser), "EX", 60 * 5); + } + + return !!anyuser; +} + module.exports = { add, create, find, + findAny, getAdmin, remove, totalAdmin, diff --git a/server/routes/auth.routes.js b/server/routes/auth.routes.js index f9d22dc..4591cc9 100644 --- a/server/routes/auth.routes.js +++ b/server/routes/auth.routes.js @@ -28,6 +28,14 @@ router.post( asyncHandler(auth.signup) ); +router.post( + "/create-admin", + locals.viewTemplate("partials/auth/form_admin"), + validators.createAdmin, + asyncHandler(helpers.verify), + asyncHandler(auth.createAdminUser) +); + router.post( "/change-password", locals.viewTemplate("partials/settings/change_password"), diff --git a/server/routes/renders.routes.js b/server/routes/renders.routes.js index 718ca26..bf1b241 100644 --- a/server/routes/renders.routes.js +++ b/server/routes/renders.routes.js @@ -28,6 +28,11 @@ router.get( asyncHandler(renders.logout) ); +router.get( + "/create-admin", + asyncHandler(renders.createAdmin) +); + router.get( "/404", asyncHandler(auth.jwtLoosePage), diff --git a/server/views/create_admin.hbs b/server/views/create_admin.hbs new file mode 100644 index 0000000..5959550 --- /dev/null +++ b/server/views/create_admin.hbs @@ -0,0 +1,3 @@ +{{> header}} +{{> auth/form_admin}} +{{> footer}} diff --git a/server/views/partials/auth/form.hbs b/server/views/partials/auth/form.hbs index 7e39e5a..baf0f90 100644 --- a/server/views/partials/auth/form.hbs +++ b/server/views/partials/auth/form.hbs @@ -23,7 +23,10 @@ {{#if errors.password}}

{{errors.password}}

{{/if}}
- +
+ {{#unless errors}} + {{#if error}} +

{{error}}

+ {{/if}} + {{/unless}} + \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index ea4c9cc..cde0880 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -1021,6 +1021,8 @@ form#login-signup .buttons-wrapper button { margin: 0; } +form#login-signup .buttons-wrapper button.full { flex-basis: 100%; } + form#login-signup a.forgot-password { align-self: flex-start; font-size: 14px; @@ -1037,6 +1039,13 @@ form#login-signup p.error { margin-bottom: 0; } +.admin-form-title { + font-size: 26px; + font-weight: 300; + margin: 0 0 3rem; + text-align: center; +} + .login-signup-message { flex: 1 1 auto; margin-top: 3rem;