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

This commit is contained in:
Pouria Ezzati 2024-11-20 19:02:02 +03:30
parent 8a73c5ec4c
commit dab1ac4139
No known key found for this signature in database
10 changed files with 159 additions and 5 deletions

View File

@ -4,6 +4,7 @@ const { v4: uuid } = require("uuid");
const bcrypt = require("bcryptjs"); const bcrypt = require("bcryptjs");
const nanoid = require("nanoid"); const nanoid = require("nanoid");
const { ROLES } = require("../consts");
const query = require("../queries"); const query = require("../queries");
const utils = require("../utils"); const utils = require("../utils");
const redis = require("../redis"); const redis = require("../redis");
@ -26,13 +27,12 @@ function authenticate(type, error, isStrict, redirect) {
(user && isStrict && !user.verified) || (user && isStrict && !user.verified) ||
(user && user.banned)) (user && user.banned))
) { ) {
const path = user.banned ? "/logout" : "/login";
if (redirect === "page") { if (redirect === "page") {
res.redirect(path); res.redirect("/logout");
return; return;
} }
if (redirect === "header") { if (redirect === "header") {
res.setHeader("HX-Redirect", path); res.setHeader("HX-Redirect", "/logout");
res.send("NOT_AUTHENTICATED"); res.send("NOT_AUTHENTICATED");
return; return;
} }
@ -125,6 +125,33 @@ async function signup(req, res) {
return res.status(201).send({ message: "A verification email has been sent." }); 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) { function login(req, res) {
const token = utils.signToken(req.user); const token = utils.signToken(req.user);
@ -382,6 +409,7 @@ module.exports = {
changeEmailRequest, changeEmailRequest,
changePassword, changePassword,
cooldown, cooldown,
createAdminUser,
featureAccess, featureAccess,
featureAccessPage, featureAccessPage,
generateApiKey, generateApiKey,

View File

@ -3,16 +3,29 @@ const utils = require("../utils");
const env = require("../env"); const env = require("../env");
async function homepage(req, res) { async function homepage(req, res) {
const isThereAUser = await query.user.findAny();
if (!isThereAUser) {
res.redirect("/create-admin");
return;
}
res.render("homepage", { res.render("homepage", {
title: "Modern open source URL shortener", title: "Modern open source URL shortener",
}); });
} }
function login(req, res) { async function login(req, res) {
if (req.user) { if (req.user) {
res.redirect("/"); res.redirect("/");
return; return;
} }
const isThereAUser = await query.user.findAny();
if (!isThereAUser) {
res.redirect("/create-admin");
return;
}
res.render("login", { res.render("login", {
title: "Log in or sign up" 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) { function notFound(req, res) {
res.render("404", { res.render("404", {
title: "404 - Not found" title: "404 - Not found"
@ -266,6 +290,7 @@ module.exports = {
confirmLinkDelete, confirmLinkDelete,
confirmUserBan, confirmUserBan,
confirmUserDelete, confirmUserDelete,
createAdmin,
createUser, createUser,
getReportEmail, getReportEmail,
getSupportEmail, getSupportEmail,

View File

@ -417,6 +417,19 @@ const login = [
.withMessage("Email length must be max 255.") .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 = [ const changePassword = [
body("currentpassword", "Password is not valid.") body("currentpassword", "Password is not valid.")
.exists({ checkFalsy: true, checkNull: true }) .exists({ checkFalsy: true, checkNull: true })
@ -593,6 +606,7 @@ module.exports = {
changePassword, changePassword,
checkUser, checkUser,
cooldown, cooldown,
createAdmin,
createLink, createLink,
createUser, createUser,
deleteLink, deleteLink,

View File

@ -38,6 +38,8 @@ async function add(params, user) {
const data = { const data = {
email: params.email, email: params.email,
password: params.password, password: params.password,
...(params.role && { role: params.role }),
...(params.verified !== undefined && { verified: params.verified }),
verification_token: uuid(), verification_token: uuid(),
verification_expires: utils.dateToUTC(addMinutes(new Date(), 60)) verification_expires: utils.dateToUTC(addMinutes(new Date(), 60))
}; };
@ -216,10 +218,27 @@ async function create(params) {
return user; 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 = { module.exports = {
add, add,
create, create,
find, find,
findAny,
getAdmin, getAdmin,
remove, remove,
totalAdmin, totalAdmin,

View File

@ -28,6 +28,14 @@ router.post(
asyncHandler(auth.signup) asyncHandler(auth.signup)
); );
router.post(
"/create-admin",
locals.viewTemplate("partials/auth/form_admin"),
validators.createAdmin,
asyncHandler(helpers.verify),
asyncHandler(auth.createAdminUser)
);
router.post( router.post(
"/change-password", "/change-password",
locals.viewTemplate("partials/settings/change_password"), locals.viewTemplate("partials/settings/change_password"),

View File

@ -28,6 +28,11 @@ router.get(
asyncHandler(renders.logout) asyncHandler(renders.logout)
); );
router.get(
"/create-admin",
asyncHandler(renders.createAdmin)
);
router.get( router.get(
"/404", "/404",
asyncHandler(auth.jwtLoosePage), asyncHandler(auth.jwtLoosePage),

View File

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

View File

@ -23,7 +23,10 @@
{{#if errors.password}}<p class="error">{{errors.password}}</p>{{/if}} {{#if errors.password}}<p class="error">{{errors.password}}</p>{{/if}}
</label> </label>
<div class="buttons-wrapper"> <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/login}}</span>
<span>{{> icons/spinner}}</span> <span>{{> icons/spinner}}</span>
Log in Log in

View File

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

View File

@ -1021,6 +1021,8 @@ form#login-signup .buttons-wrapper button {
margin: 0; margin: 0;
} }
form#login-signup .buttons-wrapper button.full { flex-basis: 100%; }
form#login-signup a.forgot-password { form#login-signup a.forgot-password {
align-self: flex-start; align-self: flex-start;
font-size: 14px; font-size: 14px;
@ -1037,6 +1039,13 @@ form#login-signup p.error {
margin-bottom: 0; margin-bottom: 0;
} }
.admin-form-title {
font-size: 26px;
font-weight: 300;
margin: 0 0 3rem;
text-align: center;
}
.login-signup-message { .login-signup-message {
flex: 1 1 auto; flex: 1 1 auto;
margin-top: 3rem; margin-top: 3rem;