diff --git a/src/public/app/widgets/type_widgets/content_widget.ts b/src/public/app/widgets/type_widgets/content_widget.ts index 15a4d23fa..0e9f3ca22 100644 --- a/src/public/app/widgets/type_widgets/content_widget.ts +++ b/src/public/app/widgets/type_widgets/content_widget.ts @@ -36,6 +36,7 @@ import RibbonOptions from "./options/appearance/ribbon.js"; import LocalizationOptions from "./options/appearance/i18n.js"; import CodeBlockOptions from "./options/appearance/code_block.js"; import EditorOptions from "./options/text_notes/editor.js"; +import ShareSettingsOptions from "./options/other/share_settings.js"; import type FNote from "../../entities/fnote.js"; import type NoteContextAwareWidget from "../note_context_aware_widget.js"; @@ -77,14 +78,14 @@ const CONTENT_WIDGETS: Record = { RevisionsSnapshotIntervalOptions, RevisionSnapshotsLimitOptions, NetworkConnectionsOptions, - HtmlImportTagsOptions + HtmlImportTagsOptions, + ShareSettingsOptions ], _optionsAdvanced: [DatabaseIntegrityCheckOptions, DatabaseAnonymizationOptions, AdvancedSyncOptions, VacuumDatabaseOptions], _backendLog: [BackendLogWidget] }; export default class ContentWidgetTypeWidget extends TypeWidget { - private $content!: JQuery; static getType() { 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 new file mode 100644 index 000000000..e43adc5e1 --- /dev/null +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -0,0 +1,97 @@ +import OptionsWidget from "../options_widget.js"; +import options from "../../../../services/options.js"; +import { t } from "../../../../services/i18n.js"; +import type { OptionMap, OptionNames } from "../../../../../../services/options_interface.js"; +import searchService from "../../../../services/search.js"; + +const TPL = ` +
+

${t("share.title")}

+ + +

${t("share.redirect_bare_domain_description")}

+ + +

${t("share.show_login_link_description")}

+
`; + +export default class ShareSettingsOptions extends OptionsWidget { + private $shareRootCheck!: JQuery; + private $shareRootStatus!: JQuery; + + doRender() { + this.$widget = $(TPL); + this.contentSized(); + + 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.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(); + } + } + }); + + // 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.$shareRootCheck.toggle(redirectBareDomain); + if (redirectBareDomain) { + await this.checkShareRoot(); + } + + this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked", options.showLoginInShareTheme === "true"); + } + + async checkShareRoot() { + const $button = this.$widget.find('.check-share-root'); + $button.prop('disabled', true); + + try { + const shareRootNotes = await searchService.searchForNotes("#shareRoot"); + const sharedShareRootNote = shareRootNotes.find(note => note.isShared()); + + if (sharedShareRootNote) { + this.$shareRootStatus + .removeClass('text-danger') + .addClass('text-success') + .text(t("share.share_root_found", {noteTitle: sharedShareRootNote.title})); + } else { + this.$shareRootStatus + .removeClass('text-success') + .addClass('text-danger') + .text(shareRootNotes.length > 0 + ? t("share.share_root_not_shared", {noteTitle: shareRootNotes[0].title}) + : t("share.share_root_not_found")); + } + } finally { + $button.prop('disabled', false); + } + } + + async save() { + const redirectBareDomain = this.$widget.find('input[name="redirectBareDomain"]').prop("checked"); + await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString()); + + const showLoginInShareTheme = this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked"); + await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); + } +} diff --git a/src/public/stylesheets/theme-next/forms.css b/src/public/stylesheets/theme-next/forms.css index a74386b63..6c67941da 100644 --- a/src/public/stylesheets/theme-next/forms.css +++ b/src/public/stylesheets/theme-next/forms.css @@ -199,6 +199,11 @@ input::selection, color: var(--input-selection-text-color); } +.form-text { + color: var(--main-text-color); + opacity: 0.8; +} + /* Input groups */ .input-group { diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 9d60673eb..ea7dc176e 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1656,6 +1656,17 @@ "hours": "Hours", "days": "Days" }, + "share": { + "title": "Share Settings", + "redirect_bare_domain": "Redirect bare domain to Share page", + "redirect_bare_domain_description": "Redirect anonymous users to the Share page instead of showing Login", + "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", + "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" + }, "time_selector": { "invalid_input": "The entered time value is not a valid number.", "minimum_input": "The entered time value needs to be at least {{minimumSeconds}} seconds." diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 6d9d9c613..50ff6b6b6 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -75,7 +75,9 @@ const ALLOWED_OPTIONS = new Set([ "textNoteEditorMultilineToolbar", "layoutOrientation", "backgroundEffects", - "allowedHtmlTags" // Allow configuring HTML import tags + "allowedHtmlTags", + "redirectBareDomain", + "showLoginInShareTheme" ]); function getOptions() { diff --git a/src/services/auth.ts b/src/services/auth.ts index 3e8957100..03f40e6e7 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -7,6 +7,8 @@ import { isElectron } from "./utils.js"; import passwordEncryptionService from "./encryption/password_encryption.js"; import config from "./config.js"; import passwordService from "./encryption/password.js"; +import options from "./options.js"; +import attributes from "./attributes.js"; import type { NextFunction, Request, Response } from "express"; const noAuthentication = config.General && config.General.noAuthentication === true; @@ -15,7 +17,16 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { if (!sqlInit.isDbInitialized()) { res.redirect("setup"); } else if (!req.session.loggedIn && !isElectron && !noAuthentication) { - res.redirect("login"); + const redirectToShare = options.getOptionBool("redirectBareDomain"); + if (redirectToShare) { + // Check if any note has the #shareRoot label + const shareRootNotes = attributes.getNotesWithLabel("shareRoot"); + if (shareRootNotes.length === 0) { + res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); + return; + } + } + res.redirect(redirectToShare ? "share" : "login"); } else { next(); } diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 339327808..926599557 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -255,7 +255,11 @@ const defaultOptions: DefaultOption[] = [ "tt" ]), isSynced: true - } + }, + + // Share settings + { name: "redirectBareDomain", value: "false", isSynced: true }, + { name: "showLoginInShareTheme", value: "false", isSynced: true } ]; /** diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index 072bf6b6f..d8d8c3fcb 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -97,6 +97,10 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions <% } %> +