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

View File

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

View File

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

View File

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

View File

@ -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"),

View File

@ -28,6 +28,11 @@ router.get(
asyncHandler(renders.logout)
);
router.get(
"/create-admin",
asyncHandler(renders.createAdmin)
);
router.get(
"/404",
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}}
</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

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