Przeglądaj źródła

add create admin page and prompt it when a kutt instance is ran for the first time

Pouria Ezzati 1 rok temu
rodzic
commit
dab1ac4139

+ 31 - 3
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,

+ 26 - 1
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,

+ 14 - 0
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,

+ 19 - 0
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,

+ 8 - 0
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"),

+ 5 - 0
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),

+ 3 - 0
server/views/create_admin.hbs

@@ -0,0 +1,3 @@
+{{> header}}
+{{> auth/form_admin}}
+{{> footer}}

+ 4 - 1
server/views/partials/auth/form.hbs

@@ -23,7 +23,10 @@
     {{#if errors.password}}<p class="error">{{errors.password}}</p>{{/if}}
   </label>
   <div class="buttons-wrapper">
-    <button type="submit" class="primary login">
+    <button 
+      type="submit" 
+      class="primary login {{#if disallow_registration}}full{{else}}{{#unless mail_enabled}}full{{/unless}}{{/if}}"
+    >
       <span>{{> icons/login}}</span>
       <span>{{> icons/spinner}}</span>
       Log in

+ 40 - 0
server/views/partials/auth/form_admin.hbs

@@ -0,0 +1,40 @@
+<form id="login-signup" hx-post="/api/auth/create-admin" hx-swap="outerHTML">
+  <h2 class="admin-form-title">
+    Create an Admin account first:
+  </h2>
+  <label class="{{#if errors.email}}error{{/if}}">
+    Email address:
+    <input
+      name="email"
+      id="email"
+      type="email"
+      autofocus="true"
+      placeholder="Email address..."
+      hx-preserve="true"
+    />
+    {{#if errors.email}}<p class="error">{{errors.email}}</p>{{/if}}
+  </label>
+  <label class="{{#if errors.password}}error{{/if}}">
+    Password:
+    <input
+      name="password"
+      id="password"
+      type="password"
+      placeholder="Password..."
+      hx-preserve="true"
+    />
+    {{#if errors.password}}<p class="error">{{errors.password}}</p>{{/if}}
+  </label>
+  <div class="buttons-wrapper admin-form">
+    <button type="submit" class="secondary full">
+      <span>{{> icons/new_user}}</span>
+      <span>{{> icons/spinner}}</span>
+      Create admin account
+    </button>
+  </div>
+  {{#unless errors}}
+    {{#if error}}
+      <p class="error">{{error}}</p>
+    {{/if}}
+  {{/unless}}
+</form>

+ 9 - 0
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;