From 8487c8cd039eb87b8723a970b7741f9568775971 Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Tue, 25 Feb 2025 14:51:29 -0700 Subject: [PATCH 1/4] WIP: allow no share path url prefix at all (not working yet) --- .../options/other/share_settings.ts | 17 ++++++++++ src/public/translations/en/translation.json | 5 ++- src/routes/api/options.ts | 4 ++- src/routes/routes.ts | 2 ++ src/services/auth.ts | 34 ++++++++++++++++++- src/services/options_init.ts | 4 ++- src/services/options_interface.ts | 2 ++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index e43adc5e18..ac5baa6afa 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -14,11 +14,24 @@ const TPL = `

${t("share.redirect_bare_domain_description")}

+ +

${t("share.show_login_link_description")}

+ +
+ +

${t("share.use_clean_urls_description")}

+
`; export default class ShareSettingsOptions extends OptionsWidget { @@ -59,6 +72,7 @@ export default class ShareSettingsOptions extends OptionsWidget { } this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked", options.showLoginInShareTheme === "true"); + this.$widget.find('input[name="useCleanUrls"]').prop("checked", options.useCleanUrls === "true"); } async checkShareRoot() { @@ -93,5 +107,8 @@ export default class ShareSettingsOptions extends OptionsWidget { const showLoginInShareTheme = this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked"); await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); + + const useCleanUrls = this.$widget.find('input[name="useCleanUrls"]').prop("checked"); + await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); } } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 4c0220d9ce..36eb03f19f 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1670,7 +1670,10 @@ "check_share_root": "Check Share Root Status", "share_root_found": "Share root note '{{noteTitle}}' is ready", "share_root_not_found": "No note with #shareRoot label found", - "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared" + "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", + "use_clean_urls": "Use clean URLs for shared notes", + "use_clean_urls_description": "When enabled, shared note URLs will be simplified from /share/STi3RCMhUvG6 to /STi3RCMhUvG6", + "share_subtree": "Share subtree" }, "time_selector": { "invalid_input": "The entered time value is not a valid number.", diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 50ff6b6b69..48097cf8f2 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -77,7 +77,9 @@ const ALLOWED_OPTIONS = new Set([ "backgroundEffects", "allowedHtmlTags", "redirectBareDomain", - "showLoginInShareTheme" + "showLoginInShareTheme", + "shareSubtree", + "useCleanUrls" ]); function getOptions() { diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 7d5fea44b4..51ac12bf87 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -100,6 +100,8 @@ const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: e }; function register(app: express.Application) { + app.use(auth.checkCleanUrl); + 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); diff --git a/src/services/auth.ts b/src/services/auth.ts index 03f40e6e7b..05c699f557 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -22,16 +22,47 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { // Check if any note has the #shareRoot label const shareRootNotes = attributes.getNotesWithLabel("shareRoot"); if (shareRootNotes.length === 0) { + // should this be a translation string? res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); return; } } + // not sure about this. 'share' is dynamic, whatever is turned up by shareRootNote res.redirect(redirectToShare ? "share" : "login"); } else { next(); } } +/** + * Checks if a URL path might be a shared note ID when clean URLs are enabled + */ +function checkCleanUrl(req: Request, res: Response, next: NextFunction) { + // Only process if not logged in and clean URLs are enabled + if (!req.session.loggedIn && !isElectron && !noAuthentication && + options.getOptionBool("redirectBareDomain") && + options.getOptionBool("useCleanUrls")) { + + // Get path without leading slash + const path = req.path.substring(1); + + // Skip processing for known routes and empty paths + if (!path || path === 'login' || path === 'setup' || path.startsWith('share/') || path.startsWith('api/')) { + next(); + return; + } + + // Redirect to the share URL with this ID + // broken, we don't know what `/share/` will be. + // oh! we need to add "what should be share path url?" to settings, + // require path begin with slash + // and allow bare `/` as an answer + res.redirect(`/share/${path}`); + } else { + next(); + } +} + // for electron things which need network stuff // currently, we're doing that for file upload because handling form data seems to be difficult function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { @@ -134,5 +165,6 @@ export default { checkAppNotInitialized, checkApiAuthOrElectron, checkEtapiToken, - checkCredentials + checkCredentials, + checkCleanUrl }; diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 77d4089ae4..8895ddd779 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -259,7 +259,9 @@ const defaultOptions: DefaultOption[] = [ // Share settings { name: "redirectBareDomain", value: "false", isSynced: true }, - { name: "showLoginInShareTheme", value: "false", isSynced: true } + { name: "showLoginInShareTheme", value: "false", isSynced: true }, + { name: "useCleanUrls", value: "false", isSynced: true }, + { name: "shareSubtree", value: "false", isSynced: true } ]; /** diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index d8d8c3fcbb..1ed5c361dc 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -100,6 +100,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions Date: Wed, 26 Feb 2025 03:10:55 -0700 Subject: [PATCH 2/4] WIP: options page works, but routing broken when logged out --- .../options/other/share_settings.ts | 84 +++++++++++++------ src/public/translations/en/translation.json | 6 +- src/routes/api/options.ts | 3 +- src/routes/login.ts | 3 +- src/services/auth.ts | 64 ++++++++++---- src/services/options_init.ts | 11 ++- src/services/options_interface.ts | 1 + src/share/routes.ts | 74 +++++++++++----- 8 files changed, 181 insertions(+), 65 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index ac5baa6afa..74da46cf08 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -9,7 +9,7 @@ const TPL = `

${t("share.title")}

${t("share.redirect_bare_domain_description")}

@@ -20,21 +20,33 @@ const TPL = ` +

${t("share.use_clean_urls_description")}

+ +
+ +
+ +
+
+ ${t("share.share_path_description")} +
+
+ +

${t("share.show_login_link_description")}

- -
- -

${t("share.use_clean_urls_description")}

-
`; export default class ShareSettingsOptions extends OptionsWidget { + private $redirectBareDomain!: JQuery; + private $showLoginInShareTheme!: JQuery; + private $useCleanUrls!: JQuery; + private $sharePath!: JQuery; private $shareRootCheck!: JQuery; private $shareRootStatus!: JQuery; @@ -42,37 +54,54 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$widget = $(TPL); this.contentSized(); + this.$redirectBareDomain = this.$widget.find(".redirect-bare-domain"); + this.$showLoginInShareTheme = this.$widget.find(".show-login-in-share-theme"); + this.$useCleanUrls = this.$widget.find(".use-clean-urls"); + this.$sharePath = this.$widget.find(".share-path"); this.$shareRootCheck = this.$widget.find('.share-root-check'); this.$shareRootStatus = this.$widget.find('.share-root-status'); - // Add change handlers for both checkboxes - this.$widget.find('input[type="checkbox"]').on("change", (e: JQuery.ChangeEvent) => { + this.$redirectBareDomain.on('change', () => { + const redirectBareDomain = this.$redirectBareDomain.is(":checked"); this.save(); // Show/hide share root status section based on redirectBareDomain checkbox - const target = e.target as HTMLInputElement; - if (target.name === 'redirectBareDomain') { - this.$shareRootCheck.toggle(target.checked); - if (target.checked) { - this.checkShareRoot(); - } + this.$shareRootCheck.toggle(redirectBareDomain); + if (redirectBareDomain) { + this.checkShareRoot(); } }); + this.$showLoginInShareTheme.on('change', () => { + const showLoginInShareTheme = this.$showLoginInShareTheme.is(":checked"); + this.save(); + }); + + this.$useCleanUrls.on('change', () => { + const useCleanUrls = this.$useCleanUrls.is(":checked"); + this.save(); + }); + + this.$sharePath.on('change', () => { + const sharePath = this.$sharePath.val() as string; + this.save(); + }); + // Add click handler for check share root button this.$widget.find('.check-share-root').on("click", () => this.checkShareRoot()); } async optionsLoaded(options: OptionMap) { const redirectBareDomain = options.redirectBareDomain === "true"; - this.$widget.find('input[name="redirectBareDomain"]').prop("checked", redirectBareDomain); + this.$redirectBareDomain.prop("checked", redirectBareDomain); this.$shareRootCheck.toggle(redirectBareDomain); if (redirectBareDomain) { await this.checkShareRoot(); } - this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked", options.showLoginInShareTheme === "true"); - this.$widget.find('input[name="useCleanUrls"]').prop("checked", options.useCleanUrls === "true"); + this.$showLoginInShareTheme.prop("checked", options.showLoginInShareTheme === "true"); + this.$useCleanUrls.prop("checked", options.useCleanUrls === "true"); + this.$sharePath.val(options.sharePath); } async checkShareRoot() { @@ -102,13 +131,20 @@ export default class ShareSettingsOptions extends OptionsWidget { } async save() { - const redirectBareDomain = this.$widget.find('input[name="redirectBareDomain"]').prop("checked"); + const redirectBareDomain = this.$redirectBareDomain.is(":checked"); await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString()); - const showLoginInShareTheme = this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked"); + const showLoginInShareTheme = this.$showLoginInShareTheme.is(":checked"); await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); - const useCleanUrls = this.$widget.find('input[name="useCleanUrls"]').prop("checked"); + const useCleanUrls = this.$useCleanUrls.is(":checked"); await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); + + // Ensure sharePath always starts with a slash + let sharePath = this.$sharePath.val() as string; + if (sharePath && !sharePath.startsWith('/')) { + sharePath = '/' + sharePath; + } + await this.updateOption<"sharePath">("sharePath", sharePath); } } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 36eb03f19f..a776954ff6 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1673,7 +1673,11 @@ "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", "use_clean_urls": "Use clean URLs for shared notes", "use_clean_urls_description": "When enabled, shared note URLs will be simplified from /share/STi3RCMhUvG6 to /STi3RCMhUvG6", - "share_subtree": "Share subtree" + "share_path": "Share path", + "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' and '/' --> '/noteId')", + "share_path_placeholder": "/share or / for root", + "share_subtree": "Share subtree", + "share_subtree_description": "Share the entire subtree, not just the note" }, "time_selector": { "invalid_input": "The entered time value is not a valid number.", diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 48097cf8f2..b2ccfa104a 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -79,7 +79,8 @@ const ALLOWED_OPTIONS = new Set([ "redirectBareDomain", "showLoginInShareTheme", "shareSubtree", - "useCleanUrls" + "useCleanUrls", + "sharePath" ]); function getOptions() { diff --git a/src/routes/login.ts b/src/routes/login.ts index 68b98e8939..18ecb9fe93 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -46,7 +46,8 @@ function setPassword(req: Request, res: Response) { if (error) { res.render("set_password", { error, - assetPath: assetPath + assetPath: assetPath, + appPath: appPath }); return; } diff --git a/src/services/auth.ts b/src/services/auth.ts index 05c699f557..c18a23ff58 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -26,9 +26,23 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); return; } + + // Get the configured share path + const sharePath = options.getOption("sharePath") || '/share'; + + // Check if we're already at the share path to prevent redirect loops + if (req.path === sharePath || req.path.startsWith(`${sharePath}/`)) { + log.info(`checkAuth: Already at share path, skipping redirect. Path: ${req.path}, SharePath: ${sharePath}`); + next(); + return; + } + + // Redirect to the share path + log.info(`checkAuth: Redirecting to share path. From: ${req.path}, To: ${sharePath}`); + res.redirect(sharePath); + } else { + res.redirect("login"); } - // not sure about this. 'share' is dynamic, whatever is turned up by shareRootNote - res.redirect(redirectToShare ? "share" : "login"); } else { next(); } @@ -43,38 +57,54 @@ function checkCleanUrl(req: Request, res: Response, next: NextFunction) { options.getOptionBool("redirectBareDomain") && options.getOptionBool("useCleanUrls")) { + // Get the configured share path + const sharePath = options.getOption("sharePath") || '/share'; + // Get path without leading slash const path = req.path.substring(1); - // Skip processing for known routes and empty paths - if (!path || path === 'login' || path === 'setup' || path.startsWith('share/') || path.startsWith('api/')) { + // Skip processing for known routes, empty paths, and paths that already start with sharePath + if (!path || + path === 'login' || + path === 'setup' || + path.startsWith('api/') || + req.path === sharePath || + req.path.startsWith(`${sharePath}/`)) { + log.info(`checkCleanUrl: Skipping redirect. Path: ${req.path}, SharePath: ${sharePath}`); + next(); + return; + } + + // If sharePath is just '/', we don't need to redirect + if (sharePath === '/') { + log.info(`checkCleanUrl: SharePath is root, skipping redirect. Path: ${req.path}`); next(); return; } // Redirect to the share URL with this ID - // broken, we don't know what `/share/` will be. - // oh! we need to add "what should be share path url?" to settings, - // require path begin with slash - // and allow bare `/` as an answer - res.redirect(`/share/${path}`); + log.info(`checkCleanUrl: Redirecting to share path. From: ${req.path}, To: ${sharePath}/${path}`); + res.redirect(`${sharePath}/${path}`); } else { next(); } } -// for electron things which need network stuff -// currently, we're doing that for file upload because handling form data seems to be difficult -function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { - if (!req.session.loggedIn && !isElectron && !noAuthentication) { +/** + * Middleware for API authentication - works for both sync and normal API + */ +function checkApiAuth(req: Request, res: Response, next: NextFunction) { + if (!req.session.loggedIn && !noAuthentication) { reject(req, res, "Logged in session not found"); } else { next(); } } -function checkApiAuth(req: Request, res: Response, next: NextFunction) { - if (!req.session.loggedIn && !noAuthentication) { +// for electron things which need network stuff +// currently, we're doing that for file upload because handling form data seems to be difficult +function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { + if (!req.session.loggedIn && !isElectron && !noAuthentication) { reject(req, res, "Logged in session not found"); } else { next(); @@ -158,6 +188,7 @@ function checkCredentials(req: Request, res: Response, next: NextFunction) { export default { checkAuth, + checkCleanUrl, checkApiAuth, checkAppInitialized, checkPasswordSet, @@ -165,6 +196,5 @@ export default { checkAppNotInitialized, checkApiAuthOrElectron, checkEtapiToken, - checkCredentials, - checkCleanUrl + checkCredentials }; diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 8895ddd779..b03f76aca9 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -124,7 +124,7 @@ const defaultOptions: DefaultOption[] = [ { name: "highlightsList", value: '["bold","italic","underline","color","bgColor"]', isSynced: true }, { name: "checkForUpdates", value: "true", isSynced: true }, { name: "disableTray", value: "false", isSynced: false }, - { name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days + { name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days { name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day { name: "customSearchEngineName", value: "DuckDuckGo", isSynced: true }, { name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true }, @@ -258,6 +258,15 @@ const defaultOptions: DefaultOption[] = [ }, // Share settings + { + name: "sharePath", + // ensure always starts with slash + value: (optionsMap) => { + const sharePath = optionsMap.sharePath || "/share"; + return sharePath.startsWith("/") ? sharePath : "/" + sharePath; + }, + isSynced: true + }, { name: "redirectBareDomain", value: "false", isSynced: true }, { name: "showLoginInShareTheme", value: "false", isSynced: true }, { name: "useCleanUrls", value: "false", isSynced: true }, diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index 1ed5c361dc..01faea5ec9 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -102,6 +102,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions { - if (req.path.substr(-1) !== "/") { - res.redirect("../share/"); - return; - } + let sharePath = options.getOption("sharePath") || '/share'; - shacaLoader.ensureLoad(); + // Handle root path specially + if (sharePath === '/') { + router.get('/', (req, res, next) => { + shacaLoader.ensureLoad(); - if (!shaca.shareRootNote) { - res.status(404).json({ message: "Share root note not found" }); - return; - } + if (!shaca.shareRootNote) { + res.status(404).json({ message: "Share root not found" }); + return; + } - renderNote(shaca.shareRootNote, req, res); - }); + renderNote(shaca.shareRootNote, req, res); + }); + } else { + router.get(`${sharePath}/`, (req, res, next) => { + if (req.path !== `${sharePath}/`) { + res.redirect(`${sharePath}/`); + return; + } + + shacaLoader.ensureLoad(); + + if (!shaca.shareRootNote) { + res.status(404).json({ message: "Share root not found" }); + return; + } + + renderNote(shaca.shareRootNote, req, res); + }); + } + + if (sharePath === '/' && options.getOptionBool("useCleanUrls") && options.getOptionBool("redirectBareDomain")) { + router.get("/:shareId", (req, res, next) => { + shacaLoader.ensureLoad(); + + const { shareId } = req.params; + + // Skip processing for known routes + if (shareId === 'login' || shareId === 'setup' || shareId.startsWith('api/')) { + next(); + return; + } + + const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; + + renderNote(note, req, res); + }); + } - router.get("/share/:shareId", (req, res, next) => { + router.get(`${sharePath}/:shareId`, (req, res, next) => { shacaLoader.ensureLoad(); const { shareId } = req.params; @@ -221,7 +255,7 @@ function register(router: Router) { renderNote(note, req, res); }); - router.get("/share/api/notes/:noteId", (req, res, next) => { + router.get(`${sharePath}/api/notes/:noteId`, (req, res, next) => { shacaLoader.ensureLoad(); let note: SNote | boolean; @@ -234,7 +268,7 @@ function register(router: Router) { res.json(note.getPojo()); }); - router.get("/share/api/notes/:noteId/download", (req, res, next) => { + router.get(`${sharePath}/api/notes/:noteId/download`, (req, res, next) => { shacaLoader.ensureLoad(); let note: SNote | boolean; @@ -256,7 +290,7 @@ function register(router: Router) { }); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - router.get("/share/api/images/:noteId/:filename", (req, res, next) => { + router.get(`${sharePath}/api/images/:noteId/:filename`, (req, res, next) => { shacaLoader.ensureLoad(); let image: SNote | boolean; @@ -282,7 +316,7 @@ function register(router: Router) { }); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - router.get("/share/api/attachments/:attachmentId/image/:filename", (req, res, next) => { + router.get(`${sharePath}/api/attachments/:attachmentId/image/:filename`, (req, res, next) => { shacaLoader.ensureLoad(); let attachment: SAttachment | boolean; @@ -300,7 +334,7 @@ function register(router: Router) { } }); - router.get("/share/api/attachments/:attachmentId/download", (req, res, next) => { + router.get(`${sharePath}/api/attachments/:attachmentId/download`, (req, res, next) => { shacaLoader.ensureLoad(); let attachment: SAttachment | boolean; @@ -322,7 +356,7 @@ function register(router: Router) { }); // used for PDF viewing - router.get("/share/api/notes/:noteId/view", (req, res, next) => { + router.get(`${sharePath}/api/notes/:noteId/view`, (req, res, next) => { shacaLoader.ensureLoad(); let note: SNote | boolean; @@ -340,7 +374,7 @@ function register(router: Router) { }); // Used for searching, require noteId so we know the subTreeRoot - router.get("/share/api/notes", (req, res, next) => { + router.get(`${sharePath}/api/notes`, (req, res, next) => { shacaLoader.ensureLoad(); const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; From bba2f6db64ddd1f5dda3437d03ca70a9ebc6546e Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Sat, 8 Mar 2025 09:35:52 -0700 Subject: [PATCH 3/4] wip: another attempt (and use translation syntax this time) --- .../app/widgets/type_widgets/options/other/share_settings.ts | 2 +- src/services/auth.ts | 2 +- src/share/routes.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 74da46cf08..0a59a317c2 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -28,7 +28,7 @@ const TPL = `
- +
${t("share.share_path_description")} diff --git a/src/services/auth.ts b/src/services/auth.ts index c18a23ff58..a2ab40b879 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -39,7 +39,7 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { // Redirect to the share path log.info(`checkAuth: Redirecting to share path. From: ${req.path}, To: ${sharePath}`); - res.redirect(sharePath); + res.redirect(`${sharePath}/`); } else { res.redirect("login"); } diff --git a/src/share/routes.ts b/src/share/routes.ts index 71c6b79705..1181692312 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -210,6 +210,11 @@ function register(router: Router) { renderNote(shaca.shareRootNote, req, res); }); } else { + router.get(`${sharePath}`, (req, res, next) => { + // Redirect to the path with trailing slash for consistency + res.redirect(`${sharePath}/`); + }); + router.get(`${sharePath}/`, (req, res, next) => { if (req.path !== `${sharePath}/`) { res.redirect(`${sharePath}/`); From 81d2fbc057d30a10208ce09f2223340c1de32424 Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:54:54 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=F0=9F=90=9B=20add=20back=20missing?= =?UTF-8?q?=20translation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/translations/en/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 50ace1d9ba..b6328a00b3 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -744,7 +744,8 @@ "basic_properties": { "note_type": "Note type", "editable": "Editable", - "basic_properties": "Basic Properties" + "basic_properties": "Basic Properties", + "language": "Language" }, "book_properties": { "view_type": "View type",