diff --git a/.example.env b/.example.env
index 6e9c4d3b1..e5a504e99 100644
--- a/.example.env
+++ b/.example.env
@@ -4,9 +4,16 @@ PORT=3000
# Optional - The name of the site where Kutt is hosted
SITE_NAME=Kutt
-# Optional - The domain that this website is on
+# Optional - The default domain for new links. This is also used as the admin domain if ADMIN_DOMAIN is not set
DEFAULT_DOMAIN=localhost:3000
+# Optional - The domain where admin functions take place.
+# Falls back to DEFAULT_DOMAIN if unset.
+ADMIN_DOMAIN=
+
+# Optional - Comma-separated list of domains available to all users for link shortening.
+OTHER_GLOBAL_DOMAINS=
+
# Required - A passphrase to encrypt JWT. Use a random long string
JWT_SECRET=
diff --git a/README.md b/README.md
index 6ad07f955..dbe1c371e 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ Support the development of Kutt by making a donation or becoming an sponsor.
## Setup
-The only prerequisite is [Node.js](https://nodejs.org/) (version 20 or above). The default database is SQLite. You can optionally install Postgres or MySQL/MariaDB for the database or Redis for the cache.
+The only prerequisite is [Node.js](https://nodejs.org/) (version 20 or above). The default database is SQLite. You can optionally install Postgres or MySQL/MariaDB for the database or Redis for the cache.
When you first start the app, you're prompted to create an admin account.
@@ -86,7 +86,7 @@ Official Kutt Docker image is available on [Docker Hub](https://hub.docker.com/r
The app is configured via environment variables. You can pass environment variables directly or create a `.env` file. View [`.example.env`](./.example.env) file for the list of configurations.
-All variables are optional except `JWT_SECRET` which is required on production.
+All variables are optional except `JWT_SECRET` which is required on production.
You can use files for each of the variables by appending `_FILE` to the name of the variable. Example: `JWT_SECRET_FILE=/path/to/secret_file`.
@@ -95,7 +95,9 @@ You can use files for each of the variables by appending `_FILE` to the name of
| `JWT_SECRET` | This is used to sign authentication tokens. Use a **long** **random** string. | - | - |
| `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` |
+| `DEFAULT_DOMAIN` | The default domain for new links (if multiple domains are configured - see also `OTHER_GLOBAL_DOMAINS`). This is also used as the admin domain if ADMIN_DOMAIN is not set | `localhost:3000` | `yoursite.com` |
+| `ADMIN_DOMAIN` | The domain where admin functions take place. If unset, falls back to `DEFAULT_DOMAIN` | `""` | `admin.yoursite.com` |
+| `OTHER_GLOBAL_DOMAINS` | Comma-separated domains available globally for link shortening (in addition to `DEFAULT_DOMAIN`) | `""` | `my.site,another.site` |
| `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` |
@@ -120,15 +122,15 @@ You can use files for each of the variables by appending `_FILE` to the name of
| `SERVER_CNAME_ADDRESS` | The subdomain shown to the user on the setting's page. It's only for display purposes and has no other use. | - | `custom.yoursite.com` |
| `CUSTOM_DOMAIN_USE_HTTPS` | Use https for links with custom domain. It's on you to generate SSL certificates for those domains manually—at least on this version for now. | `false` | `true` |
| `ENABLE_RATE_LIMIT` | Enable rate limiting for some API routes. If Redis is enabled uses Redis, otherwise, uses memory. | `false` | `true` |
-| `MAIL_ENABLED` | Enable emails, which are used for signup, verifying or changing email address, resetting password, and sending reports. If is disabled, all these functionalities will be disabled too. | `false` | `true` |
+| `MAIL_ENABLED` | Enable emails, which are used for signup, verifying or changing email address, resetting password, and sending reports. If is disabled, all these functionalities will be disabled too. | `false` | `true` |
| `MAIL_HOST` | Email server host | - | `your-mail-server.com` |
-| `MAIL_PORT` | Email server port | `587` | `465` (SSL) |
-| `MAIL_USER` | Email server user | - | `myuser` |
-| `MAIL_PASSWORD` | Email server password for the user | - | `mypassword` |
-| `MAIL_FROM` | Email address to send the user from | - | `example@yoursite.com` |
-| `MAIL_SECURE` | Whether use SSL for the email server connection | `false` | `true` |
-| `REPORT_EMAIL` | The email address that will receive submitted reports | - | `example@yoursite.com` |
-| `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` |
+| `MAIL_PORT` | Email server port | `587` | `465` (SSL) |
+| `MAIL_USER` | Email server user | - | `myuser` |
+| `MAIL_PASSWORD` | Email server password for the user | - | `mypassword` |
+| `MAIL_FROM` | Email address to send the user from | - | `example@yoursite.com` |
+| `MAIL_SECURE` | Whether use SSL for the email server connection | `false` | `true` |
+| `REPORT_EMAIL` | The email address that will receive submitted reports | - | `example@yoursite.com` |
+| `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` |
## Themes and customizations
@@ -165,7 +167,7 @@ custom/
- **views**: Custom HTML templates to render. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/views))
- It should follow the same file naming and folder structure as [`/server/views`](./server/views)
- Although we try to keep the original file names unchanged, be aware that new changes on Kutt might break your custom views.
-
+
#### Example theme: Crimson
This is an example and official theme. Crimson includes custom styles, images, and views.
diff --git a/package-lock.json b/package-lock.json
index 00c1f0bed..2ffc07694 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"date-fns": "2.30.0",
- "dotenv": "^16.4.7",
+ "dotenv": "16.4.7",
"envalid": "8.0.0",
"express": "4.21.2",
"express-rate-limit": "7.5.0",
diff --git a/server/env.js b/server/env.js
index 87f7bc373..0c078bac5 100644
--- a/server/env.js
+++ b/server/env.js
@@ -30,6 +30,8 @@ const spec = {
PORT: num({ default: 3000 }),
SITE_NAME: str({ example: "Kutt", default: "Kutt" }),
DEFAULT_DOMAIN: str({ example: "kutt.it", default: "localhost:3000" }),
+ ADMIN_DOMAIN: str({ example: "admin.kutt.it", default: "" }),
+ OTHER_GLOBAL_DOMAINS: str({ example: "kutt.example.org,kutt.example.com", default: "" }),
LINK_LENGTH: num({ default: 6 }),
LINK_CUSTOM_ALPHABET: str({ default: "abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789" }),
TRUST_PROXY: bool({ default: true }),
diff --git a/server/handlers/links.handler.js b/server/handlers/links.handler.js
index 93a0a6489..691388d32 100644
--- a/server/handlers/links.handler.js
+++ b/server/handlers/links.handler.js
@@ -56,20 +56,20 @@ async function getAdmin(req, res) {
const banned = utils.parseBooleanQuery(req.query.banned);
const anonymous = utils.parseBooleanQuery(req.query.anonymous);
const has_domain = utils.parseBooleanQuery(req.query.has_domain);
-
+
const match = {
...(banned !== undefined && { banned }),
...(anonymous !== undefined && { user_id: [anonymous ? "is" : "is not", null] }),
...(has_domain !== undefined && { domain_id: [has_domain ? "is not" : "is", null] }),
};
-
+
// if domain is equal to the defualt domain,
// it means admins is looking for links with the defualt domain (no custom user domain)
if (domain === env.DEFAULT_DOMAIN) {
domain = undefined;
match.domain_id = null;
}
-
+
const [data, total] = await Promise.all([
query.link.getAdmin(match, { limit, search, user, domain, skip }),
query.link.totalAdmin(match, { search, user, domain })
@@ -99,9 +99,9 @@ async function getAdmin(req, res) {
async function create(req, res) {
const { reuse, password, customurl, description, target, fetched_domain, expire_in } = req.body;
const domain_id = fetched_domain ? fetched_domain.id : null;
-
+
const targetDomain = utils.removeWww(URL.parse(target).hostname);
-
+
const tasks = await Promise.all([
reuse &&
query.link.find({
@@ -118,13 +118,13 @@ async function create(req, res) {
validators.bannedDomain(targetDomain),
validators.bannedHost(targetDomain)
]);
-
+
// if "reuse" is true, try to return
// the existent URL without creating one
if (tasks[0]) {
return res.json(utils.sanitize.link(tasks[0]));
}
-
+
// Check if custom link already exists
if (tasks[1]) {
const error = "Custom URL is already in use.";
@@ -145,16 +145,16 @@ async function create(req, res) {
});
link.domain = fetched_domain?.address;
-
+
if (req.isHTML) {
res.setHeader("HX-Trigger", "reloadMainTable");
const shortURL = utils.getShortURL(link.address, link.domain);
return res.render("partials/shortener", {
- link: shortURL.link,
+ link: shortURL.link,
url: shortURL.url,
});
}
-
+
return res
.status(201)
.send(utils.sanitize.link({ ...link }));
@@ -172,14 +172,14 @@ async function edit(req, res) {
let isChanged = false;
[
- [req.body.address, "address"],
- [req.body.target, "target"],
- [req.body.description, "description"],
- [req.body.expire_in, "expire_in"],
+ [req.body.address, "address"],
+ [req.body.target, "target"],
+ [req.body.description, "description"],
+ [req.body.expire_in, "expire_in"],
[req.body.password, "password"]
].forEach(([value, name]) => {
if (!value) {
- if (name === "password" && link.password)
+ if (name === "password" && link.password)
req.body.password = null;
else {
delete req.body[name];
@@ -206,7 +206,7 @@ async function edit(req, res) {
}
const { address, target, description, expire_in, password } = req.body;
-
+
const targetDomain = target && utils.removeWww(URL.parse(target).hostname);
const domain_id = link.domain_id || null;
@@ -265,14 +265,14 @@ async function editAdmin(req, res) {
let isChanged = false;
[
- [req.body.address, "address"],
- [req.body.target, "target"],
- [req.body.description, "description"],
- [req.body.expire_in, "expire_in"],
+ [req.body.address, "address"],
+ [req.body.target, "target"],
+ [req.body.description, "description"],
+ [req.body.expire_in, "expire_in"],
[req.body.password, "password"]
].forEach(([value, name]) => {
if (!value) {
- if (name === "password" && link.password)
+ if (name === "password" && link.password)
req.body.password = null;
else {
delete req.body[name];
@@ -299,7 +299,7 @@ async function editAdmin(req, res) {
}
const { address, target, description, expire_in, password } = req.body;
-
+
const targetDomain = target && utils.removeWww(URL.parse(target).hostname);
const domain_id = link.domain_id || null;
@@ -382,7 +382,7 @@ async function report(req, res) {
});
return;
}
-
+
return res
.status(200)
.send({ message: "Thanks for the report, we'll take actions shortly." });
@@ -492,7 +492,7 @@ async function redirect(req, res, next) {
const isRequestingInfo = /.*\+$/gi.test(req.params.id);
if (isRequestingInfo && !link.password) {
if (req.isHTML) {
- res.render("url_info", {
+ res.render("url_info", {
title: "Short link information",
target: link.target,
link: utils.getShortURL(link.address, link.domain).link
@@ -659,4 +659,4 @@ module.exports = {
redirect,
redirectProtected,
redirectCustomDomainHomepage,
-}
\ No newline at end of file
+}
diff --git a/server/handlers/locals.handler.js b/server/handlers/locals.handler.js
index 7b1b4ab25..0f5044c02 100644
--- a/server/handlers/locals.handler.js
+++ b/server/handlers/locals.handler.js
@@ -22,6 +22,7 @@ function viewTemplate(template) {
function config(req, res, next) {
res.locals.default_domain = env.DEFAULT_DOMAIN;
+ res.locals.admin_domain = utils.getAdminDomain();
res.locals.site_name = env.SITE_NAME;
res.locals.contact_email = env.CONTACT_EMAIL;
res.locals.server_ip_address = env.SERVER_IP_ADDRESS;
@@ -30,13 +31,30 @@ 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.other_global_domains = utils.getGlobalDomains();
next();
}
async function user(req, res, next) {
const user = req.user;
+ let userDomains = [];
+ if (user) {
+ userDomains = await query.domain.get({ user_id: user.id });
+ userDomains = userDomains.map(utils.sanitize.domain);
+ }
+
+ const defaultDomain = env.DEFAULT_DOMAIN;
+ const globalDomains = utils.getGlobalDomains().filter(d => d !== defaultDomain);
+ const userDomainAddresses = userDomains.map(d => d.address);
+
+ const filteredUserDomains = userDomains.filter(
+ d => d.address !== defaultDomain && !globalDomains.includes(d.address)
+ );
+
+ res.locals.default_domain = defaultDomain;
+ res.locals.other_global_domains = globalDomains;
+ res.locals.user_domains = filteredUserDomains;
res.locals.user = user;
- res.locals.domains = user && (await query.domain.get({ user_id: user.id })).map(utils.sanitize.domain);
next();
}
@@ -86,4 +104,4 @@ module.exports = {
protected,
user,
viewTemplate,
-}
\ No newline at end of file
+}
diff --git a/server/handlers/validators.handler.js b/server/handlers/validators.handler.js
index c479ecaa0..26e482012 100644
--- a/server/handlers/validators.handler.js
+++ b/server/handlers/validators.handler.js
@@ -78,23 +78,29 @@ const createLink = [
.withMessage("Expire time should be more than 1 minute.")
.customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),
body("domain")
- .optional({ nullable: true, checkFalsy: true })
- .customSanitizer(value => value === env.DEFAULT_DOMAIN ? null : value)
- .custom(checkUser)
- .withMessage("Only users can use this field.")
- .isString()
- .withMessage("Domain should be string.")
- .customSanitizer(value => value.toLowerCase())
- .custom(async (address, { req }) => {
- const domain = await query.domain.find({
- address,
- user_id: req.user.id
- });
- req.body.fetched_domain = domain || null;
-
- if (!domain) return Promise.reject();
- })
- .withMessage("You can't use this domain.")
+ .optional({ nullable: true, checkFalsy: true })
+ .customSanitizer(value => value === env.DEFAULT_DOMAIN ? null : value)
+ .custom(checkUser)
+ .withMessage("Only users can use this field.")
+ .isString()
+ .withMessage("Domain should be string.")
+ .customSanitizer(value => value && value.toLowerCase())
+ .custom(async (address, { req }) => {
+ let domain;
+ if (!address) {
+ domain = await query.domain.find({ address: env.DEFAULT_DOMAIN.toLowerCase() });
+ } else {
+ domain = await query.domain.find({ address });
+ }
+
+ if (domain && domain.user_id && domain.user_id !== req.user.id) {
+ return Promise.reject();
+ }
+
+ req.body.fetched_domain = domain || null;
+ if (!domain) return Promise.reject();
+ })
+ .withMessage("You can't use this domain.")
];
const editLink = [
@@ -350,7 +356,7 @@ const createUser = [
.isEmail()
.custom(async (value, { req }) => {
const user = await query.user.find({ email: value });
- if (user)
+ if (user)
return Promise.reject();
})
.withMessage("User already exists."),
@@ -552,7 +558,7 @@ module.exports = {
deleteUserByAdmin,
editLink,
getStats,
- login,
+ login,
newPassword,
redirectProtected,
removeDomain,
@@ -561,4 +567,4 @@ module.exports = {
resetPassword,
signup,
signupEmailTaken,
-}
\ No newline at end of file
+}
diff --git a/server/mail/mail.js b/server/mail/mail.js
index 93e5304d7..d28acf92c 100644
--- a/server/mail/mail.js
+++ b/server/mail/mail.js
@@ -3,7 +3,8 @@ const path = require("node:path");
const fs = require("node:fs");
const { resetMailText, verifyMailText, changeEmailText } = require("./text");
-const { CustomError } = require("../utils");
+const utils = require("../utils");
+const { CustomError } = utils;
const env = require("../env");
const mailConfig = {
@@ -26,7 +27,7 @@ const verifyEmailTemplatePath = path.join(__dirname, "template-verify.html");
const changeEmailTemplatePath = path.join(__dirname,"template-change-email.html");
-let resetEmailTemplate,
+let resetEmailTemplate,
verifyEmailTemplate,
changeEmailTemplate;
@@ -34,15 +35,15 @@ let resetEmailTemplate,
if (env.MAIL_ENABLED) {
resetEmailTemplate = fs
.readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME);
verifyEmailTemplate = fs
.readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME);
changeEmailTemplate = fs
.readFileSync(changeEmailTemplatePath, { encoding: "utf-8" })
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME);
}
@@ -57,11 +58,11 @@ async function verification(user) {
subject: "Verify your account",
text: verifyMailText
.replace(/{{verification}}/gim, user.verification_token)
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME),
html: verifyEmailTemplate
.replace(/{{verification}}/gim, user.verification_token)
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME)
});
@@ -74,21 +75,21 @@ async function changeEmail(user) {
if (!env.MAIL_ENABLED) {
throw new Error("Attempting to send change email token but email is not enabled.");
};
-
+
const mail = await transporter.sendMail({
from: env.MAIL_FROM || env.MAIL_USER,
to: user.change_email_address,
subject: "Verify your new email address",
text: changeEmailText
.replace(/{{verification}}/gim, user.change_email_token)
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME),
html: changeEmailTemplate
.replace(/{{verification}}/gim, user.change_email_token)
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
.replace(/{{site_name}}/gm, env.SITE_NAME)
});
-
+
if (!mail.accepted.length) {
throw new CustomError("Couldn't send verification email. Try again later.");
}
@@ -105,10 +106,10 @@ async function resetPasswordToken(user) {
subject: "Reset your password",
text: resetMailText
.replace(/{{resetpassword}}/gm, user.reset_password_token)
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN),
+ .replace(/{{domain}}/gm, utils.getAdminDomain()),
html: resetEmailTemplate
.replace(/{{resetpassword}}/gm, user.reset_password_token)
- .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+ .replace(/{{domain}}/gm, utils.getAdminDomain())
});
if (!mail.accepted.length) {
diff --git a/server/server.js b/server/server.js
index 606ed0e11..7384c82bc 100644
--- a/server/server.js
+++ b/server/server.js
@@ -14,69 +14,74 @@ const locals = require("./handlers/locals.handler");
const links = require("./handlers/links.handler");
const routes = require("./routes");
const utils = require("./utils");
+const ensureDomains = require("./utils/ensureDomains");
+(async () => {
+ // make sure domains from .env are present in the database
+ await ensureDomains();
-// run the cron jobs
-// the app might be running in cluster mode (multiple instances) so run the cron job only on one cluster (the first one)
-// NODE_APP_INSTANCE variable is added by pm2 automatically, if you're using something else to cluster your app, then make sure to set this variable
-if (env.NODE_APP_INSTANCE === 0) {
- require("./cron");
-}
+ // run the cron jobs
+ // the app might be running in cluster mode (multiple instances) so run the cron job only on one cluster (the first one)
+ // NODE_APP_INSTANCE variable is added by pm2 automatically, if you're using something else to cluster your app, then make sure to set this variable
+ if (env.NODE_APP_INSTANCE === 0) {
+ require("./cron");
+ }
-// intialize passport authentication library
-require("./passport");
+ // intialize passport authentication library
+ require("./passport");
-// create express app
-const app = express();
+ // create express app
+ const app = express();
-// this tells the express app that it's running behind a proxy server
-// and thus it should get the IP address from the proxy server
-if (env.TRUST_PROXY) {
- app.set("trust proxy", true);
-}
+ // this tells the express app that it's running behind a proxy server
+ // and thus it should get the IP address from the proxy server
+ if (env.TRUST_PROXY) {
+ app.set("trust proxy", true);
+ }
-app.use(helmet({ contentSecurityPolicy: false }));
-app.use(cookieParser());
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
+ app.use(helmet({ contentSecurityPolicy: false }));
+ app.use(cookieParser());
+ app.use(express.json());
+ app.use(express.urlencoded({ extended: true }));
-// serve static
-app.use("/images", express.static("custom/images"));
-app.use("/css", express.static("custom/css", { extensions: ["css"] }));
-app.use(express.static("static"));
+ // serve static
+ app.use("/images", express.static("custom/images"));
+ app.use("/css", express.static("custom/css", { extensions: ["css"] }));
+ app.use(express.static("static"));
-app.use(passport.initialize());
-app.use(locals.isHTML);
-app.use(locals.config);
+ app.use(passport.initialize());
+ app.use(locals.isHTML);
+ app.use(locals.config);
-// template engine / serve html
+ // template engine / serve html
-app.set("view engine", "hbs");
-app.set("views", [
- path.join(__dirname, "../custom/views"),
- path.join(__dirname, "views"),
-]);
-utils.registerHandlebarsHelpers();
+ app.set("view engine", "hbs");
+ app.set("views", [
+ path.join(__dirname, "../custom/views"),
+ path.join(__dirname, "views"),
+ ]);
+ utils.registerHandlebarsHelpers();
-// if is custom domain, redirect to the set homepage
-app.use(asyncHandler(links.redirectCustomDomainHomepage));
+ // if is custom domain, redirect to the set homepage
+ app.use(asyncHandler(links.redirectCustomDomainHomepage));
-// render html pages
-app.use("/", routes.render);
+ // render html pages
+ app.use("/", routes.render);
-// handle api requests
-app.use("/api/v2", routes.api);
-app.use("/api", routes.api);
+ // handle api requests
+ app.use("/api/v2", routes.api);
+ app.use("/api", routes.api);
-// finally, redirect the short link to the target
-app.get("/:id", asyncHandler(links.redirect));
+ // finally, redirect the short link to the target
+ app.get("/:id", asyncHandler(links.redirect));
-// 404 pages that don't exist
-app.get("*", renders.notFound);
+ // 404 pages that don't exist
+ app.get("*", renders.notFound);
-// handle errors coming from above routes
-app.use(helpers.error);
-
-app.listen(env.PORT, () => {
- console.log(`> Ready on http://localhost:${env.PORT}`);
-});
+ // handle errors coming from above routes
+ app.use(helpers.error);
+
+ app.listen(env.PORT, () => {
+ console.log(`> Ready on http://localhost:${env.PORT}`);
+ });
+})();
diff --git a/server/utils/ensureDomains.js b/server/utils/ensureDomains.js
new file mode 100644
index 000000000..75a33c42c
--- /dev/null
+++ b/server/utils/ensureDomains.js
@@ -0,0 +1,21 @@
+const env = require("../env");
+const query = require("../queries");
+const utils = require("./utils");
+
+async function ensureDomains() {
+ // List of domains to ensure in DB
+ const domains = [
+ env.DEFAULT_DOMAIN.trim().toLowerCase(),
+ ...utils.getGlobalDomains().map(d => d.trim().toLowerCase())
+ ].filter(Boolean);
+
+ for (const address of domains) {
+ let domain = await query.domain.find({ address });
+ if (!domain) {
+ await query.domain.add({ address, user_id: null, banned: false });
+ console.log(`Added domain to DB: ${address}`);
+ }
+ }
+}
+
+module.exports = ensureDomains;
diff --git a/server/utils/utils.js b/server/utils/utils.js
index 7981e0d24..08bbf8778 100644
--- a/server/utils/utils.js
+++ b/server/utils/utils.js
@@ -87,7 +87,7 @@ function statsObjectToArray(obj) {
value: obj[key][name]
}))
.sort((a, b) => b.value - a.value);
-
+
return {
browser: objToArr("browser"),
os: objToArr("os"),
@@ -123,12 +123,12 @@ function dateToUTC(date) {
if (knex.isSQLite) {
return dateUTC.substring(0, 10) + " " + dateUTC.substring(11, 19);
}
-
+
// mysql doesn't save time in utc, so format the date in local timezone instead
if (knex.isMySQL) {
return format(new Date(date), "yyyy-MM-dd HH:mm:ss");
}
-
+
// return unformatted utc string for postgres
return dateUTC;
}
@@ -348,7 +348,7 @@ function registerHandlebarsHelpers() {
hbs.registerHelper("json", function(context) {
return JSON.stringify(context);
});
-
+
const blocks = {};
hbs.registerHelper("extend", function(name, context) {
@@ -392,6 +392,14 @@ function getCustomCSSFileNames() {
return custom_css_file_names;
}
+function getAdminDomain() {
+ return env.ADMIN_DOMAIN || env.DEFAULT_DOMAIN;
+}
+
+function getGlobalDomains() {
+ return env.OTHER_GLOBAL_DOMAINS.split(",").map(s => s.trim()).filter(Boolean) || [];
+}
+
module.exports = {
addProtocol,
customAddressRegex,
@@ -400,8 +408,10 @@ module.exports = {
dateToUTC,
deleteCurrentToken,
generateId,
+ getAdminDomain,
getCustomCSSFileNames,
getDifferenceFunction,
+ getGlobalDomains,
getInitStats,
getShortURL,
getStatsPeriods,
@@ -419,4 +429,4 @@ module.exports = {
statsObjectToArray,
urlRegex,
...knexUtils,
-}
\ No newline at end of file
+}
diff --git a/server/views/banned.hbs b/server/views/banned.hbs
index a2434f312..6b7fe29c8 100644
--- a/server/views/banned.hbs
+++ b/server/views/banned.hbs
@@ -1,14 +1,14 @@
{{> header}}
- Link has been banned and removed because of
+ Link has been banned and removed because of
malware or scam.
- If you noticed a malware/scam link shortened by {{default_domain}},
+ If you noticed a malware/scam link shortened by {{admin_domain}},
send us a report
.
{{message}}
{{else}}{{error}}
{{/if}} {{/if}} - \ No newline at end of file + diff --git a/server/views/partials/shortener.hbs b/server/views/partials/shortener.hbs index c9e5ecfb5..2a794295a 100644 --- a/server/views/partials/shortener.hbs +++ b/server/views/partials/shortener.hbs @@ -2,19 +2,19 @@