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 .

-{{> footer}} \ No newline at end of file +{{> footer}} diff --git a/server/views/partials/report/form.hbs b/server/views/partials/report/form.hbs index f1f7ffc0b..cfd973a4d 100644 --- a/server/views/partials/report/form.hbs +++ b/server/views/partials/report/form.hbs @@ -3,21 +3,21 @@ hx-post="/api/links/report" hx-sync="this:abort" hx-swap="outerHTML" -> +> {{#if message}}

{{message}}

{{else}}
{{#if error}}

{{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 @@
{{#if link}}
- {{> icons/check}}
-

{{link}} @@ -24,12 +24,12 @@

Cut your links shorter.

{{/unless}}
-
@@ -55,9 +55,9 @@ {{/unless}}