Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ SITE_NAME=Kutt
# Optional - The domain that this website is on
DEFAULT_DOMAIN=localhost:3000

# Optional - The path the service will run on (ex: localhost:3000/url-shortener)
BASE_PATH=

# Optional - Whether the shortened links will use the base path (default false)
SHORT_URLS_INCLUDE_PATH=false

# Required - A passphrase to encrypt JWT. Use a random long string
JWT_SECRET=

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ You can use files for each of the variables by appending `_FILE` to the name of
| `PORT` | The port to start the app on | `3000` | `8888` |
| `SITE_NAME` | Name of the website | `Kutt` | `Your Site` |
| `DEFAULT_DOMAIN` | The domain address that this app runs on | `localhost:3000` | `yoursite.com` |
| `BASE_PATH` | The path the service will run on | `/` | `/url-shortener` |
| `SHORT_URLS_INCLUDE_PATH` | Whether the shortened links will use the base path. If this value is false and BASE_PATH is specified, your proxy will need to route to the BASE_PATH of the service. | `false` | `true` |
| `LINK_LENGTH` | The length of of shortened address | `6` | `5` |
| `LINK_CUSTOM_ALPHABET` | Alphabet used to generate custom addresses. Default value omits o, O, 0, i, I, l, 1, and j to avoid confusion when reading the URL. | (abcd..789) | `abcABC^&*()@` |
| `DISALLOW_REGISTRATION` | Disable registration. Note that if `MAIL_ENABLED` is set to false, then the registration would still be disabled since it relies on emails to sign up users. | `true` | `false` |
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions server/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const spec = {
REPORT_EMAIL: str({ default: "" }),
CONTACT_EMAIL: str({ default: "" }),
NODE_APP_INSTANCE: num({ default: 0 }),
BASE_PATH: str({ default: "" }),
SHORT_URLS_INCLUDE_PATH: bool({ default: false }),
};

for (const key in spec) {
Expand Down
6 changes: 3 additions & 3 deletions server/handlers/auth.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ function authenticate(type, error, isStrict, redirect) {
(user && user.banned))
) {
if (redirect === "page") {
res.redirect("/logout");
res.redirect(utils.getPath('/logout'));
return;
}
if (redirect === "header") {
res.setHeader("HX-Redirect", "/logout");
res.setHeader("HX-Redirect", utils.getPath('/logout'));
res.send("NOT_AUTHENTICATED");
return;
}
Expand Down Expand Up @@ -357,7 +357,7 @@ function featureAccess(features, redirect) {
for (let i = 0; i < features.length; ++i) {
if (!features[i]) {
if (redirect) {
return res.redirect("/");
return res.redirect(utils.getPath("/"));
} else {
throw new CustomError("Request is not allowed.", 400);
}
Expand Down
3 changes: 2 additions & 1 deletion server/handlers/helpers.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { CustomError } = require("../utils");
const query = require("../queries");
const redis = require("../redis");
const env = require("../env");
const utils = require("../utils");

function error(error, req, res, _next) {
if (!(error instanceof CustomError)) {
Expand Down Expand Up @@ -131,7 +132,7 @@ async function adminSetup(req, res, next) {
return;
}

res.redirect("/create-admin");
res.redirect(utils.getPath('/create-admin'));
}

module.exports = {
Expand Down
10 changes: 5 additions & 5 deletions server/handlers/links.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ async function ban(req, res) {

async function redirect(req, res, next) {
const isPreservedUrl = utils.preservedURLs.some(
item => item === req.path.replace("/", "")
item => item === req.path.replace(env.BASE_PATH, "")
);

if (isPreservedUrl) return next();
Expand All @@ -480,12 +480,12 @@ async function redirect(req, res, next) {
// 3. When no link, if has domain redirect to domain's homepage
// otherwise redirect to 404
if (!link) {
return res.redirect(domain?.homepage || "/404");
return res.redirect(domain?.homepage || utils.getPath('/404'));
}

// 4. If link is banned, redirect to banned page.
if (link.banned) {
return res.redirect("/banned");
return res.redirect(utils.getPath("/banned"));
}

// 5. If wants to see link info, then redirect
Expand Down Expand Up @@ -594,7 +594,7 @@ async function redirectCustomDomainHomepage(req, res, next) {
const path = req.path;
const pathName = path.replace("/", "").split("/")[0];
if (
path === "/" ||
path === env.BASE_PATH ||
utils.preservedURLs.includes(pathName)
) {
const domain = await query.domain.find({ address: host });
Expand All @@ -618,7 +618,7 @@ async function stats(req, res) {

if (!link) {
if (req.isHTML) {
res.setHeader("HX-Redirect", "/404");
res.setHeader("HX-Redirect", utils.getPath("/404"));
res.status(200).send("");
return;
}
Expand Down
1 change: 1 addition & 0 deletions server/handlers/locals.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function config(req, res, next) {
res.locals.mail_enabled = env.MAIL_ENABLED;
res.locals.report_email = env.REPORT_EMAIL;
res.locals.custom_styles = utils.getCustomCSSFileNames();
res.locals.base_path = env.BASE_PATH;
next();
}

Expand Down
8 changes: 4 additions & 4 deletions server/handlers/renders.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const env = require("../env");

async function homepage(req, res) {
if (env.DISALLOW_ANONYMOUS_LINKS && !req.user) {
res.redirect("/login");
res.redirect(utils.getPath("/login"));
return;
}
res.render("homepage", {
Expand All @@ -20,7 +20,7 @@ async function homepage(req, res) {

async function login(req, res) {
if (req.user) {
res.redirect("/");
res.redirect(utils.getPath("/"));
return;
}

Expand All @@ -39,7 +39,7 @@ function logout(req, res) {
async function createAdmin(req, res) {
const isThereAUser = await query.user.findAny();
if (isThereAUser) {
res.redirect("/login");
res.redirect(utils.getPath("/login"));
return;
}
res.render("create_admin", {
Expand Down Expand Up @@ -79,7 +79,7 @@ async function banned(req, res) {

async function report(req, res) {
if (!env.REPORT_EMAIL) {
res.redirect("/");
res.redirect(utils.getPath("/"));
return;
}
res.render("report", {
Expand Down
3 changes: 3 additions & 0 deletions server/mail/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ if (env.MAIL_ENABLED) {
resetEmailTemplate = fs
.readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
.replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
.replace(/{{base_path}}/gm, env.BASE_PATH)
.replace(/{{site_name}}/gm, env.SITE_NAME);
verifyEmailTemplate = fs
.readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
.replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
.replace(/{{base_path}}/gm, env.BASE_PATH)
.replace(/{{site_name}}/gm, env.SITE_NAME);
changeEmailTemplate = fs
.readFileSync(changeEmailTemplatePath, { encoding: "utf-8" })
.replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
.replace(/{{base_path}}/gm, env.BASE_PATH)
.replace(/{{site_name}}/gm, env.SITE_NAME);
}

Expand Down
4 changes: 2 additions & 2 deletions server/mail/template-reset.html
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@
>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;"><tr><td style="padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;" align="left"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="http://{{domain}}/reset-password/{{resetpassword}}" style="height:31pt; v-text-anchor:middle; width:81pt;" arcsize="143%" strokecolor="#2196F3" fillcolor="#2196F3"><w:anchorlock/><v:textbox inset="0,0,0,0"><center style="color:#ffffff; font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size:16px;"><![endif]-->
<a
href="https://{{domain}}/reset-password/{{resetpassword}}"
href="https://{{domain}}{{base_path}}/reset-password/{{resetpassword}}"
target="_blank"
style="display: block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #ffffff; background-color: #2196F3; border-radius: 60px; -webkit-border-radius: 60px; -moz-border-radius: 60px; max-width: 128px; width: 48px;width: auto; border-top: 0px solid transparent; border-right: 0px solid transparent; border-bottom: 0px solid transparent; border-left: 0px solid transparent; padding-top: 5px; padding-right: 30px; padding-bottom: 5px; padding-left: 30px; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;mso-border-alt: none"
>
Expand All @@ -478,7 +478,7 @@
<span style="font-size:14px; line-height:25px;">
<a
style="color:#0068A5;text-decoration: underline;"
href="https://{{domain}}"
href="https://{{domain}}{{base_path}}"
target="_blank"
rel="noopener"
data-mce-selected="1"
Expand Down
28 changes: 17 additions & 11 deletions server/server.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const env = require("./env");
const { Router } = require("express");

const cookieParser = require("cookie-parser");
const passport = require("passport");
Expand Down Expand Up @@ -40,10 +41,13 @@ app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const router = Router();

// serve static
app.use("/images", express.static("custom/images"));
app.use("/css", express.static("custom/css", { extensions: ["css"] }));
app.use(express.static("static"));
router.use("/images", express.static("custom/images"));
router.use("/css", express.static("custom/css", { extensions: ["css"] }));
router.use(express.static("static"));
router.use(express.static("static"));

app.use(passport.initialize());
app.use(locals.isHTML);
Expand All @@ -59,21 +63,23 @@ app.set("views", [
utils.registerHandlebarsHelpers();

// if is custom domain, redirect to the set homepage
app.use(asyncHandler(links.redirectCustomDomainHomepage));
router.use(asyncHandler(links.redirectCustomDomainHomepage));

// render html pages
app.use("/", routes.render);
router.use("/", routes.render);

// handle api requests
app.use("/api/v2", routes.api);
app.use("/api", routes.api);
router.use("/api/v2", routes.api);
router.use("/api", routes.api);

// finally, redirect the short link to the target
app.get("/:id", asyncHandler(links.redirect));
// redirect the short link to the target (using BASE_PATH if specified)
(env.SHORT_URLS_INCLUDE_PATH ? router : app).get("/:id", asyncHandler(links.redirect));

// 404 pages that don't exist
app.get("*", renders.notFound);
// finally, 404 pages that don't exist
router.get("*", renders.notFound);

// configure to run on the specified base path
app.use(env.BASE_PATH, router);
// handle errors coming from above routes
app.use(helpers.error);

Expand Down
18 changes: 17 additions & 1 deletion server/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ function addProtocol(url) {

function getShortURL(address, domain) {
const protocol = (env.CUSTOM_DOMAIN_USE_HTTPS || !domain) && !env.isDev ? "https://" : "http://";
const link = `${domain || env.DEFAULT_DOMAIN}/${address}`;
const linkDomain = domain || env.DEFAULT_DOMAIN;
let path = '';

if (env.BASE_PATH) {
if (linkDomain === env.DEFAULT_DOMAIN) {
path = env.BASE_PATH;
} else if (env.SHORT_URLS_INCLUDE_PATH) {
path = env.BASE_PATH;
}
}

const link = `${linkDomain}${path}/${address}`;
const url = `${protocol}${link}`;
return { address, link, url };
}
Expand Down Expand Up @@ -185,6 +196,10 @@ const preservedURLs = [
"pricing"
];

function getPath(path) {
return `${env.BASE_PATH}${path}`;
}

function parseBooleanQuery(query) {
if (query === "true" || query === true) return true;
if (query === "false" || query === false) return false;
Expand Down Expand Up @@ -410,6 +425,7 @@ module.exports = {
parseDatetime,
parseTimestamps,
preservedURLs,
getPath,
registerHandlebarsHelpers,
removeWww,
sanitize,
Expand Down
2 changes: 1 addition & 1 deletion server/views/404.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<h2>
404 | Link could not be found.
</h2>
<a class="back-to-home" href="/">
<a class="back-to-home" href="./">
← Back to homepage
</a>
</div>
Expand Down
2 changes: 1 addition & 1 deletion server/views/banned.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</h2>
<h4>
If you noticed a malware/scam link shortened by {{default_domain}},
<a href="/report" title="Send report">
<a href="./report" title="Send report">
send us a report
</a>.
</h4>
Expand Down
2 changes: 1 addition & 1 deletion server/views/error.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Error!
</h2>
<p>{{message}}</p>
<a class="back-to-home" href="/">
<a class="back-to-home" href="./">
← Back to homepage
</a>
</div>
Expand Down
30 changes: 15 additions & 15 deletions server/views/layout.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<link rel="icon" sizes="196x196" href="/images/favicon-196x196.png" />
<link rel="icon" sizes="32x32" href="/images/favicon-32x32.png" />
<link rel="icon" sizes="16x16" href="/images/favicon-16x16.png" />
<link rel="apple-touch-icon" href="/images/favicon-196x196.png" />
<link rel="mask-icon" href="/images/icon.svg" color="blue" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" sizes="196x196" href="./images/favicon-196x196.png" />
<link rel="icon" sizes="32x32" href="./images/favicon-32x32.png" />
<link rel="icon" sizes="16x16" href="./images/favicon-16x16.png" />
<link rel="apple-touch-icon" href="./images/favicon-196x196.png" />
<link rel="mask-icon" href="./images/icon.svg" color="blue" />
<link rel="manifest" href="./manifest.webmanifest" />
<meta name="theme-color" content="#f3f3f3" />
<meta property="fb:app_id" content="123456789" />
<meta name="htmx-config" content='{"withCredentials":true}'>
<meta property="og:url" content="https://{{default_domain}}" />
<meta property="og:url" content="https://{{default_domain}}{{base_path}}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{site_name}}" />
<meta property="og:image" content="https://{{default_domain}}/images/card.png" />
<meta property="og:image" content="https://{{default_domain}}{{base_path}}/images/card.png" />
<meta property="og:description" content="Free & Open Source Modern URL Shortener" />
<meta name="twitter:url" content="https://{{default_domain}}" />
<meta name="twitter:url" content="https://{{default_domain}}{{base_path}}" />
<meta name="twitter:title" content="{{site_name}}" />
<meta name="twitter:description" content="Free & Open Source Modern URL Shortener" />
<meta name="twitter:image" content="https://{{default_domain}}/images/card.png" />
<meta name="twitter:image" content="https://{{default_domain}}{{base_path}}/images/card.png" />
<meta name="description" content="{{site_name}} is a free and open source URL shortener with custom domains and stats." />
<title>{{site_name}} | {{title}}</title>
<link rel="stylesheet" href="/css/styles.css">
<link rel="stylesheet" href="./css/styles.css">
{{#each custom_styles}}
<link rel="stylesheet" href="/css/{{this}}">
<link rel="stylesheet" href="./css/{{this}}">
{{/each}}
{{{block "stylesheets"}}}
</head>
Expand All @@ -35,8 +35,8 @@
</div>

{{{block "scripts"}}}
<script src="/libs/htmx.min.js"></script>
<script src="/libs/qrcode.min.js"></script>
<script src="/scripts/main.js"></script>
<script src="./libs/htmx.min.js"></script>
<script src="./libs/qrcode.min.js"></script>
<script src="./scripts/main.js"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion server/views/logout.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{> header}}
<div class="login-signup-message" hx-get="/" hx-trigger="load delay:1s" hx-target="body" hx-push-url="/">
<div class="login-signup-message" hx-get="./" hx-trigger="load delay:1s" hx-target="body" hx-push-url="./">
<h1>
Logged out. Redirecting to homepage...
</h1>
Expand Down
2 changes: 1 addition & 1 deletion server/views/partials/admin/dialog/add_domain.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h2>Add domain</h2>
<form
id="add-domain-form"
hx-post="/api/domains/admin"
hx-post="./api/domains/admin"
hx-target="closest .content"
hx-swap="outerHTML"
hx-indicator="closest .content"
Expand Down
Loading