From cf3e476ebf28aaca061fa31e3cb5bfcd48e2443c Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Sun, 24 Nov 2024 08:57:35 -0700 Subject: [PATCH] feature complete Rolled back custom url setting. It was getting too complicated. For example, what if user defined urls that are already used by existing routes? We'd need to add path validation and probably other stuff too. My motivation was to obscure the login link in order to provide a little protection from simple bot attacks. It's likely better to take a considered security look (from folks who know this space!) --- .../options/other/share_settings.js | 46 ++++++------------- src/public/translations/en/translation.json | 12 ++--- src/routes/routes.ts | 14 +++++- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.js b/src/public/app/widgets/type_widgets/options/other/share_settings.js index bee1b56fc..c0727a852 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.js +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.js @@ -6,31 +6,19 @@ const TPL = `

${t('share.settings_title')}

- - -

${t('share.redirect_url_description')}

+
+ + +

${t('share.redirect_bare_domain_description')}

+
- - -

${t('share.login_redirect_url_description')}

-
- -
- -

${t('share.show_login_description')}

+
+ + +

${t('share.show_login_in_share_description')}

+
`; @@ -38,24 +26,18 @@ const TPL = ` export default class ShareSettingsWidget extends OptionsWidget { doRender() { this.$widget = $(TPL); - - this.$shareRedirectUrl = this.$widget.find(".share-redirect-url"); - this.$loginRedirectUrl = this.$widget.find(".login-redirect-url"); + this.$redirectBareDomain = this.$widget.find(".redirect-bare-domain"); this.$showLoginInShare = this.$widget.find(".show-login-in-share"); - this.$shareRedirectUrl.on('change', () => - this.updateOption('shareRedirectUrl', this.$shareRedirectUrl.val() || 'share')); - - this.$loginRedirectUrl.on('change', () => - this.updateOption('loginRedirectUrl', this.$loginRedirectUrl.val() || 'login')); + this.$redirectBareDomain.on('change', () => + this.updateCheckboxOption('redirectBareDomain', this.$redirectBareDomain)); this.$showLoginInShare.on('change', () => this.updateCheckboxOption('showLoginInShareTheme', this.$showLoginInShare)); } optionsLoaded(options) { - this.$shareRedirectUrl.val(options.shareRedirectUrl || 'share'); - this.$loginRedirectUrl.val(options.loginRedirectUrl || 'login'); + this.setCheckboxState(this.$redirectBareDomain, options.redirectBareDomain); this.setCheckboxState(this.$showLoginInShare, options.showLoginInShareTheme); } } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 76236412c..26a8f533d 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1562,12 +1562,10 @@ "clipped-from": "Clipped from: %s" }, "share": { - "settings_title": "Share and Login Settings", - "redirect_url_label": "Share Redirect URL", - "redirect_url_description": "URL to redirect to when user is not logged in (default: 'share')", - "login_redirect_url_label": "Login Redirect URL", - "login_redirect_url_description": "URL to redirect to for login page (default: '/login')", - "show_login_label": "Show login link in share theme", - "show_login_description": "When enabled, displays a login link in the share theme interface (default: false)" + "settings_title": "Share Settings", + "redirect_bare_domain": "Redirect bare domain to share", + "redirect_bare_domain_description": "When enabled, accessing the root URL (/) will redirect to the share page (/share)", + "show_login_in_share": "Show login in share theme", + "show_login_in_share_description": "When enabled, shows a login button in the share theme" } } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 52faa1b57..1e11f65cb 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -54,7 +54,7 @@ import clipperRoute from "./api/clipper.js"; import similarNotesRoute from "./api/similar_notes.js"; import keysRoute from "./api/keys.js"; import backendLogRoute from "./api/backend_log.js"; -import statsRoute from "./api/stats.js"; +import statsRoute from "../stats/stats.js"; import fontsRoute from "./api/fonts.js"; import etapiTokensApiRoutes from "./api/etapi_tokens.js"; import relationMapApiRoute from "./api/relation-map.js"; @@ -102,6 +102,15 @@ const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: e }; function register(app: express.Application) { + // Add bare domain redirect handler + app.use((req, res, next) => { + if (req.path === '/' && optionService.getOption('redirectBareDomain')) { + res.redirect('/share'); + } else { + next(); + } + }); + route(GET, '/', [auth.checkAuth, csrfMiddleware], indexRoute.index); route(GET, '/login', [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); route(GET, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage); @@ -110,6 +119,9 @@ function register(app: express.Application) { windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // limit each IP to 10 requests per windowMs skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted! + message: 'Too many login attempts, please try again later.', + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false // Disable the `X-RateLimit-*` headers }); route(PST, '/login', [loginRateLimiter], loginRoute.login);