Skip to content
Open
Show file tree
Hide file tree
Changes from all 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