diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index 5a21313421..6b2b2df729 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1743,9 +1743,17 @@ "show_login_link": "在共享主题中显示登录链接", "show_login_link_description": "在共享页面底部添加登录链接", "check_share_root": "检查共享根状态", + "check_share_root_error": "检查共享根状态时发生意外错误,请检查日志以获取更多信息。", + "share_note_title": "'{{noteTitle}}'", "share_root_found": "共享根笔记 '{{noteTitle}}' 已准备好", "share_root_not_found": "未找到带有 #shareRoot 标签的笔记", - "share_root_not_shared": "笔记 '{{noteTitle}}' 具有 #shareRoot 标签,但未共享" + "share_root_not_shared": "笔记 '{{noteTitle}}' 具有 #shareRoot 标签,但未共享", + "share_root_multiple_found": "找到多个具有 #shareRoot 标签的共享笔记:{{- foundNoteTitles}}。将使用笔记 {{- activeNoteTitle}} 作为共享根笔记。", + "share_path": "共享路径", + "share_path_description": "共享笔记的 URL 前缀(例如 '/share' --> '/share/noteId' 或 '/custom-path' --> '/custom-path/noteId')。支持多级嵌套(例如 '/custom-path/sub-path' --> '/custom-path/sub-path/noteId')。刷新页面以应用更改。", + "share_path_placeholder": "/share 或 /custom-path", + "share_subtree": "共享子树", + "share_subtree_description": "共享整个子树,而不是仅共享笔记" }, "time_selector": { "invalid_input": "输入的时间值不是有效数字。", diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 2f60eb8c3e..fb3d3d34d1 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1907,9 +1907,17 @@ "show_login_link": "Show Login link in Share theme", "show_login_link_description": "Add a login link to the Share page footer", "check_share_root": "Check Share Root Status", + "check_share_root_error": "An unexpected error happened while checking the Share Root Status, please check the logs for more information.", + "share_note_title": "'{{noteTitle}}'", "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", + "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", + "share_path": "Share path", + "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId'). Multiple levels of nesting are supported (e.g. '/custom-path/sub-path' --> '/custom-path/sub-path/noteId'). Refresh the page to apply the changes.", + "share_path_placeholder": "/share or /custom-path", + "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/apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts b/apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts new file mode 100644 index 0000000000..2ff042b1b2 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts @@ -0,0 +1,13 @@ +// Ensure sharePath always starts with a single slash and does not end with (one or multiple) trailing slashes +export function normalizeSharePathInput(sharePathInput: string) { + const REGEXP_STARTING_SLASH = /^\/+/g; + const REGEXP_TRAILING_SLASH = /\b\/+$/g; + + const normalizedSharePath = (!sharePathInput.startsWith("/") + ? `/${sharePathInput}` + : sharePathInput) + .replaceAll(REGEXP_TRAILING_SLASH, "") + .replaceAll(REGEXP_STARTING_SLASH, "/"); + + return normalizedSharePath; +} diff --git a/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts b/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts new file mode 100644 index 0000000000..aa9c4efc5b --- /dev/null +++ b/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { normalizeSharePathInput } from "./share_path_utils.js"; + +type TestCase any> = [ + desc: string, + fnParams: Parameters, + expected: ReturnType +]; + +describe("ShareSettingsOptions", () => { + + describe("#normalizeSharePathInput", () => { + + const testCases: TestCase[] = [ + [ + "should handle multiple trailing '/' and remove them completely", + ["/trailingtest////"], + "/trailingtest" + ], + [ + "should handle multiple starting '/' and replace them by a single '/'", + ["////startingtest"], + "/startingtest" + ], + [ + "should handle multiple starting & trailing '/' and replace them by a single '/'", + ["////startingAndTrailingTest///"], + "/startingAndTrailingTest" + ], + [ + "should not remove any '/' other than at the end or start of the input", + ["/test/with/subpath"], + "/test/with/subpath" + ], + [ + "should prepend the string with a '/' if it does not start with one", + ["testpath"], + "/testpath" + ], + [ + "should not change anything, if the string is a single '/'", + ["/"], + "/" + ], + ]; + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = normalizeSharePathInput(...fnParams); + expect(actual).toStrictEqual(expected); + }); + }); + + + }) + +}) \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html index 95cae2d8d8..723e4d3b0e 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html @@ -6,8 +6,7 @@ - -

Features, interaction and limitations

+

Features, interaction and limitations

  • Searching by note title.
  • Automatic dark/light mode based on the user's browser settings.
  • @@ -189,11 +188,9 @@

    Sharing a note

    Share Note

    -
  • -

    Access the Shared Note: The link provided will open the - note in your browser. If your server is not configured with a public IP, - the URL will refer to localhost (127.0.0.1).

    -
  • +
  • Access the Shared Note: The link provided will open the + note in your browser. If your server is not configured with a public IP, + the URL will refer to localhost (127.0.0.1).
  • Sharing a note subtree

    When you share a note, you actually share the entire subtree of notes diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index 8e9d6e1a69..0acc301112 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -97,6 +97,8 @@ const ALLOWED_OPTIONS = new Set([ "allowedHtmlTags", "redirectBareDomain", "showLoginInShareTheme", + "shareSubtree", + "sharePath", "splitEditorOrientation", "seenCallToActions", diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index f1aeb92097..641970842e 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -80,6 +80,7 @@ const GET = "get", DEL = "delete"; function register(app: express.Application) { + 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/apps/server/src/services/auth.ts b/apps/server/src/services/auth.ts index b10ef80977..973b117ff3 100644 --- a/apps/server/src/services/auth.ts +++ b/apps/server/src/services/auth.ts @@ -37,9 +37,26 @@ 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; } + + // 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"); } res.redirect(hasRedirectBareDomain ? "share" : "login"); } else if (currentTotpStatus !== lastAuthState.totpEnabled || currentSsoStatus !== lastAuthState.ssoEnabled) { @@ -81,15 +98,6 @@ function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) } } -function checkApiAuth(req: Request, res: Response, next: NextFunction) { - if (!req.session.loggedIn && !noAuthentication) { - console.warn(`Missing session with ID '${req.sessionID}'.`); - reject(req, res, "Logged in session not found"); - } else { - next(); - } -} - function checkAppInitialized(req: Request, res: Response, next: NextFunction) { if (!sqlInit.isDbInitialized()) { res.redirect("setup"); diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts index 0908a05126..2154230757 100644 --- a/apps/server/src/services/options_init.ts +++ b/apps/server/src/services/options_init.ts @@ -196,8 +196,10 @@ const defaultOptions: DefaultOption[] = [ }, // Share settings + { name: "sharePath", value: "/share", isSynced: true }, { name: "redirectBareDomain", value: "false", isSynced: true }, { name: "showLoginInShareTheme", value: "false", isSynced: true }, + { name: "shareSubtree", value: "false", isSynced: true }, // AI Options { name: "aiEnabled", value: "false", isSynced: true }, diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 83ddfde2e5..acd4bb451b 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -32,7 +32,7 @@ export function getContent(note: SNote) { }; if (note.type === "text") { - renderText(result, note); + renderText(result, note, relativePath); } else if (note.type === "code") { renderCode(result); } else if (note.type === "mermaid") { @@ -106,10 +106,10 @@ function renderText(result: Result, note: SNote) { if (result.content.includes(``)) { result.header += ` - - - - + + + +