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!)
This commit is contained in:
matt wilkie
2024-11-24 08:57:35 -07:00
parent 025d33627b
commit cf3e476ebf
3 changed files with 32 additions and 40 deletions

View File

@@ -6,31 +6,19 @@ const TPL = `
<h4>${t('share.settings_title')}</h4>
<div class="form-group">
<label>${t('share.redirect_url_label')}</label>
<input type="text"
class="share-redirect-url form-control"
name="shareRedirectUrl"
placeholder="share"/>
<p>${t('share.redirect_url_description')}</p>
<div class="custom-control custom-checkbox">
<input class="custom-control-input redirect-bare-domain" type="checkbox" id="redirect-bare-domain">
<label class="custom-control-label" for="redirect-bare-domain">${t('share.redirect_bare_domain')}</label>
<p>${t('share.redirect_bare_domain_description')}</p>
</div>
</div>
<div class="form-group">
<label>${t('share.login_redirect_url_label')}</label>
<input type="text"
class="login-redirect-url form-control"
name="loginRedirectUrl"
placeholder="login"/>
<p>${t('share.login_redirect_url_description')}</p>
</div>
<div class="form-group">
<label class="form-check">
<input type="checkbox"
class="show-login-in-share form-check-input"
name="showLoginInShareTheme"/>
${t('share.show_login_label')}
</label>
<p>${t('share.show_login_description')}</p>
<div class="custom-control custom-checkbox">
<input class="custom-control-input show-login-in-share" type="checkbox" id="showLoginInShare">
<label class="custom-control-label" for="showLoginInShare">${t('share.show_login_in_share')}</label>
<p>${t('share.show_login_in_share_description')}</p>
</div>
</div>
</div>
`;
@@ -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);
}
}

View File

@@ -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"
}
}

View File

@@ -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);