소스 검색

:sparkles: Support read-only publish service

* :art: kernel supports read-only publishing services

* :bug: Fix authentication vulnerabilities

* :art: Protect secret information

* :art: Adjust the permission control

* :art: Adjust the permission control

* :art: Fixed the vulnerability that `getFile` gets file `conf.json`

* :art: Add API `/api/setting/setPublish`

* :art: Add API `/api/setting/getPublish`

* :bug: Fixed the issue that PWA-related files could not pass BasicAuth

* :art: Add a settings panel for publishing features

* :memo: Add guide for `Publish Service`

* :memo: Update Japanese user guide

* :art: Merge fixed static file services
Yingyi / 颖逸 1 년 전
부모
커밋
ba2193403d
47개의 변경된 파일3702개의 추가작업 그리고 387개의 파일을 삭제
  1. 13 0
      app/appearance/langs/en_US.json
  2. 13 0
      app/appearance/langs/es_ES.json
  3. 13 0
      app/appearance/langs/fr_FR.json
  4. 14 1
      app/appearance/langs/ja_JP.json
  5. 13 0
      app/appearance/langs/zh_CHT.json
  6. 13 0
      app/appearance/langs/zh_CN.json
  7. 0 2
      app/guide/20210808180117-6v0mkxr/.gitignore
  8. 1 1
      app/guide/20210808180117-6v0mkxr/.siyuan/sort.json
  9. 526 0
      app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20240517031132-jmr5ihm.sy
  10. 0 2
      app/guide/20210808180117-czj9bvb/.gitignore
  11. 1 1
      app/guide/20210808180117-czj9bvb/.siyuan/sort.json
  12. 540 0
      app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20240517031028-xqinazo.sy
  13. 0 2
      app/guide/20211226090932-5lcq56f/.gitignore
  14. 1 1
      app/guide/20211226090932-5lcq56f/.siyuan/sort.json
  15. 540 0
      app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20240517031052-mgoxs16.sy
  16. 1 1
      app/guide/20240530133126-axarxgx/.siyuan/sort.json
  17. 676 0
      app/guide/20240530133126-axarxgx/20240530101000-4qitucx/20240530101000-g3ugxml/20240610233601-ylh1mvk.sy
  18. 6 4
      app/src/config/editor.ts
  19. 8 0
      app/src/config/index.ts
  20. 213 0
      app/src/config/publish.ts
  21. 26 15
      app/src/layout/util.ts
  22. 2 1
      app/src/menus/commonMenuItem.ts
  23. 6 2
      app/src/menus/workspace.ts
  24. 4 0
      app/src/protyle/util/compatibility.ts
  25. 55 0
      app/src/types/config.d.ts
  26. 4 2
      app/src/util/assets.ts
  27. 14 12
      app/src/util/fetch.ts
  28. 20 2
      kernel/api/file.go
  29. 258 257
      kernel/api/router.go
  30. 53 0
      kernel/api/setting.go
  31. 0 27
      kernel/api/snippet.go
  32. 13 1
      kernel/api/system.go
  33. 45 0
      kernel/conf/publish.go
  34. 1 0
      kernel/go.mod
  35. 2 0
      kernel/go.sum
  36. 133 0
      kernel/model/auth.go
  37. 23 0
      kernel/model/conf.go
  38. 56 0
      kernel/model/role.go
  39. 53 0
      kernel/model/session.go
  40. 1 15
      kernel/server/port.go
  41. 41 0
      kernel/server/proxy/fixedport.go
  42. 161 0
      kernel/server/proxy/publish.go
  43. 109 36
      kernel/server/serve.go
  44. 2 1
      kernel/sql/asset.go
  45. 23 0
      kernel/util/net.go
  46. 1 0
      kernel/util/path.go
  47. 4 1
      kernel/util/working.go

+ 13 - 0
app/appearance/langs/en_US.json

@@ -1,4 +1,17 @@
 {
+  "publish": "Publish",
+  "publishService": "Publish service",
+  "publishServiceNotStarted": "Publish Service Not Started",
+  "publishServiceTip": "When enabled, the publish service will be started. This service publishes the content of the current workspace in read-only mode on the local network.",
+  "publishServicePort": "Service port",
+  "publishServicePortTip": "Enable the publish service using the specified port number. If set to <code class='fn__code'>0</code>, a random port will be used.",
+  "publishServiceAddresses": "Service access addresses",
+  "publishServiceAddressesTip": "Possible network addresses to access the publish service.",
+  "publishServiceAuth": "Service basic authentication",
+  "publishServiceAuthTip": "When enabled, authentication is required to access the publish service.",
+  "publishServiceAuthAccounts": "Authenticated accounts",
+  "publishServiceAuthAccountsTip": "List of Basic authentication accounts. Visitors need to enter the username and password from this list to view the published content.",
+  "publishServiceAuthAccountAdd": "Add account",
   "copyMirror": "Copy mirror",
   "duplicateMirror": "Duplicate mirror",
   "duplicateCompletely": "Duplicate completely",

+ 13 - 0
app/appearance/langs/es_ES.json

@@ -1,4 +1,17 @@
 {
+  "publish": "Publicar",
+  "publishService": "Publicar servicio",
+  "publishServiceNotStarted": "Servicio de publicación no iniciado",
+  "publishServiceTip": "Al activar esto, se iniciará el servicio de publicación. Este servicio publicará el contenido del espacio de trabajo actual en modo de solo lectura en la LAN",
+  "publishServicePort": "Número de puerto del servicio",
+  "publishServicePortTip": "Activar el servicio de publicación con el número de puerto especificado. Si se establece en 0, se utilizará un puerto aleatorio",
+  "publishServiceAddresses": "Direcciones de acceso al servicio",
+  "publishServiceAddressesTip": "Direcciones de red desde las que se puede acceder al servicio de publicación",
+  "publishServiceAuth": "Autenticación básica del servicio",
+  "publishServiceAuthTip": "Al activar esto, se requerirá autenticación al acceder al servicio de publicación",
+  "publishServiceAuthAccounts": "Cuentas de autenticación",
+  "publishServiceAuthAccountsTip": "Lista de cuentas de autenticación básica. Después de activar la autenticación básica, los visitantes deberán ingresar el nombre de usuario y la contraseña de la lista para ver el contenido publicado",
+  "publishServiceAuthAccountAdd": "Agregar cuenta",
   "copyMirror": "Copiar espejo",
   "duplicateMirror": "Espejo duplicado",
   "duplicateCompletely": "Duplicar completamente",

+ 13 - 0
app/appearance/langs/fr_FR.json

@@ -1,4 +1,17 @@
 {
+  "publish": "Publier",
+  "publishService": "Publier le service",
+  "publishServiceNotStarted": "Service de publication non démarré",
+  "publishServiceTip": "Lorsqu'activé, le service de publication démarre. Ce service publie en mode lecture seule le contenu de l'espace de travail actuel dans le réseau local.",
+  "publishServicePort": "Numéro de port du service",
+  "publishServicePortTip": "Active le service de publication avec le numéro de port spécifié. Si défini sur 0, un port aléatoire sera utilisé.",
+  "publishServiceAddresses": "Adresses d'accès au service",
+  "publishServiceAddressesTip": "Adresses réseau qui peuvent accéder au service de publication",
+  "publishServiceAuth": "Authentification Basic du service",
+  "publishServiceAuthTip": "Lorsqu'activé, une authentification est requise pour accéder au service de publication",
+  "publishServiceAuthAccounts": "Comptes d'authentification",
+  "publishServiceAuthAccountsTip": "Liste des comptes d'authentification Basic. Lorsque l'authentification Basic est activée, les visiteurs doivent entrer un nom d'utilisateur et un mot de passe figurant dans cette liste pour consulter le contenu publié.",
+  "publishServiceAuthAccountAdd": "Ajouter un compte",
   "copyMirror": "Copier le miroir",
   "duplicateMirror": "Miroir en double",
   "duplicateCompletely": "Dupliquer complètement",

+ 14 - 1
app/appearance/langs/ja_JP.json

@@ -1,4 +1,17 @@
 {
+  "publish": "公開する",
+  "publishService": "サービスを公開する",
+  "publishServiceNotStarted": "サービスが開始されていません",
+  "publishServiceTip": "有効にすると、サービスを開始します。このサービスは、現在のワークスペースの内容を読み取り専用モードでローカルネットワークに公開します",
+  "publishServicePort": "サービスポート",
+  "publishServicePortTip": "指定したポート番号を使用してサービスを有効にします。0に設定するとランダムなポートが使用されます",
+  "publishServiceAddresses": "サービスアドレス",
+  "publishServiceAddressesTip": "サービスを公開することが可能なネットワークアドレス",
+  "publishServiceAuth": "サービスの基本認証",
+  "publishServiceAuthTip": "有効にすると、公開サービスへのアクセス時に認証が必要になります",
+  "publishServiceAuthAccounts": "認証アカウント",
+  "publishServiceAuthAccountsTip": "基本認証アカウントのリスト。基本認証を有効にした場合、訪問者はリスト内のユーザー名とパスワードを入力して公開内容を表示することができます",
+  "publishServiceAuthAccountAdd": "アカウントを追加する",
   "copyMirror": "ミラーをコピー",
   "duplicateMirror": "ミラーを複製",
   "duplicateCompletely": "完全に複製",
@@ -1523,4 +1536,4 @@
     "247": "ファイル [%s] は制限サイズ [%s] を超えているためアップロードされませんでした",
     "248": "目標の見出しがコンテナブロック内にあるためドロップできません"
   }
-}
+}

+ 13 - 0
app/appearance/langs/zh_CHT.json

@@ -1,4 +1,17 @@
 {
+  "publish": "發布",
+  "publishService": "發布服務",
+  "publishServiceNotStarted": "發布服務未啟動",
+  "publishServiceTip": "啟用後將啟動發布服務。該服務以只讀模式在區域網中發布當前工作空間的內容",
+  "publishServicePort": "服務端口號",
+  "publishServicePortTip": "使用指定的端口號啟用發布服務。若設置為 <code class='fn__code'>0</code> 則使用隨機端口",
+  "publishServiceAddresses": "服務訪問地址",
+  "publishServiceAddressesTip": "可能訪問到發布服務的網路地址",
+  "publishServiceAuth": "服務 Basic 認證",
+  "publishServiceAuthTip": "啟用後在訪問發布服務時需要進行認證",
+  "publishServiceAuthAccounts": "認證帳戶",
+  "publishServiceAuthAccountsTip": "Basic 認證帳戶列表。啟用 Basic 認證後訪問者輸入列表中的用戶名與密碼後才能查看發布的內容",
+  "publishServiceAuthAccountAdd": "添加帳戶",
   "copyMirror": "複製鏡像",
   "duplicateMirror": "複製為鏡像副本",
   "duplicateCompletely": "複製為完整副本",

+ 13 - 0
app/appearance/langs/zh_CN.json

@@ -1,4 +1,17 @@
 {
+  "publish": "发布",
+  "publishService": "发布服务",
+  "publishServiceNotStarted": "发布服务未启动",
+  "publishServiceTip": "启用后将启动发布服务。该服务以只读模式在局域网中发布当前工作空间的内容",
+  "publishServicePort": "服务端口号",
+  "publishServicePortTip": "使用指定的端口号启用发布服务。若设置为 <code class='fn__code'>0</code> 则使用随机端口",
+  "publishServiceAddresses": "服务访问地址",
+  "publishServiceAddressesTip": "可能访问到发布服务的网络地址",
+  "publishServiceAuth": "服务 Basic 认证",
+  "publishServiceAuthTip": "启用后访问者在访问发布服务时需要使用用户名与密码进行认证",
+  "publishServiceAuthAccounts": "认证账户",
+  "publishServiceAuthAccountsTip": "Basic 认证账户列表。访问者输入列表中的用户名与密码后才能查看发布的内容",
+  "publishServiceAuthAccountAdd": "添加账户",
   "copyMirror": "复制镜像",
   "duplicateMirror": "复制为镜像副本",
   "duplicateCompletely": "复制为完整副本",

+ 0 - 2
app/guide/20210808180117-6v0mkxr/.gitignore

@@ -1,2 +0,0 @@
-.idea/
-.siyuan/history/

+ 1 - 1
app/guide/20210808180117-6v0mkxr/.siyuan/sort.json

@@ -1 +1 @@
-{"20200923234011-ieuun1p":1,"20200923234602-gy54e67":8,"20200923234731-h3zkwm2":3,"20200924093441-ft2rhps":1,"20200924095938-a9p5450":2,"20200924100110-vcg96wy":1,"20200924100635-ms0p9lb":6,"20200924100717-yzwzn64":21,"20200924100744-br924ar":10,"20200924100808-j9sddk9":2,"20200924100906-0u4zfq3":4,"20200924100950-9op5xi1":18,"20200924101106-19z4kaa":1,"20200924101200-gss5vee":4,"20200924101225-k254i8g":2,"20200924101256-f8b1sbi":3,"20201004194026-s8h2cog":19,"20201117112518-dott91x":6,"20201121224345-rc27qvo":9,"20201204184532-3qm9l8n":11,"20201210233038-3xr19g5":5,"20201222100222-q47d64s":3,"20201222100339-i5hzcph":2,"20201227201128-m1wrouw":20,"20201227201751-gv0fpx2":22,"20210110181011-fbhoesf":5,"20210117215840-jcl17fx":4,"20210127203829-qe2mzof":12,"20210331201142-4g923es":14,"20210505164949-c085p1d":3,"20210613191509-cbkxcbz":7,"20210615213222-vs5tzbd":15,"20210721112159-9p645xm":1,"20210721112206-mhr9wxi":2,"20210721160238-yvhbh0h":4,"20210808180303-6yi0dv5":1,"20210808180303-axh6q1d":4,"20210808180303-h361q1i":2,"20210808180303-l3qg72k":3,"20210808180303-xaduj2o":5,"20210824202056-udkf7wg":8,"20211010212318-3wx2kqb":13,"20220105101227-n5zpr1a":6,"20220415232231-pqcizol":1,"20220628204454-hhxohv5":2,"20220708103401-mgydrfg":3,"20221016204105-qx2aq0g":3,"20221223221636-ms2b4w9":16,"20230104152135-1iei0xa":23,"20230106104821-9nfphwm":1,"20230304000547-ibldj1z":17,"20230405172236-pg3l9eu":6,"20230429115711-ejbts4s":5,"20230506205948-yah52eb":9,"20230802114825-2jkkct7":5,"20230805231614-vqn28eh":7,"20230805231816-h1z9mpc":2,"20230805232018-hgrq0ju":1,"20230805232134-3d6mx2k":2,"20240113110040-7sgw8kl":2,"20240119211017-1vbbt95":4,"20240119212048-0huuevw":5,"20240208172514-9dsv6na":7,"20240317202444-5txwumu":7}
+{"20200923234011-ieuun1p":1,"20200923234602-gy54e67":8,"20200923234731-h3zkwm2":3,"20200924093441-ft2rhps":1,"20200924095938-a9p5450":2,"20200924100110-vcg96wy":1,"20200924100635-ms0p9lb":6,"20200924100717-yzwzn64":21,"20200924100744-br924ar":10,"20200924100808-j9sddk9":2,"20200924100906-0u4zfq3":4,"20200924100950-9op5xi1":18,"20200924101106-19z4kaa":1,"20200924101200-gss5vee":4,"20200924101225-k254i8g":2,"20200924101256-f8b1sbi":3,"20201004194026-s8h2cog":19,"20201117112518-dott91x":6,"20201121224345-rc27qvo":9,"20201204184532-3qm9l8n":11,"20201210233038-3xr19g5":5,"20201222100222-q47d64s":3,"20201222100339-i5hzcph":2,"20201227201128-m1wrouw":20,"20201227201751-gv0fpx2":22,"20210110181011-fbhoesf":5,"20210117215840-jcl17fx":4,"20210127203829-qe2mzof":12,"20210331201142-4g923es":14,"20210505164949-c085p1d":3,"20210613191509-cbkxcbz":7,"20210615213222-vs5tzbd":15,"20210721112159-9p645xm":1,"20210721112206-mhr9wxi":2,"20210721160238-yvhbh0h":4,"20210808180303-6yi0dv5":1,"20210808180303-axh6q1d":4,"20210808180303-h361q1i":2,"20210808180303-l3qg72k":3,"20210808180303-xaduj2o":5,"20210824202056-udkf7wg":8,"20211010212318-3wx2kqb":13,"20220105101227-n5zpr1a":6,"20220415232231-pqcizol":1,"20220628204454-hhxohv5":2,"20220708103401-mgydrfg":3,"20221016204105-qx2aq0g":3,"20221223221636-ms2b4w9":16,"20230104152135-1iei0xa":23,"20230106104821-9nfphwm":1,"20230304000547-ibldj1z":17,"20230405172236-pg3l9eu":6,"20230429115711-ejbts4s":5,"20230506205948-yah52eb":9,"20230802114825-2jkkct7":5,"20230805231614-vqn28eh":7,"20230805231816-h1z9mpc":2,"20230805232018-hgrq0ju":1,"20230805232134-3d6mx2k":2,"20240113110040-7sgw8kl":2,"20240119211017-1vbbt95":4,"20240119212048-0huuevw":5,"20240208172514-9dsv6na":7,"20240317202444-5txwumu":7,"20240517031132-jmr5ihm":24}

+ 526 - 0
app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20240517031132-jmr5ihm.sy

@@ -0,0 +1,526 @@
+{
+	"ID": "20240517031132-jmr5ihm",
+	"Spec": "1",
+	"Type": "NodeDocument",
+	"Properties": {
+		"id": "20240517031132-jmr5ihm",
+		"title": "Publish service",
+		"type": "doc",
+		"updated": "20240517031304"
+	},
+	"Children": [
+		{
+			"ID": "20240517031304-um049u9",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031304-um049u9",
+				"updated": "20240517031304"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "Overview"
+				}
+			]
+		},
+		{
+			"ID": "20240517031304-kebcgzv",
+			"Type": "NodeParagraph",
+			"Properties": {
+				"id": "20240517031304-kebcgzv",
+				"updated": "20240517031305"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "Joplin supports publishing the content of the current workspace in read-only mode on the local area network."
+				}
+			]
+		},
+		{
+			"ID": "20240517031304-7e0kvj3",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031304-7e0kvj3",
+				"updated": "20240517031304"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "Usage"
+				}
+			]
+		},
+		{
+			"ID": "20240517031304-gt5qj60",
+			"Type": "NodeList",
+			"ListData": {},
+			"Properties": {
+				"id": "20240517031304-gt5qj60",
+				"updated": "20240517031305"
+			},
+			"Children": [
+				{
+					"ID": "20240517031304-sjppgk3",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031304-sjppgk3",
+						"updated": "20240517031304"
+					},
+					"Children": [
+						{
+							"ID": "20240517031304-r9a0anm",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031304-r9a0anm",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "Open "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "Settings"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ - "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "Publish"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ to enter the publishing service settings panel."
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031304-u6inl9h",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031304-u6inl9h",
+						"updated": "20240517031304"
+					},
+					"Children": [
+						{
+							"ID": "20240517031304-ye7ittw",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031304-ye7ittw",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "Set the "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "Server port"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								}
+							]
+						},
+						{
+							"ID": "20240517031304-olvdqhj",
+							"Type": "NodeList",
+							"ListData": {},
+							"Properties": {
+								"id": "20240517031304-olvdqhj",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"ID": "20240517031304-1ttdtl4",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031304-1ttdtl4",
+										"updated": "20240517031304"
+									},
+									"Children": [
+										{
+											"ID": "20240517031304-sl59b81",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031304-sl59b81",
+												"updated": "20240517031304"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "The default port number is "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "code",
+													"TextMarkTextContent": "6808"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​."
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240517031304-t37dfgj",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031304-t37dfgj",
+										"updated": "20240517031304"
+									},
+									"Children": [
+										{
+											"ID": "20240517031304-mxx5df4",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031304-mxx5df4",
+												"updated": "20240517031304"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "If the port number is set to "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "code",
+													"TextMarkTextContent": "0"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​, a random port will be used."
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031304-y4kcmaf",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031304-y4kcmaf",
+						"updated": "20240517031304"
+					},
+					"Children": [
+						{
+							"ID": "20240517031304-5qilsih",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031304-5qilsih",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "If access control is required for the publishing service:"
+								}
+							]
+						},
+						{
+							"ID": "20240517031304-659m8fi",
+							"Type": "NodeList",
+							"ListData": {},
+							"Properties": {
+								"id": "20240517031304-659m8fi",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"ID": "20240517031304-xh0kq2y",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031304-xh0kq2y",
+										"updated": "20240517031304"
+									},
+									"Children": [
+										{
+											"ID": "20240517031304-lay209u",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031304-lay209u",
+												"updated": "20240517031304"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "Add "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "Authenticated accounts"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ and enable the "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "Service basic authentication"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ switch."
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240517031304-ri1nm5j",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031304-ri1nm5j",
+										"updated": "20240517031304"
+									},
+									"Children": [
+										{
+											"ID": "20240517031304-4y267ml",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031304-4y267ml",
+												"updated": "20240517031304"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "When enabled, the publishing service will use the "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "a",
+													"TextMarkAHref": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme",
+													"TextMarkTextContent": "Basic authentication scheme"
+												},
+												{
+													"Type": "NodeText",
+													"Data": " to authenticate visitors."
+												}
+											]
+										},
+										{
+											"ID": "20240517031304-h61xxm7",
+											"Type": "NodeList",
+											"ListData": {},
+											"Properties": {
+												"id": "20240517031304-h61xxm7",
+												"updated": "20240517031304"
+											},
+											"Children": [
+												{
+													"ID": "20240517031304-1k96w0z",
+													"Type": "NodeListItem",
+													"ListData": {
+														"BulletChar": 42,
+														"Marker": "Kg=="
+													},
+													"Properties": {
+														"id": "20240517031304-1k96w0z",
+														"updated": "20240517031304"
+													},
+													"Children": [
+														{
+															"ID": "20240517031304-l5hafpz",
+															"Type": "NodeParagraph",
+															"Properties": {
+																"id": "20240517031304-l5hafpz",
+																"updated": "20240517031304"
+															},
+															"Children": [
+																{
+																	"Type": "NodeText",
+																	"Data": "Visitors must enter the username and password set in the \"Authentication Accounts\" before browsing the published content."
+																}
+															]
+														}
+													]
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031304-0nps2p8",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031304-0nps2p8",
+						"updated": "20240517031304"
+					},
+					"Children": [
+						{
+							"ID": "20240517031304-y16fc8r",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031304-y16fc8r",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "Enable the "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "Publish service"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ switch."
+								}
+							]
+						}
+					]
+				}
+			]
+		},
+		{
+			"ID": "20240517031304-6cy4lbq",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031304-6cy4lbq",
+				"updated": "20240517031304"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "Note"
+				}
+			]
+		},
+		{
+			"ID": "20240517031304-3slh48m",
+			"Type": "NodeList",
+			"ListData": {},
+			"Properties": {
+				"id": "20240517031304-3slh48m",
+				"updated": "20240517031305"
+			},
+			"Children": [
+				{
+					"ID": "20240517031304-uquw6i6",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031304-uquw6i6",
+						"updated": "20240517031304"
+					},
+					"Children": [
+						{
+							"ID": "20240517031304-j38u7hu",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031304-j38u7hu",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "After enabling the publishing service, visitors can browse the content of the entire workspace."
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031304-c6iqquc",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031304-c6iqquc",
+						"updated": "20240517031304"
+					},
+					"Children": [
+						{
+							"ID": "20240517031304-1qsa7qz",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031304-1qsa7qz",
+								"updated": "20240517031304"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "When "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "Service basic authentication"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ is disabled, all visitors can browse the content of the entire workspace without authentication."
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	]
+}

+ 0 - 2
app/guide/20210808180117-czj9bvb/.gitignore

@@ -1,2 +0,0 @@
-.idea/
-.siyuan/history/

+ 1 - 1
app/guide/20210808180117-czj9bvb/.siyuan/sort.json

@@ -1 +1 @@
-{"20200812220555-lj3enxa":1,"20200813004551-gm0pbn1":18,"20200813004931-q4cu8na":1,"20200813013559-sgbzl5k":3,"20200813093015-u6bopdt":3,"20200813125307-pxsjela":2,"20200813131152-0wk5akh":4,"20200813163359-v04n73b":10,"20200822191536-rm6hwid":4,"20200825162036-4dx365o":1,"20200828105441-r76vmu5":21,"20200905090211-2vixtlf":2,"20200910201551-h4twhas":6,"20200915214115-42b8zma":10,"20200922102318-oz84yu3":2,"20201004184819-nj8ibyg":19,"20201117101902-2ewjjum":6,"20201121212605-9td1a62":11,"20201204181006-7bkppue":11,"20201210103036-1x3vm8t":5,"20201222093044-rx4zjoy":2,"20201222095049-hghafhe":3,"20201227173504-847cs1q":20,"20201227194925-7ipoiv6":22,"20210110175347-2xrwoiq":5,"20210117211155-56n4odu":4,"20210127202655-2334vvv":12,"20210331200042-94gs1hh":14,"20210505163537-oo97zov":3,"20210612224500-ywcms1m":7,"20210615211733-v6rzowm":15,"20210808180320-abz7w6k":2,"20210808180320-fqgskfj":1,"20210808180320-gyngv2x":3,"20210808180320-qgr0b3q":5,"20210808180321-hbvl5c2":6,"20210824201257-cy7icrc":8,"20211010211311-ffz0wbu":13,"20220415190432-r3xqn3r":1,"20220628204444-9n0y9h2":2,"20221016213308-uz5af79":3,"20221223215557-o6gfsoy":16,"20230104144904-39br4c6":23,"20230106101434-e6g4av3":1,"20230303235619-ex5l63e":17,"20230405155631-leo4vc6":6,"20230428153709-hioyy5l":7,"20230429114837-70asb4j":5,"20230506210010-houyyvy":9,"20230519105228-hm0y74i":8,"20230805222417-2lj3dvk":7,"20230805225107-qm1m2f5":2,"20230805230131-sn7obzb":1,"20230805230218-aea8icj":2,"20230808120347-3cob0nb":2,"20230808120347-mw3qrwy":4,"20230808120347-pzvmkik":1,"20230808120348-hynr7og":5,"20230808120348-lgcp9zm":3,"20230808120348-vaxi6eq":6,"20230808120348-yut741f":7,"20240113102857-c63dmo5":2,"20240119205452-o8xp4ve":4,"20240119205543-hknwwrl":5,"20240208113259-nykkvaq":7,"20240317200013-fim8wm8":9}
+{"20200812220555-lj3enxa":1,"20200813004551-gm0pbn1":18,"20200813004931-q4cu8na":1,"20200813013559-sgbzl5k":3,"20200813093015-u6bopdt":3,"20200813125307-pxsjela":2,"20200813131152-0wk5akh":4,"20200813163359-v04n73b":10,"20200822191536-rm6hwid":4,"20200825162036-4dx365o":1,"20200828105441-r76vmu5":21,"20200905090211-2vixtlf":2,"20200910201551-h4twhas":6,"20200915214115-42b8zma":10,"20200922102318-oz84yu3":2,"20201004184819-nj8ibyg":19,"20201117101902-2ewjjum":6,"20201121212605-9td1a62":11,"20201204181006-7bkppue":11,"20201210103036-1x3vm8t":5,"20201222093044-rx4zjoy":2,"20201222095049-hghafhe":3,"20201227173504-847cs1q":20,"20201227194925-7ipoiv6":22,"20210110175347-2xrwoiq":5,"20210117211155-56n4odu":4,"20210127202655-2334vvv":12,"20210331200042-94gs1hh":14,"20210505163537-oo97zov":3,"20210612224500-ywcms1m":7,"20210615211733-v6rzowm":15,"20210808180320-abz7w6k":2,"20210808180320-fqgskfj":1,"20210808180320-gyngv2x":3,"20210808180320-qgr0b3q":5,"20210808180321-hbvl5c2":6,"20210824201257-cy7icrc":8,"20211010211311-ffz0wbu":13,"20220415190432-r3xqn3r":1,"20220628204444-9n0y9h2":2,"20221016213308-uz5af79":3,"20221223215557-o6gfsoy":16,"20230104144904-39br4c6":23,"20230106101434-e6g4av3":1,"20230303235619-ex5l63e":17,"20230405155631-leo4vc6":6,"20230428153709-hioyy5l":7,"20230429114837-70asb4j":5,"20230506210010-houyyvy":9,"20230519105228-hm0y74i":8,"20230805222417-2lj3dvk":7,"20230805225107-qm1m2f5":2,"20230805230131-sn7obzb":1,"20230805230218-aea8icj":2,"20230808120347-3cob0nb":2,"20230808120347-mw3qrwy":4,"20230808120347-pzvmkik":1,"20230808120348-hynr7og":5,"20230808120348-lgcp9zm":3,"20230808120348-vaxi6eq":6,"20230808120348-yut741f":7,"20240113102857-c63dmo5":2,"20240119205452-o8xp4ve":4,"20240119205543-hknwwrl":5,"20240208113259-nykkvaq":7,"20240317200013-fim8wm8":9,"20240517031028-xqinazo":24}

+ 540 - 0
app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20240517031028-xqinazo.sy

@@ -0,0 +1,540 @@
+{
+	"ID": "20240517031028-xqinazo",
+	"Spec": "1",
+	"Type": "NodeDocument",
+	"Properties": {
+		"id": "20240517031028-xqinazo",
+		"title": "发布服务",
+		"type": "doc",
+		"updated": "20240517031044"
+	},
+	"Children": [
+		{
+			"ID": "20240517031044-7ox0m9z",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031044-7ox0m9z",
+				"updated": "20240517031044"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "概述"
+				}
+			]
+		},
+		{
+			"ID": "20240517031044-6mppc0k",
+			"Type": "NodeParagraph",
+			"Properties": {
+				"id": "20240517031044-6mppc0k",
+				"updated": "20240517031044"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "思源笔记支持以只读模式在局域网中发布当前工作空间的内容。"
+				}
+			]
+		},
+		{
+			"ID": "20240517031044-anhkwow",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031044-anhkwow",
+				"updated": "20240517031044"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "使用方式"
+				}
+			]
+		},
+		{
+			"ID": "20240517031044-0vj6or7",
+			"Type": "NodeList",
+			"ListData": {},
+			"Properties": {
+				"id": "20240517031044-0vj6or7",
+				"updated": "20240517031044"
+			},
+			"Children": [
+				{
+					"ID": "20240517031044-8ugboo7",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031044-8ugboo7",
+						"updated": "20240517031044"
+					},
+					"Children": [
+						{
+							"ID": "20240517031044-grogeue",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031044-grogeue",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "打开 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "设置"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ - "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "发布"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ 进入发布服务的设置面板"
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031044-mta08zv",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031044-mta08zv",
+						"updated": "20240517031044"
+					},
+					"Children": [
+						{
+							"ID": "20240517031044-7dg9xy9",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031044-7dg9xy9",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "设置 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "服务端口号"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								}
+							]
+						},
+						{
+							"ID": "20240517031044-vzj8o1a",
+							"Type": "NodeList",
+							"ListData": {},
+							"Properties": {
+								"id": "20240517031044-vzj8o1a",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"ID": "20240517031044-t2igim6",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031044-t2igim6",
+										"updated": "20240517031044"
+									},
+									"Children": [
+										{
+											"ID": "20240517031044-8ghv9tj",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031044-8ghv9tj",
+												"updated": "20240517031044"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "默认的端口号为 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "code",
+													"TextMarkTextContent": "6808"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​"
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240517031044-3oto1iz",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031044-3oto1iz",
+										"updated": "20240517031044"
+									},
+									"Children": [
+										{
+											"ID": "20240517031044-0m68jzx",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031044-0m68jzx",
+												"updated": "20240517031044"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "若端口号设置为 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "code",
+													"TextMarkTextContent": "0"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ 将使用随机端口"
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031044-ddii8sb",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031044-ddii8sb",
+						"updated": "20240517031044"
+					},
+					"Children": [
+						{
+							"ID": "20240517031044-vaiskyz",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031044-vaiskyz",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "若需要对发布服务进行访问控制"
+								}
+							]
+						},
+						{
+							"ID": "20240517031044-ze7l6sg",
+							"Type": "NodeList",
+							"ListData": {},
+							"Properties": {
+								"id": "20240517031044-ze7l6sg",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"ID": "20240517031044-fxpcxvc",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031044-fxpcxvc",
+										"updated": "20240517031044"
+									},
+									"Children": [
+										{
+											"ID": "20240517031044-bno9bca",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031044-bno9bca",
+												"updated": "20240517031044"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "添加 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "认证账户"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ 并开启 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "服务 Basic 认证"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ 开关"
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240517031044-29kw4ks",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031044-29kw4ks",
+										"updated": "20240517031044"
+									},
+									"Children": [
+										{
+											"ID": "20240517031044-g05n8gc",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031044-g05n8gc",
+												"updated": "20240517031044"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "开启后发布服务将使用 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "a",
+													"TextMarkAHref": "https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication#basic_验证方案",
+													"TextMarkTextContent": "Basic 验证方案"
+												},
+												{
+													"Type": "NodeText",
+													"Data": " 对访问者进行认证"
+												}
+											]
+										},
+										{
+											"ID": "20240517031044-e54f9vf",
+											"Type": "NodeList",
+											"ListData": {},
+											"Properties": {
+												"id": "20240517031044-e54f9vf",
+												"updated": "20240517031044"
+											},
+											"Children": [
+												{
+													"ID": "20240517031044-kr1t232",
+													"Type": "NodeListItem",
+													"ListData": {
+														"BulletChar": 42,
+														"Marker": "Kg=="
+													},
+													"Properties": {
+														"id": "20240517031044-kr1t232",
+														"updated": "20240517031044"
+													},
+													"Children": [
+														{
+															"ID": "20240517031044-rf8nyba",
+															"Type": "NodeParagraph",
+															"Properties": {
+																"id": "20240517031044-rf8nyba",
+																"updated": "20240517031044"
+															},
+															"Children": [
+																{
+																	"Type": "NodeText",
+																	"Data": "访问者在浏览发布内容前必须正确输入 "
+																},
+																{
+																	"Type": "NodeTextMark",
+																	"TextMarkType": "kbd",
+																	"TextMarkTextContent": "认证账户"
+																},
+																{
+																	"Type": "NodeText",
+																	"Data": "​ 中设置的用户名与密码"
+																}
+															]
+														}
+													]
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031044-s5d5aak",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031044-s5d5aak",
+						"updated": "20240517031044"
+					},
+					"Children": [
+						{
+							"ID": "20240517031044-ec9vaac",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031044-ec9vaac",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "开启 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "发布服务"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ 开关"
+								}
+							]
+						}
+					]
+				}
+			]
+		},
+		{
+			"ID": "20240517031044-6le4t1r",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031044-6le4t1r",
+				"updated": "20240517031044"
+			},
+			"Children": [
+				{
+					"Type": "NodeTextMark",
+					"TextMarkType": "tag",
+					"TextMarkTextContent": "注意"
+				},
+				{
+					"Type": "NodeText",
+					"Data": "​"
+				}
+			]
+		},
+		{
+			"ID": "20240517031044-ahzjzvg",
+			"Type": "NodeList",
+			"ListData": {},
+			"Properties": {
+				"id": "20240517031044-ahzjzvg",
+				"updated": "20240517031044"
+			},
+			"Children": [
+				{
+					"ID": "20240517031044-8f3g2fd",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031044-8f3g2fd",
+						"updated": "20240517031044"
+					},
+					"Children": [
+						{
+							"ID": "20240517031044-uiys91u",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031044-uiys91u",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "开启发布服务后访问者可以浏览整个工作空间的内容"
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031044-979coq3",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031044-979coq3",
+						"updated": "20240517031044"
+					},
+					"Children": [
+						{
+							"ID": "20240517031044-7be46qj",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031044-7be46qj",
+								"updated": "20240517031044"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "关闭 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "服务 Basic 认证"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ 后所有的访问者无需认证即可浏览整个工作空间的内容"
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	]
+}

+ 0 - 2
app/guide/20211226090932-5lcq56f/.gitignore

@@ -1,2 +0,0 @@
-.idea/
-.siyuan/history/

+ 1 - 1
app/guide/20211226090932-5lcq56f/.siyuan/sort.json

@@ -1 +1 @@
-{"20211226114339-dk0gtpr":8,"20211226114929-08ap1r0":9,"20211226115043-afhev0g":4,"20211226115227-r1rty9v":3,"20211226115423-d5z1joq":1,"20211226115825-mhcslw2":1,"20211226120055-9mityht":1,"20211226120147-ib6yy3i":2,"20211226120247-63nd8y5":3,"20211226120349-rbkmozu":4,"20211226120422-bkzsd2e":5,"20211226120508-yzh70eh":6,"20211226120802-77aj0is":7,"20211226120854-dr1jfx2":2,"20211226120933-vnjgwwh":3,"20211226121109-f060fkg":4,"20211226121203-rjjngpz":5,"20211226121319-emrk2yy":1,"20211226121322-9argcys":3,"20211226121329-c5v3dto":22,"20211226121332-irgblss":4,"20211226121438-xaafdo8":2,"20211226121503-k3jma6m":1,"20211226121808-fnxmngk":2,"20211226122358-hctqcn5":21,"20211226122459-08mi5cq":20,"20211226122523-rl8356a":19,"20211226122549-jktxego":18,"20211226122707-8cr09co":15,"20211226122728-cnqf7rz":14,"20211226122814-r1rdpcx":13,"20211226122943-st7fpcj":12,"20211226123004-dplpw0o":11,"20211226123038-4umgpxy":10,"20211226123101-qjw03ab":8,"20211226123130-jpeg5b2":6,"20211226123154-fd5e001":5,"20211226123216-tlxw66f":4,"20211226123241-51pujtr":3,"20211226123302-akitvb1":2,"20220105101348-corstqc":6,"20220415232129-shpzg6r":1,"20220628204420-ui79vkt":2,"20220708102441-u6wopo9":3,"20221016213639-1nag9jj":3,"20221223221501-mops33i":16,"20230104151953-48hwkwf":23,"20230106104645-o838uew":1,"20230304000829-9jwu3po":17,"20230405172131-yb16aax":6,"20230429115206-ob8nl8t":5,"20230506211210-1roopyo":9,"20230805232636-zh0adz2":6,"20230805232719-04mqbcx":2,"20230805232903-erdoerp":1,"20230805232920-5fdco36":2,"20240113110500-dz2ae4n":2,"20240119210914-a2tm8c4":4,"20240119212000-qkldbjm":5,"20240208171522-y7dxcno":7,"20240317202230-l8duv3r":7,"20240508010647-8nuyk31":5}
+{"20211226114339-dk0gtpr":8,"20211226114929-08ap1r0":9,"20211226115043-afhev0g":4,"20211226115227-r1rty9v":3,"20211226115423-d5z1joq":1,"20211226115825-mhcslw2":1,"20211226120055-9mityht":1,"20211226120147-ib6yy3i":2,"20211226120247-63nd8y5":3,"20211226120349-rbkmozu":4,"20211226120422-bkzsd2e":5,"20211226120508-yzh70eh":6,"20211226120802-77aj0is":7,"20211226120854-dr1jfx2":2,"20211226120933-vnjgwwh":3,"20211226121109-f060fkg":4,"20211226121203-rjjngpz":5,"20211226121319-emrk2yy":1,"20211226121322-9argcys":3,"20211226121329-c5v3dto":22,"20211226121332-irgblss":4,"20211226121438-xaafdo8":2,"20211226121503-k3jma6m":1,"20211226121808-fnxmngk":2,"20211226122358-hctqcn5":21,"20211226122459-08mi5cq":20,"20211226122523-rl8356a":19,"20211226122549-jktxego":18,"20211226122707-8cr09co":15,"20211226122728-cnqf7rz":14,"20211226122814-r1rdpcx":13,"20211226122943-st7fpcj":12,"20211226123004-dplpw0o":11,"20211226123038-4umgpxy":10,"20211226123101-qjw03ab":8,"20211226123130-jpeg5b2":6,"20211226123154-fd5e001":5,"20211226123216-tlxw66f":4,"20211226123241-51pujtr":3,"20211226123302-akitvb1":2,"20220105101348-corstqc":6,"20220415232129-shpzg6r":1,"20220628204420-ui79vkt":2,"20220708102441-u6wopo9":3,"20221016213639-1nag9jj":3,"20221223221501-mops33i":16,"20230104151953-48hwkwf":23,"20230106104645-o838uew":1,"20230304000829-9jwu3po":17,"20230405172131-yb16aax":6,"20230429115206-ob8nl8t":5,"20230506211210-1roopyo":9,"20230805232636-zh0adz2":6,"20230805232719-04mqbcx":2,"20230805232903-erdoerp":1,"20230805232920-5fdco36":2,"20240113110500-dz2ae4n":2,"20240119210914-a2tm8c4":4,"20240119212000-qkldbjm":5,"20240208171522-y7dxcno":7,"20240317202230-l8duv3r":7,"20240508010647-8nuyk31":5,"20240517031052-mgoxs16":24}

+ 540 - 0
app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20240517031052-mgoxs16.sy

@@ -0,0 +1,540 @@
+{
+	"ID": "20240517031052-mgoxs16",
+	"Spec": "1",
+	"Type": "NodeDocument",
+	"Properties": {
+		"id": "20240517031052-mgoxs16",
+		"title": "發布服務",
+		"type": "doc",
+		"updated": "20240517031115"
+	},
+	"Children": [
+		{
+			"ID": "20240517031115-pjkrctx",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031115-pjkrctx",
+				"updated": "20240517031115"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "概述"
+				}
+			]
+		},
+		{
+			"ID": "20240517031116-a0xf075",
+			"Type": "NodeParagraph",
+			"Properties": {
+				"id": "20240517031116-a0xf075",
+				"updated": "20240517031116"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "思源筆記支援以只讀模式在區域網路中發佈當前工作空間的內容。"
+				}
+			]
+		},
+		{
+			"ID": "20240517031116-ml8jd6n",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240517031116-ml8jd6n",
+				"updated": "20240517031116"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "使用方式"
+				}
+			]
+		},
+		{
+			"ID": "20240517031116-5u42tlr",
+			"Type": "NodeList",
+			"ListData": {},
+			"Properties": {
+				"id": "20240517031116-5u42tlr",
+				"updated": "20240517031116"
+			},
+			"Children": [
+				{
+					"ID": "20240517031116-06mevu7",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031116-06mevu7",
+						"updated": "20240517031116"
+					},
+					"Children": [
+						{
+							"ID": "20240517031116-wqo2aje",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031116-wqo2aje",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "開啟 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "設定"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ - "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "發佈"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ 進入發佈服務的設定面板"
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031116-dsvhebl",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031116-dsvhebl",
+						"updated": "20240517031116"
+					},
+					"Children": [
+						{
+							"ID": "20240517031116-13481er",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031116-13481er",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "設定 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "服務端口號"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								}
+							]
+						},
+						{
+							"ID": "20240517031116-sjbg5cn",
+							"Type": "NodeList",
+							"ListData": {},
+							"Properties": {
+								"id": "20240517031116-sjbg5cn",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"ID": "20240517031116-z1q43ui",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031116-z1q43ui",
+										"updated": "20240517031116"
+									},
+									"Children": [
+										{
+											"ID": "20240517031116-ssdjmbf",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031116-ssdjmbf",
+												"updated": "20240517031116"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "默認的端口號為 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "code",
+													"TextMarkTextContent": "6808"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​"
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240517031116-jkcxgaj",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031116-jkcxgaj",
+										"updated": "20240517031116"
+									},
+									"Children": [
+										{
+											"ID": "20240517031116-ka1wome",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031116-ka1wome",
+												"updated": "20240517031116"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "若端口號設定為 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "code",
+													"TextMarkTextContent": "0"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ 將使用隨機端口"
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031116-ipkirs4",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031116-ipkirs4",
+						"updated": "20240517031116"
+					},
+					"Children": [
+						{
+							"ID": "20240517031116-g8qr29s",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031116-g8qr29s",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "若需要對發佈服務進行訪問控制"
+								}
+							]
+						},
+						{
+							"ID": "20240517031116-maj6mmd",
+							"Type": "NodeList",
+							"ListData": {},
+							"Properties": {
+								"id": "20240517031116-maj6mmd",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"ID": "20240517031116-du7rttx",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031116-du7rttx",
+										"updated": "20240517031116"
+									},
+									"Children": [
+										{
+											"ID": "20240517031116-ct3chrz",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031116-ct3chrz",
+												"updated": "20240517031116"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "添加 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "認證帳戶"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ 並開啟 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "服務 Basic 認證"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ 開關"
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240517031116-dlaep2b",
+									"Type": "NodeListItem",
+									"ListData": {
+										"BulletChar": 42,
+										"Marker": "Kg=="
+									},
+									"Properties": {
+										"id": "20240517031116-dlaep2b",
+										"updated": "20240517031116"
+									},
+									"Children": [
+										{
+											"ID": "20240517031116-3u2v8g7",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240517031116-3u2v8g7",
+												"updated": "20240517031116"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "開啟後發佈服務將使用 "
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "a",
+													"TextMarkAHref": "https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication#basic_验证方案",
+													"TextMarkTextContent": "Basic 驗證方案"
+												},
+												{
+													"Type": "NodeText",
+													"Data": " 對訪問者進行認證"
+												}
+											]
+										},
+										{
+											"ID": "20240517031116-qmzfje7",
+											"Type": "NodeList",
+											"ListData": {},
+											"Properties": {
+												"id": "20240517031116-qmzfje7",
+												"updated": "20240517031116"
+											},
+											"Children": [
+												{
+													"ID": "20240517031116-xthto7w",
+													"Type": "NodeListItem",
+													"ListData": {
+														"BulletChar": 42,
+														"Marker": "Kg=="
+													},
+													"Properties": {
+														"id": "20240517031116-xthto7w",
+														"updated": "20240517031116"
+													},
+													"Children": [
+														{
+															"ID": "20240517031116-kbz5qye",
+															"Type": "NodeParagraph",
+															"Properties": {
+																"id": "20240517031116-kbz5qye",
+																"updated": "20240517031116"
+															},
+															"Children": [
+																{
+																	"Type": "NodeText",
+																	"Data": "訪問者在瀏覽發佈內容前必須正確輸入 "
+																},
+																{
+																	"Type": "NodeTextMark",
+																	"TextMarkType": "kbd",
+																	"TextMarkTextContent": "認證帳戶"
+																},
+																{
+																	"Type": "NodeText",
+																	"Data": "​ 中設定的用戶名與密碼"
+																}
+															]
+														}
+													]
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031116-bx01v3n",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031116-bx01v3n",
+						"updated": "20240517031116"
+					},
+					"Children": [
+						{
+							"ID": "20240517031116-44j60lx",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031116-44j60lx",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "開啟 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "發佈服務"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ 開關"
+								}
+							]
+						}
+					]
+				}
+			]
+		},
+		{
+			"ID": "20240517031116-2v0lh0m",
+			"Type": "NodeHeading",
+			"HeadingLevel": 3,
+			"Properties": {
+				"id": "20240517031116-2v0lh0m",
+				"updated": "20240517031116"
+			},
+			"Children": [
+				{
+					"Type": "NodeTextMark",
+					"TextMarkType": "tag",
+					"TextMarkTextContent": "注意"
+				},
+				{
+					"Type": "NodeText",
+					"Data": "​"
+				}
+			]
+		},
+		{
+			"ID": "20240517031116-lx0pe26",
+			"Type": "NodeList",
+			"ListData": {},
+			"Properties": {
+				"id": "20240517031116-lx0pe26",
+				"updated": "20240517031116"
+			},
+			"Children": [
+				{
+					"ID": "20240517031116-kvje8ru",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031116-kvje8ru",
+						"updated": "20240517031116"
+					},
+					"Children": [
+						{
+							"ID": "20240517031116-gydcx3b",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031116-gydcx3b",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "開啟發佈服務後訪問者可以瀏覽整個工作空間的內容"
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240517031116-ufsul2c",
+					"Type": "NodeListItem",
+					"ListData": {
+						"BulletChar": 42,
+						"Marker": "Kg=="
+					},
+					"Properties": {
+						"id": "20240517031116-ufsul2c",
+						"updated": "20240517031116"
+					},
+					"Children": [
+						{
+							"ID": "20240517031116-xyb4012",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240517031116-xyb4012",
+								"updated": "20240517031116"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "關閉 "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "服務 Basic 認證"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ 後所有的訪問者無需認證即可瀏覽整個工作空間的內容"
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	]
+}

+ 1 - 1
app/guide/20240530133126-axarxgx/.siyuan/sort.json

@@ -1 +1 @@
-{"20240530101000-0zd9si2":13,"20240530101000-1m5un7l":10,"20240530101000-1vvnhju":6,"20240530101000-35bbvcx":2,"20240530101000-3eaevtp":7,"20240530101000-3ourzoa":2,"20240530101000-3qhz7br":1,"20240530101000-3xv6jjr":9,"20240530101000-40hohog":19,"20240530101000-4cqkoel":23,"20240530101000-4p096e8":1,"20240530101000-4qitucx":1,"20240530101000-58z6rjh":2,"20240530101000-5gi76ax":8,"20240530101000-5k5d5i3":2,"20240530101000-6x9ivi7":11,"20240530101000-75auqcn":6,"20240530101000-78d5w10":4,"20240530101000-7yv4y5z":4,"20240530101000-875snki":7,"20240530101000-8m2dowc":9,"20240530101000-96n5y9v":1,"20240530101000-a91lmk2":3,"20240530101000-bb0qktp":1,"20240530101000-bgv304g":8,"20240530101000-boiita2":4,"20240530101000-bpj42r0":2,"20240530101000-cb37szr":7,"20240530101000-d31i9v5":2,"20240530101000-dro2zi9":5,"20240530101000-dytmky4":22,"20240530101000-e6z5okf":3,"20240530101000-ei5t6tt":17,"20240530101000-f16hpct":20,"20240530101000-flot1gj":7,"20240530101000-g3ugxml":5,"20240530101000-gc0tuyd":3,"20240530101000-gcdp3gl":3,"20240530101000-gvtksyj":1,"20240530101000-h0cp5vx":5,"20240530101000-hkgs92c":5,"20240530101000-hnu6o79":2,"20240530101000-hq9sn3k":1,"20240530101000-jp793ic":4,"20240530101000-mpln2lp":6,"20240530101000-mrlr6e9":4,"20240530101000-na9sys7":1,"20240530101000-odye3ea":3,"20240530101000-qf0xtkd":2,"20240530101000-scr2p21":5,"20240530101000-to5uvpi":12,"20240530101000-txrc7z1":15,"20240530101000-uxno1yp":3,"20240530101000-vejnh1k":1,"20240530101000-vql5s27":5,"20240530101000-wo49zvq":2,"20240530101000-xc13cm6":6,"20240530101000-xq26o73":21,"20240530101000-xr22qn8":4,"20240530101000-xsbxokr":18,"20240530101000-ynpmvl7":16,"20240530101000-zj4k048":3,"20240530101000-znj103k":10,"20240530101000-zprnpfi":14}
+{"20240530101000-0zd9si2":13,"20240530101000-1m5un7l":10,"20240530101000-1vvnhju":6,"20240530101000-35bbvcx":2,"20240530101000-3eaevtp":7,"20240530101000-3ourzoa":2,"20240530101000-3qhz7br":1,"20240530101000-3xv6jjr":9,"20240530101000-40hohog":19,"20240530101000-4cqkoel":23,"20240530101000-4p096e8":1,"20240530101000-4qitucx":1,"20240530101000-58z6rjh":2,"20240530101000-5gi76ax":8,"20240530101000-5k5d5i3":2,"20240530101000-6x9ivi7":11,"20240530101000-75auqcn":6,"20240530101000-78d5w10":4,"20240530101000-7yv4y5z":4,"20240530101000-875snki":7,"20240530101000-8m2dowc":9,"20240530101000-96n5y9v":1,"20240530101000-a91lmk2":3,"20240530101000-bb0qktp":1,"20240530101000-bgv304g":8,"20240530101000-boiita2":4,"20240530101000-bpj42r0":2,"20240530101000-cb37szr":7,"20240530101000-d31i9v5":2,"20240530101000-dro2zi9":5,"20240530101000-dytmky4":22,"20240530101000-e6z5okf":3,"20240530101000-ei5t6tt":17,"20240530101000-f16hpct":20,"20240530101000-flot1gj":7,"20240530101000-g3ugxml":5,"20240530101000-gc0tuyd":3,"20240530101000-gcdp3gl":3,"20240530101000-gvtksyj":1,"20240530101000-h0cp5vx":5,"20240530101000-hkgs92c":5,"20240530101000-hnu6o79":2,"20240530101000-hq9sn3k":1,"20240530101000-jp793ic":4,"20240530101000-mpln2lp":6,"20240530101000-mrlr6e9":4,"20240530101000-na9sys7":1,"20240530101000-odye3ea":3,"20240530101000-qf0xtkd":2,"20240530101000-scr2p21":5,"20240530101000-to5uvpi":12,"20240530101000-txrc7z1":15,"20240530101000-uxno1yp":3,"20240530101000-vejnh1k":1,"20240530101000-vql5s27":5,"20240530101000-wo49zvq":2,"20240530101000-xc13cm6":6,"20240530101000-xq26o73":21,"20240530101000-xr22qn8":4,"20240530101000-xsbxokr":18,"20240530101000-ynpmvl7":16,"20240530101000-zj4k048":3,"20240530101000-znj103k":10,"20240530101000-zprnpfi":14,"20240610233601-ylh1mvk":24}

+ 676 - 0
app/guide/20240530133126-axarxgx/20240530101000-4qitucx/20240530101000-g3ugxml/20240610233601-ylh1mvk.sy

@@ -0,0 +1,676 @@
+{
+	"ID": "20240610233601-ylh1mvk",
+	"Spec": "1",
+	"Type": "NodeDocument",
+	"Properties": {
+		"id": "20240610233601-ylh1mvk",
+		"title": "サービスの公開",
+		"type": "doc",
+		"updated": "20240610235250"
+	},
+	"Children": [
+		{
+			"ID": "20240610233629-jjii9tu",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240610233629-jjii9tu",
+				"updated": "20240610235250"
+			},
+			"Children": [
+				{
+					"Type": "NodeHeadingC8hMarker",
+					"Data": "## ",
+					"Properties": {
+						"id": ""
+					}
+				},
+				{
+					"Type": "NodeText",
+					"Data": "概要",
+					"Properties": {
+						"id": ""
+					}
+				}
+			]
+		},
+		{
+			"ID": "20240610233629-49em84g",
+			"Type": "NodeParagraph",
+			"Properties": {
+				"id": "20240610233629-49em84g",
+				"updated": "20240610233739"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "SiYuan Note は、現在のワークスペースの内容を読み取り専用モードでローカルネットワークで公開することができます。",
+					"Properties": {
+						"id": ""
+					}
+				}
+			]
+		},
+		{
+			"ID": "20240610233629-bxuf2mx",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240610233629-bxuf2mx",
+				"updated": "20240610235250"
+			},
+			"Children": [
+				{
+					"Type": "NodeText",
+					"Data": "使い方"
+				}
+			]
+		},
+		{
+			"ID": "20240610233629-7d7e66r",
+			"Type": "NodeList",
+			"ListData": {
+				"BulletChar": 42,
+				"Padding": 2,
+				"Marker": "Kg==",
+				"Num": -1
+			},
+			"Properties": {
+				"id": "20240610233629-7d7e66r",
+				"updated": "20240610235250"
+			},
+			"Children": [
+				{
+					"ID": "20240610233629-jn7c5xq",
+					"Type": "NodeListItem",
+					"Data": "*",
+					"ListData": {
+						"BulletChar": 42,
+						"Padding": 2,
+						"Marker": "Kg==",
+						"Num": -1
+					},
+					"Properties": {
+						"id": "20240610233629-jn7c5xq",
+						"updated": "20240610234354"
+					},
+					"Children": [
+						{
+							"ID": "20240610233629-ibn4bp6",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240610233629-ibn4bp6",
+								"updated": "20240610234354"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "設定"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ - "
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "公開する"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ に移動して、公開サービスの設定パネルに入ります。"
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240610233629-j8ml6bw",
+					"Type": "NodeListItem",
+					"Data": "*",
+					"ListData": {
+						"Tight": true,
+						"BulletChar": 42,
+						"Padding": 2,
+						"Marker": "Kg==",
+						"Num": -1
+					},
+					"Properties": {
+						"id": "20240610233629-j8ml6bw",
+						"updated": "20240610234344"
+					},
+					"Children": [
+						{
+							"ID": "20240610233629-fubq5bz",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240610233629-fubq5bz",
+								"updated": "20240610234344"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "サービスポート"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ を設定します。"
+								}
+							]
+						},
+						{
+							"ID": "20240610233629-d9q97le",
+							"Type": "NodeList",
+							"ListData": {
+								"Tight": true,
+								"BulletChar": 42,
+								"Padding": 2,
+								"Marker": "Kg==",
+								"Num": -1
+							},
+							"Properties": {
+								"id": "20240610233629-d9q97le",
+								"updated": "20240610233629"
+							},
+							"Children": [
+								{
+									"ID": "20240610233629-y8ynj1h",
+									"Type": "NodeListItem",
+									"Data": "*",
+									"ListData": {
+										"Tight": true,
+										"BulletChar": 42,
+										"Padding": 2,
+										"Marker": "Kg==",
+										"Num": -1
+									},
+									"Properties": {
+										"id": "20240610233629-y8ynj1h",
+										"updated": "20240610233629"
+									},
+									"Children": [
+										{
+											"ID": "20240610233629-fwe6fgu",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240610233629-fwe6fgu",
+												"updated": "20240610233629"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "デフォルトのポート番号は ",
+													"Properties": {
+														"id": ""
+													}
+												},
+												{
+													"Type": "NodeTextMark",
+													"Properties": {
+														"id": ""
+													},
+													"TextMarkType": "code",
+													"TextMarkTextContent": "6808"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ です。",
+													"Properties": {
+														"id": ""
+													}
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240610233629-amkxqp8",
+									"Type": "NodeListItem",
+									"Data": "*",
+									"ListData": {
+										"Tight": true,
+										"BulletChar": 42,
+										"Padding": 2,
+										"Marker": "Kg==",
+										"Num": -1
+									},
+									"Properties": {
+										"id": "20240610233629-amkxqp8",
+										"updated": "20240610233629"
+									},
+									"Children": [
+										{
+											"ID": "20240610233629-726ai0u",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240610233629-726ai0u",
+												"updated": "20240610233629"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "ポート番号を ",
+													"Properties": {
+														"id": ""
+													}
+												},
+												{
+													"Type": "NodeTextMark",
+													"Properties": {
+														"id": ""
+													},
+													"TextMarkType": "code",
+													"TextMarkTextContent": "0"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ に設定すると、ランダムなポートが使用されます。",
+													"Properties": {
+														"id": ""
+													}
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240610233629-c2bncke",
+					"Type": "NodeListItem",
+					"Data": "*",
+					"ListData": {
+						"Tight": true,
+						"BulletChar": 42,
+						"Padding": 2,
+						"Marker": "Kg==",
+						"Num": -1
+					},
+					"Properties": {
+						"id": "20240610233629-c2bncke",
+						"updated": "20240610235250"
+					},
+					"Children": [
+						{
+							"ID": "20240610233629-c02853s",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240610233629-c02853s",
+								"updated": "20240610233629"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "公開サービスにアクセス制限を設定する場合",
+									"Properties": {
+										"id": ""
+									}
+								}
+							]
+						},
+						{
+							"ID": "20240610233629-a5alsxq",
+							"Type": "NodeList",
+							"ListData": {
+								"BulletChar": 42,
+								"Padding": 2,
+								"Marker": "Kg==",
+								"Num": -1
+							},
+							"Properties": {
+								"id": "20240610233629-a5alsxq",
+								"updated": "20240610235250"
+							},
+							"Children": [
+								{
+									"ID": "20240610233629-4p8jyop",
+									"Type": "NodeListItem",
+									"Data": "*",
+									"ListData": {
+										"BulletChar": 42,
+										"Padding": 2,
+										"Marker": "Kg==",
+										"Num": -1
+									},
+									"Properties": {
+										"id": "20240610233629-4p8jyop",
+										"updated": "20240610235142"
+									},
+									"Children": [
+										{
+											"ID": "20240610233629-6pki0nz",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240610233629-6pki0nz",
+												"updated": "20240610235142"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "​"
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "認証アカウント"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ を追加し、"
+												},
+												{
+													"Type": "NodeTextMark",
+													"TextMarkType": "kbd",
+													"TextMarkTextContent": "サービスの基本認証"
+												},
+												{
+													"Type": "NodeText",
+													"Data": "​ スイッチをオンにします。"
+												}
+											]
+										}
+									]
+								},
+								{
+									"ID": "20240610233629-3s2kd31",
+									"Type": "NodeListItem",
+									"Data": "*",
+									"ListData": {
+										"Tight": true,
+										"BulletChar": 42,
+										"Padding": 2,
+										"Marker": "Kg==",
+										"Num": -1
+									},
+									"Properties": {
+										"id": "20240610233629-3s2kd31",
+										"updated": "20240610235250"
+									},
+									"Children": [
+										{
+											"ID": "20240610233629-fozs5am",
+											"Type": "NodeParagraph",
+											"Properties": {
+												"id": "20240610233629-fozs5am",
+												"updated": "20240610233721"
+											},
+											"Children": [
+												{
+													"Type": "NodeText",
+													"Data": "この後、公開サービスはアクセスするユーザーを ",
+													"Properties": {
+														"id": ""
+													}
+												},
+												{
+													"Type": "NodeTextMark",
+													"Properties": {
+														"id": ""
+													},
+													"TextMarkType": "a",
+													"TextMarkAHref": "https://developer.mozilla.org/ja/docs/Web/HTTP/Authentication#basic_認証方式",
+													"TextMarkTextContent": "Basic 認証方式"
+												},
+												{
+													"Type": "NodeText",
+													"Data": " を使用して認証します。",
+													"Properties": {
+														"id": ""
+													}
+												}
+											]
+										},
+										{
+											"ID": "20240610233629-b18hlwj",
+											"Type": "NodeList",
+											"ListData": {
+												"Tight": true,
+												"BulletChar": 42,
+												"Padding": 2,
+												"Marker": "Kg==",
+												"Num": -1
+											},
+											"Properties": {
+												"id": "20240610233629-b18hlwj",
+												"updated": "20240610235250"
+											},
+											"Children": [
+												{
+													"ID": "20240610233629-4mziigo",
+													"Type": "NodeListItem",
+													"Data": "*",
+													"ListData": {
+														"Tight": true,
+														"BulletChar": 42,
+														"Padding": 2,
+														"Marker": "Kg==",
+														"Num": -1
+													},
+													"Properties": {
+														"id": "20240610233629-4mziigo",
+														"updated": "20240610235250"
+													},
+													"Children": [
+														{
+															"ID": "20240610233629-526pequ",
+															"Type": "NodeParagraph",
+															"Properties": {
+																"id": "20240610233629-526pequ",
+																"updated": "20240610235250"
+															},
+															"Children": [
+																{
+																	"Type": "NodeText",
+																	"Data": "ユーザーは、公開コンテンツを閲覧する前に、"
+																},
+																{
+																	"Type": "NodeTextMark",
+																	"TextMarkType": "kbd",
+																	"TextMarkTextContent": "認証アカウント"
+																},
+																{
+																	"Type": "NodeText",
+																	"Data": "​ で設定したユーザー名とパスワードを正しく入力する必要があります。"
+																}
+															]
+														}
+													]
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240610233629-a4d5gct",
+					"Type": "NodeListItem",
+					"Data": "*",
+					"ListData": {
+						"Tight": true,
+						"BulletChar": 42,
+						"Padding": 2,
+						"Marker": "Kg==",
+						"Num": -1
+					},
+					"Properties": {
+						"id": "20240610233629-a4d5gct",
+						"updated": "20240610234926"
+					},
+					"Children": [
+						{
+							"ID": "20240610233629-cqspvm0",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240610233629-cqspvm0",
+								"updated": "20240610234926"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "サービスを公開する"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ スイッチをオンにします"
+								}
+							]
+						}
+					]
+				}
+			]
+		},
+		{
+			"ID": "20240610233629-ns08k8s",
+			"Type": "NodeHeading",
+			"HeadingLevel": 2,
+			"Properties": {
+				"id": "20240610233629-ns08k8s",
+				"updated": "20240610235147"
+			},
+			"Children": [
+				{
+					"Type": "NodeHeadingC8hMarker",
+					"Data": "## ",
+					"Properties": {
+						"id": ""
+					}
+				},
+				{
+					"Type": "NodeText",
+					"Data": "​",
+					"Properties": {
+						"id": ""
+					}
+				},
+				{
+					"Type": "NodeTextMark",
+					"Properties": {
+						"id": ""
+					},
+					"TextMarkType": "tag",
+					"TextMarkTextContent": "注意事項"
+				},
+				{
+					"Type": "NodeText",
+					"Data": "​",
+					"Properties": {
+						"id": ""
+					}
+				}
+			]
+		},
+		{
+			"ID": "20240610233629-uqp9rz9",
+			"Type": "NodeList",
+			"ListData": {
+				"Tight": true,
+				"BulletChar": 42,
+				"Padding": 2,
+				"Marker": "Kg==",
+				"Num": -1
+			},
+			"Properties": {
+				"id": "20240610233629-uqp9rz9",
+				"updated": "20240610235147"
+			},
+			"Children": [
+				{
+					"ID": "20240610233629-jiv7u6m",
+					"Type": "NodeListItem",
+					"Data": "*",
+					"ListData": {
+						"Tight": true,
+						"BulletChar": 42,
+						"Padding": 2,
+						"Marker": "Kg==",
+						"Num": -1
+					},
+					"Properties": {
+						"id": "20240610233629-jiv7u6m",
+						"updated": "20240610233629"
+					},
+					"Children": [
+						{
+							"ID": "20240610233629-j8ancdr",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240610233629-j8ancdr",
+								"updated": "20240610233629"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "公開サービスを開始すると、訪問者はワークスペースの全コンテンツを閲覧できます。",
+									"Properties": {
+										"id": ""
+									}
+								}
+							]
+						}
+					]
+				},
+				{
+					"ID": "20240610233629-10tmo1g",
+					"Type": "NodeListItem",
+					"Data": "*",
+					"ListData": {
+						"Tight": true,
+						"BulletChar": 42,
+						"Padding": 2,
+						"Marker": "Kg==",
+						"Num": -1
+					},
+					"Properties": {
+						"id": "20240610233629-10tmo1g",
+						"updated": "20240610235147"
+					},
+					"Children": [
+						{
+							"ID": "20240610233629-20lo5k4",
+							"Type": "NodeParagraph",
+							"Properties": {
+								"id": "20240610233629-20lo5k4",
+								"updated": "20240610235147"
+							},
+							"Children": [
+								{
+									"Type": "NodeText",
+									"Data": "​"
+								},
+								{
+									"Type": "NodeTextMark",
+									"TextMarkType": "kbd",
+									"TextMarkTextContent": "サービスの基本認証"
+								},
+								{
+									"Type": "NodeText",
+									"Data": "​ を無効にすると、すべての訪問者はワークスペースの全コンテンツを認証なしで閲覧できます。"
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	]
+}

+ 6 - 4
app/src/config/editor.ts

@@ -310,10 +310,12 @@ export const editor = {
         if (fontFamilyElement.tagName === "SELECT") {
             let fontFamilyHTML = `<option value="">${window.siyuan.languages.default}</option>`;
             fetchPost("/api/system/getSysFonts", {}, (response) => {
-                response.data.forEach((item: string) => {
-                    fontFamilyHTML += `<option value="${item}"${window.siyuan.config.editor.fontFamily === item ? " selected" : ""}>${item}</option>`;
-                });
-                fontFamilyElement.innerHTML = fontFamilyHTML;
+                if (response.code === 0) {
+                    response.data.forEach((item: string) => {
+                        fontFamilyHTML += `<option value="${item}"${window.siyuan.config.editor.fontFamily === item ? " selected" : ""}>${item}</option>`;
+                    });
+                    fontFamilyElement.innerHTML = fontFamilyHTML;
+                }
             });
         }
         editor.element.querySelector("#clearHistory").addEventListener("click", () => {

+ 8 - 0
app/src/config/index.ts

@@ -13,6 +13,7 @@ import {query} from "./query";
 import {Dialog} from "../dialog";
 import {ai} from "./ai";
 import {flashcard} from "./flashcard";
+import {publish} from "./publish";
 import {App} from "../index";
 import {isHuawei} from "../protyle/util/compatibility";
 import {Constants} from "../constants";
@@ -79,6 +80,11 @@ export const genItemPanel = (type: string, containerElement: Element, app: App)
             query.element = containerElement;
             query.bindEvent();
             break;
+        case "publish":
+            containerElement.innerHTML = publish.genHTML();
+            publish.element = containerElement;
+            publish.bindEvent();
+            break;
         default:
             break;
     }
@@ -115,6 +121,7 @@ export const openSetting = (app: App) => {
     <li data-name="keymap" class="b3-list-item"><svg class="b3-list-item__graphic"><use xlink:href="#iconKeymap"></use></svg><span class="b3-list-item__text">${window.siyuan.languages.keymap}</span></li>
     <li data-name="account" class="b3-list-item"><svg class="b3-list-item__graphic"><use xlink:href="#iconAccount"></use></svg><span class="b3-list-item__text">${window.siyuan.languages.account}</span></li>
     <li data-name="repos" class="b3-list-item"><svg class="b3-list-item__graphic"><use xlink:href="#iconCloud"></use></svg><span class="b3-list-item__text">${window.siyuan.languages.cloud}</span></li>
+    <li data-name="publish" class="b3-list-item"><svg class="b3-list-item__graphic"><use xlink:href="#iconLanguage"></use></svg><span class="b3-list-item__text">${window.siyuan.languages.publish}</span></li>
     <li data-name="about" class="b3-list-item"><svg class="b3-list-item__graphic"><use xlink:href="#iconInfo"></use></svg><span class="b3-list-item__text">${window.siyuan.languages.about}</span></li>
   </ul>
   <div class="config__tab-wrap">
@@ -131,6 +138,7 @@ export const openSetting = (app: App) => {
       <div class="config__tab-container fn__none" style="overflow: scroll" data-name="keymap"></div>
       <div class="config__tab-container config__tab-container--full fn__none" data-name="account"></div>
       <div class="config__tab-container fn__none" data-name="repos"></div>
+      <div class="config__tab-container fn__none" data-name="publish"></div>
       <div class="config__tab-container fn__none" data-name="about"></div>
       <div class="fn__hr--b"></div>
   </div>

+ 213 - 0
app/src/config/publish.ts

@@ -0,0 +1,213 @@
+import {hasClosestByMatchTag} from "../protyle/util/hasClosest";
+import {fetchPost} from "../util/fetch";
+
+export const publish = {
+    element: undefined as Element,
+    genHTML: () => {
+        return `
+<!-- publish service -->
+<label class="fn__flex b3-label">
+    <div class="fn__flex-1">
+        ${window.siyuan.languages.publishService}
+        <div class="b3-label__text">${window.siyuan.languages.publishServiceTip}</div>
+    </div>
+    <span class="fn__space"></span>
+    <input class="b3-switch fn__flex-center" id="publishEnable" type="checkbox"${window.siyuan.config.publish.enable ? " checked" : ""}/>
+</label>
+
+<div class="b3-label">
+    <!-- publish service port -->
+    <div class="fn__flex">
+        <div class="fn__flex-1">
+            ${window.siyuan.languages.publishServicePort}
+            <div class="b3-label__text">${window.siyuan.languages.publishServicePortTip}</div>
+        </div>
+        <span class="fn__space"></span>
+        <input class="b3-text-field fn__flex-center fn__size200" id="publishPort" type="number" min="0" max="65535" value="${window.siyuan.config.publish.port}">
+    </div>
+
+    <!-- publish service address list -->
+    <div class="b3-label">
+        <div class="fn__flex config__item">
+            <div class="fn__flex-1">
+                ${window.siyuan.languages.publishServiceAddresses}
+                <div class="b3-label__text">${window.siyuan.languages.publishServiceAddressesTip}</div>
+            </div>
+            <div class="fn__space"></div>
+        </div>
+        <div class="b3-label" id="publishAddresses">
+        </div>
+    </div>
+</div>
+
+<div class="b3-label">
+    <!-- publish service auth -->
+    <label class="fn__flex">
+        <div class="fn__flex-1">
+            ${window.siyuan.languages.publishServiceAuth}
+            <div class="b3-label__text">${window.siyuan.languages.publishServiceAuthTip}</div>
+        </div>
+        <span class="fn__space"></span>
+        <input class="b3-switch fn__flex-center" id="publishAuthEnable" type="checkbox"${window.siyuan.config.publish.auth.enable ? " checked" : ""}/>
+    </label>
+
+    <!-- publish service auth accounts -->
+    <div class="b3-label">
+        <div class="fn__flex config__item">
+            <div class="fn__flex-1">
+                ${window.siyuan.languages.publishServiceAuthAccounts}
+                <div class="b3-label__text">${window.siyuan.languages.publishServiceAuthAccountsTip}</div>
+            </div>
+            <div class="fn__space"></div>
+            <button class="b3-button b3-button--outline fn__size200 fn__flex-center" id="publishAuthAccountAdd">
+                <svg><use xlink:href="#iconAdd"></use></svg>${window.siyuan.languages.publishServiceAuthAccountAdd}
+            </button>
+        </div>
+        <div class="fn__flex" id="publishAuthAccounts">
+        </div>
+    </div>
+</div>
+`;
+    },
+    bindEvent: () => {
+        const publishAuthAccountAdd = publish.element.querySelector<HTMLButtonElement>("#publishAuthAccountAdd");
+
+        /* add account */
+        publishAuthAccountAdd.addEventListener("click", () => {
+            window.siyuan.config.publish.auth.accounts.push({
+                username: "",
+                password: "",
+                memo: "",
+            });
+            publish._renderPublishAuthAccounts(publish.element);
+        });
+
+        /* input change */
+        publish.element.querySelectorAll("input").forEach(item => {
+            item.addEventListener("change", () => {
+                publish._savePublish();
+            });
+        });
+
+        publish._refreshPublish();
+    },
+    _refreshPublish: () => {
+        fetchPost("/api/setting/getPublish", {}, publish._updatePublishConfig.bind(null, true));
+    },
+    _savePublish: (reloadAccounts = true) => {
+        const publishEnable = publish.element.querySelector<HTMLInputElement>("#publishEnable");
+        const publishPort = publish.element.querySelector<HTMLInputElement>("#publishPort");
+        const publishAuthEnable = publish.element.querySelector<HTMLInputElement>("#publishAuthEnable");
+
+        fetchPost("/api/setting/setPublish", {
+            enable: publishEnable.checked,
+            port: publishPort.valueAsNumber,
+            auth: {
+                enable: publishAuthEnable.checked,
+                accounts: window.siyuan.config.publish.auth.accounts,
+            } as Config.IPublishAuth,
+        } as Config.IPublish, publish._updatePublishConfig.bind(null, reloadAccounts));
+    },
+    _updatePublishConfig: (
+        reloadAccounts: boolean,
+        response: IWebSocketData,
+    ) => {
+        if (response.code === 0) {
+            window.siyuan.config.publish = response.data.publish;
+
+            reloadAccounts && publish._renderPublishAuthAccounts(publish.element);
+            publish._renderPublishAddressList(publish.element, response.data.port);
+        } else {
+            publish._renderPublishAddressList(publish.element, 0);
+        }
+    },
+    _renderPublishAuthAccounts: (
+        element: Element,
+        accounts: Config.IPublishAuthAccount[] = window.siyuan.config.publish.auth.accounts,
+    ) => {
+        const publishAuthAccounts = element.querySelector<HTMLDivElement>("#publishAuthAccounts");
+        publishAuthAccounts.innerHTML = `<ul class="fn__flex-1" style="margin-top: 16px">${
+                accounts
+                    .map((account, index) => `
+                        <li class="b3-label--inner fn__flex" data-index="${index}">
+                            <input class="b3-text-field fn__block" data-name="username" value="${account.username}" placeholder="${window.siyuan.languages.userName}">
+                            <span class="fn__space"></span>
+                            <div class="b3-form__icona fn__block">
+                                <input class="b3-text-field fn__block b3-form__icona-input" type="password" data-name="password" value="${account.password}" placeholder="${window.siyuan.languages.password}">
+                                <svg class="b3-form__icona-icon" data-action="togglePassword"><use xlink:href="#iconEye"></use></svg>
+                            </div>
+                            <span class="fn__space"></span>
+                            <input class="b3-text-field fn__block" data-name="memo" value="${account.memo}" placeholder="${window.siyuan.languages.memo}">
+                            <span class="fn__space"></span>
+                            <span data-action="remove" class="block__icon block__icon--show">
+                                <svg><use xlink:href="#iconTrashcan"></use></svg>
+                            </span>
+                        </li>`
+                    )
+                    .join("")
+            }</ul>`;
+
+        /* account field changed */
+        publishAuthAccounts
+            .querySelectorAll("input")
+            .forEach(input => {
+                input.addEventListener("change", () => {
+                    const li = hasClosestByMatchTag(input, "LI");
+                    if (li) {
+                        const index = parseInt(li.dataset.index);
+                        const name = input.dataset.name as keyof Config.IPublishAuthAccount;
+                        if (name in window.siyuan.config.publish.auth.accounts[index]) {
+                            window.siyuan.config.publish.auth.accounts[index][name] = input.value;
+                            publish._savePublish(false);
+                        }
+                    }
+                });
+            });
+
+        /* delete account */
+        publishAuthAccounts
+            .querySelectorAll('.block__icon[data-action="remove"]')
+            .forEach(remove => {
+                remove.addEventListener("click", () => {
+                    const li = hasClosestByMatchTag(remove, "LI");
+                    if (li) {
+                        const index = parseInt(li.dataset.index);
+                        window.siyuan.config.publish.auth.accounts.splice(index, 1);
+                        publish._savePublish();
+                    }
+                });
+            });
+
+        /* Toggle the password display status */
+        publishAuthAccounts
+            .querySelectorAll('.b3-form__icona-icon[data-action="togglePassword"]')
+            .forEach(togglePassword => {
+                togglePassword.addEventListener("click", () => {
+                    const isEye = togglePassword.firstElementChild.getAttribute("xlink:href") === "#iconEye";
+                    togglePassword.firstElementChild.setAttribute("xlink:href", isEye ? "#iconEyeoff" : "#iconEye");
+                    togglePassword.previousElementSibling.setAttribute("type", isEye ? "text" : "password");
+                });
+            });
+    },
+    _renderPublishAddressList: (
+        element: Element,
+        port: number,
+    ) => {
+        const publishAddresses = element.querySelector<HTMLDivElement>("#publishAddresses");
+        if (port === 0) {
+            publishAddresses.innerText = window.siyuan.languages.publishServiceNotStarted;
+        } else {
+            publishAddresses.innerHTML = `<ul class="b3-list b3-list--background fn__flex-1">${
+                window.siyuan.config.localIPs
+                    .filter(ip => !(ip.startsWith("[") && ip.endsWith("]")))
+                    .map(ip => `<li><code class="fn__code">${ip}:${port}</code></li>`)
+                    .join("")
+                }${
+                    window.siyuan.config.localIPs
+                    .filter(ip => (ip.startsWith("[") && ip.endsWith("]")))
+                    .map(ip => `<li><code class="fn__code">${ip}:${port}</code></li>`)
+                    .join("")
+            }</ul>`;
+        }
+    },
+}

+ 26 - 15
app/src/layout/util.ts

@@ -169,13 +169,17 @@ const dockToJSON = (dock: Dock) => {
 };
 
 export const resetLayout = () => {
-    fetchPost("/api/system/setUILayout", {layout: {}}, () => {
-        window.siyuan.storage[Constants.LOCAL_FILEPOSITION] = {};
-        setStorageVal(Constants.LOCAL_FILEPOSITION, window.siyuan.storage[Constants.LOCAL_FILEPOSITION]);
-        window.siyuan.storage[Constants.LOCAL_DIALOGPOSITION] = {};
-        setStorageVal(Constants.LOCAL_DIALOGPOSITION, window.siyuan.storage[Constants.LOCAL_DIALOGPOSITION]);
+    if (window.siyuan.config.readonly) {
         window.location.reload();
-    });
+    } else {
+        fetchPost("/api/system/setUILayout", {layout: {}}, () => {
+            window.siyuan.storage[Constants.LOCAL_FILEPOSITION] = {};
+            setStorageVal(Constants.LOCAL_FILEPOSITION, window.siyuan.storage[Constants.LOCAL_FILEPOSITION]);
+            window.siyuan.storage[Constants.LOCAL_DIALOGPOSITION] = {};
+            setStorageVal(Constants.LOCAL_DIALOGPOSITION, window.siyuan.storage[Constants.LOCAL_DIALOGPOSITION]);
+            window.location.reload();
+        });
+    }
 };
 
 let saveCount = 0;
@@ -211,10 +215,12 @@ export const saveLayout = () => {
         if (isWindow()) {
             sessionStorage.setItem("layout", JSON.stringify(layoutJSON));
         } else {
-            fetchPost("/api/system/setUILayout", {
-                layout: layoutJSON,
-                errorExit: false    // 后台不接受该参数,用于请求发生错误时退出程序
-            });
+            if (!window.siyuan.config.readonly) {
+                fetchPost("/api/system/setUILayout", {
+                    layout: layoutJSON,
+                    errorExit: false    // 后台不接受该参数,用于请求发生错误时退出程序
+                });
+            }
         }
     }
 };
@@ -250,12 +256,17 @@ export const exportLayout = (options: {
     getAllModels().editor.forEach(item => {
         saveScroll(item.editor.protyle);
     });
-    fetchPost("/api/system/setUILayout", {
-        layout: layoutJSON,
-        errorExit: options.errorExit    // 后台不接受该参数,用于请求发生错误时退出程序
-    }, () => {
+
+    if (window.siyuan.config.readonly) {
         options.cb();
-    });
+    } else {
+        fetchPost("/api/system/setUILayout", {
+            layout: layoutJSON,
+            errorExit: options.errorExit    // 后台不接受该参数,用于请求发生错误时退出程序
+        }, () => {
+            options.cb();
+        });
+    }
 };
 
 export const getAllLayout = () => {

+ 2 - 1
app/src/menus/commonMenuItem.ts

@@ -507,8 +507,9 @@ export const exportMd = (id: string) => {
                                 });
                             });
                             return;
+                        } else if (response.code === 0) {
+                            showMessage(window.siyuan.languages.exportTplSucc);
                         }
-                        showMessage(window.siyuan.languages.exportTplSucc);
                     });
                     dialog.destroy();
                 });

+ 6 - 2
app/src/menus/workspace.ts

@@ -340,9 +340,13 @@ export const workspaceMenu = (app: App, rect: DOMRect) => {
                             window.siyuan.menus.menu.remove();
                             return;
                         }
-                        fetchPost("/api/system/setUILayout", {layout: item.layout}, () => {
+                        if (window.siyuan.config.readonly) {
                             window.location.reload();
-                        });
+                        } else {
+                            fetchPost("/api/system/setUILayout", {layout: item.layout}, () => {
+                                window.location.reload();
+                            });
+                        }
                     });
                 }
             });

+ 4 - 0
app/src/protyle/util/compatibility.ts

@@ -287,6 +287,10 @@ export const getLocalStorage = (cb: () => void) => {
 };
 
 export const setStorageVal = (key: string, val: any) => {
+    if (window.siyuan.config.readonly) {
+        return;
+    }
+
     fetchPost("/api/storage/setLocalStorageVal", {
         app: Constants.SIYUAN_APPID,
         key,

+ 55 - 0
app/src/types/config.d.ts

@@ -63,6 +63,11 @@ declare namespace Config {
          * Whether to open the user guide after startup
          */
         openHelp: boolean;
+        /**
+         * Publishing service
+         * 发布服务
+         */
+        publish: IPublish;
         /**
          * Whether it is running in read-only mode
          * 全局只读
@@ -1030,6 +1035,56 @@ declare namespace Config {
      */
     export type TLogLevel = "off" | "trace" | "debug" | "info" | "warn" | "error" | "fatal";
 
+    /**
+     * Publishing service
+     */
+    export interface IPublish {
+        /**
+         * Whether to open the publishing service
+         */
+        enable: boolean;
+        /**
+         * The basic authentication settings of publishing service
+         */
+        auth: IPublishAuth;
+        /**
+         * Port on which the publishing service listens
+         */
+        port: number;
+    }
+
+    /**
+     * Publishing service authentication settings
+     */
+    export interface IPublishAuth {
+        /**
+         * Whether to enable basic authentication for publishing services
+         */
+        enable: boolean;
+        /**
+         * List of basic verified accounts
+         */
+        accounts: IPublishAuthAccount[];
+    }
+
+    /**
+     * Basic authentication account
+     */
+    export interface IPublishAuthAccount {
+        /**
+         * Account username
+         */
+        username: string;
+        /**
+         * Account password
+         */
+        password: string;
+        /**
+         * The memo text of the account
+         */
+        memo: string;
+    }
+
     /**
      * Snapshot repository related configuration
      */

+ 4 - 2
app/src/util/assets.ts

@@ -150,8 +150,10 @@ export const initAssets = () => {
                     return;
                 }
             }
-            window.siyuan.config.appearance = response.data.appearance;
-            loadAssets(response.data.appearance);
+            if (response.code === 0) {
+                window.siyuan.config.appearance = response.data.appearance;
+                loadAssets(response.data.appearance);
+            }
         });
     });
 };

+ 14 - 12
app/src/util/fetch.ts

@@ -32,18 +32,20 @@ export const fetchPost = (url: string, data?: any, cb?: (response: IWebSocketDat
         init.headers = headers;
     }
     fetch(url, init).then((response) => {
-        if (response.status === 404) {
-            return {
-                data: null,
-                msg: response.statusText,
-                code: response.status,
-            };
-        } else {
-            if (response.headers.get("content-type")?.indexOf("application/json") > -1) {
-                return response.json();
-            } else {
-                return response.text();
-            }
+        switch (response.status) {
+            case 403:
+            case 404:
+                return {
+                    data: null,
+                    msg: response.statusText,
+                    code: response.status,
+                };
+            default:
+                if (response.headers.get("content-type")?.indexOf("application/json") > -1) {
+                    return response.json();
+                } else {
+                    return response.text();
+                }
         }
     }).then((response: IWebSocketData) => {
         if (typeof response === "string") {

+ 20 - 2
kernel/api/file.go

@@ -166,13 +166,31 @@ func getFile(c *gin.Context) {
 		return
 	}
 	if info.IsDir() {
-		logging.LogErrorf("file [%s] is a directory", fileAbsPath)
+		logging.LogErrorf("path [%s] is a directory path", fileAbsPath)
 		ret.Code = http.StatusMethodNotAllowed
-		ret.Msg = "file is a directory"
+		ret.Msg = "This is a directory path"
 		c.JSON(http.StatusAccepted, ret)
 		return
 	}
 
+	// REF: https://github.com/siyuan-note/siyuan/issues/11364
+	if role := model.GetGinContextRole(c); !model.IsValidRole(role, []model.Role{
+		model.RoleAdministrator,
+	}) {
+		if relPath, err := filepath.Rel(util.ConfDir, fileAbsPath); err != nil {
+			logging.LogErrorf("Get a relative path from [%s] to [%s] failed: %s", util.ConfDir, fileAbsPath, err)
+			ret.Code = http.StatusInternalServerError
+			ret.Msg = err.Error()
+			c.JSON(http.StatusAccepted, ret)
+			return
+		} else if relPath == "conf.json" {
+			ret.Code = http.StatusForbidden
+			ret.Msg = http.StatusText(http.StatusForbidden)
+			c.JSON(http.StatusAccepted, ret)
+			return
+		}
+	}
+
 	data, err := filelock.ReadFile(fileAbsPath)
 	if nil != err {
 		logging.LogErrorf("read file [%s] failed: %s", fileAbsPath, err)

+ 258 - 257
kernel/api/router.go

@@ -33,122 +33,121 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/system/loginAuth", model.LoginAuth)
 	ginServer.Handle("POST", "/api/system/logoutAuth", model.LogoutAuth)
 	ginServer.Handle("GET", "/api/system/getCaptcha", model.GetCaptcha)
-	ginServer.Handle("POST", "/api/system/setUILayout", setUILayout) // 这里不加鉴权 After modifying the access authentication code on the browser side, the other side does not refresh https://github.com/siyuan-note/siyuan/issues/8028
-	ginServer.Handle("GET", "/snippets/*filepath", serveSnippets)
 
 	// 需要鉴权
 
 	ginServer.Handle("POST", "/api/system/getEmojiConf", model.CheckAuth, getEmojiConf)
-	ginServer.Handle("POST", "/api/system/setAPIToken", model.CheckAuth, model.CheckReadonly, setAPIToken)
-	ginServer.Handle("POST", "/api/system/setAccessAuthCode", model.CheckAuth, model.CheckReadonly, setAccessAuthCode)
-	ginServer.Handle("POST", "/api/system/setFollowSystemLockScreen", model.CheckAuth, model.CheckReadonly, setFollowSystemLockScreen)
-	ginServer.Handle("POST", "/api/system/setNetworkServe", model.CheckAuth, model.CheckReadonly, setNetworkServe)
-	ginServer.Handle("POST", "/api/system/setUploadErrLog", model.CheckAuth, model.CheckReadonly, setUploadErrLog)
-	ginServer.Handle("POST", "/api/system/setAutoLaunch", model.CheckAuth, model.CheckReadonly, setAutoLaunch)
-	ginServer.Handle("POST", "/api/system/setGoogleAnalytics", model.CheckAuth, model.CheckReadonly, setGoogleAnalytics)
-	ginServer.Handle("POST", "/api/system/setDownloadInstallPkg", model.CheckAuth, model.CheckReadonly, setDownloadInstallPkg)
-	ginServer.Handle("POST", "/api/system/setNetworkProxy", model.CheckAuth, model.CheckReadonly, setNetworkProxy)
-	ginServer.Handle("POST", "/api/system/setWorkspaceDir", model.CheckAuth, model.CheckReadonly, setWorkspaceDir)
-	ginServer.Handle("POST", "/api/system/getWorkspaces", model.CheckAuth, getWorkspaces)
-	ginServer.Handle("POST", "/api/system/getMobileWorkspaces", model.CheckAuth, getMobileWorkspaces)
-	ginServer.Handle("POST", "/api/system/checkWorkspaceDir", model.CheckAuth, model.CheckReadonly, checkWorkspaceDir)
-	ginServer.Handle("POST", "/api/system/createWorkspaceDir", model.CheckAuth, model.CheckReadonly, createWorkspaceDir)
-	ginServer.Handle("POST", "/api/system/removeWorkspaceDir", model.CheckAuth, model.CheckReadonly, removeWorkspaceDir)
-	ginServer.Handle("POST", "/api/system/removeWorkspaceDirPhysically", model.CheckAuth, model.CheckReadonly, removeWorkspaceDirPhysically)
-	ginServer.Handle("POST", "/api/system/setAppearanceMode", model.CheckAuth, setAppearanceMode)
-	ginServer.Handle("POST", "/api/system/getSysFonts", model.CheckAuth, getSysFonts)
-	ginServer.Handle("POST", "/api/system/exit", model.CheckAuth, exit)
+	ginServer.Handle("POST", "/api/system/setAPIToken", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAPIToken)
+	ginServer.Handle("POST", "/api/system/setAccessAuthCode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAccessAuthCode)
+	ginServer.Handle("POST", "/api/system/setFollowSystemLockScreen", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setFollowSystemLockScreen)
+	ginServer.Handle("POST", "/api/system/setNetworkServe", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setNetworkServe)
+	ginServer.Handle("POST", "/api/system/setUploadErrLog", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setUploadErrLog)
+	ginServer.Handle("POST", "/api/system/setAutoLaunch", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAutoLaunch)
+	ginServer.Handle("POST", "/api/system/setGoogleAnalytics", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setGoogleAnalytics)
+	ginServer.Handle("POST", "/api/system/setDownloadInstallPkg", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setDownloadInstallPkg)
+	ginServer.Handle("POST", "/api/system/setNetworkProxy", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setNetworkProxy)
+	ginServer.Handle("POST", "/api/system/setWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setWorkspaceDir)
+	ginServer.Handle("POST", "/api/system/getWorkspaces", model.CheckAuth, model.CheckAdminRole, getWorkspaces)
+	ginServer.Handle("POST", "/api/system/getMobileWorkspaces", model.CheckAuth, model.CheckAdminRole, getMobileWorkspaces)
+	ginServer.Handle("POST", "/api/system/checkWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkWorkspaceDir)
+	ginServer.Handle("POST", "/api/system/createWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createWorkspaceDir)
+	ginServer.Handle("POST", "/api/system/removeWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeWorkspaceDir)
+	ginServer.Handle("POST", "/api/system/removeWorkspaceDirPhysically", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeWorkspaceDirPhysically)
+	ginServer.Handle("POST", "/api/system/setAppearanceMode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAppearanceMode)
+	ginServer.Handle("POST", "/api/system/setUILayout", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setUILayout)
+	ginServer.Handle("POST", "/api/system/getSysFonts", model.CheckAuth, model.CheckAdminRole, getSysFonts)
+	ginServer.Handle("POST", "/api/system/exit", model.CheckAuth, model.CheckAdminRole, exit)
 	ginServer.Handle("POST", "/api/system/getConf", model.CheckAuth, getConf)
-	ginServer.Handle("POST", "/api/system/checkUpdate", model.CheckAuth, checkUpdate)
-	ginServer.Handle("POST", "/api/system/exportLog", model.CheckAuth, exportLog)
+	ginServer.Handle("POST", "/api/system/checkUpdate", model.CheckAuth, model.CheckAdminRole, checkUpdate)
+	ginServer.Handle("POST", "/api/system/exportLog", model.CheckAuth, model.CheckAdminRole, exportLog)
 	ginServer.Handle("POST", "/api/system/getChangelog", model.CheckAuth, getChangelog)
-	ginServer.Handle("POST", "/api/system/getNetwork", model.CheckAuth, getNetwork)
+	ginServer.Handle("POST", "/api/system/getNetwork", model.CheckAuth, model.CheckAdminRole, getNetwork)
 
-	ginServer.Handle("POST", "/api/storage/setLocalStorage", model.CheckAuth, model.CheckReadonly, setLocalStorage)
+	ginServer.Handle("POST", "/api/storage/setLocalStorage", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setLocalStorage)
 	ginServer.Handle("POST", "/api/storage/getLocalStorage", model.CheckAuth, getLocalStorage)
-	ginServer.Handle("POST", "/api/storage/setLocalStorageVal", model.CheckAuth, model.CheckReadonly, setLocalStorageVal)
-	ginServer.Handle("POST", "/api/storage/removeLocalStorageVals", model.CheckAuth, model.CheckReadonly, removeLocalStorageVals)
-	ginServer.Handle("POST", "/api/storage/setCriterion", model.CheckAuth, model.CheckReadonly, setCriterion)
+	ginServer.Handle("POST", "/api/storage/setLocalStorageVal", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setLocalStorageVal)
+	ginServer.Handle("POST", "/api/storage/removeLocalStorageVals", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeLocalStorageVals)
+	ginServer.Handle("POST", "/api/storage/setCriterion", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setCriterion)
 	ginServer.Handle("POST", "/api/storage/getCriteria", model.CheckAuth, getCriteria)
-	ginServer.Handle("POST", "/api/storage/removeCriterion", model.CheckAuth, model.CheckReadonly, removeCriterion)
+	ginServer.Handle("POST", "/api/storage/removeCriterion", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeCriterion)
 	ginServer.Handle("POST", "/api/storage/getRecentDocs", model.CheckAuth, getRecentDocs)
 
-	ginServer.Handle("POST", "/api/account/login", model.CheckAuth, model.CheckReadonly, login)
-	ginServer.Handle("POST", "/api/account/checkActivationcode", model.CheckAuth, model.CheckReadonly, checkActivationcode)
-	ginServer.Handle("POST", "/api/account/useActivationcode", model.CheckAuth, model.CheckReadonly, useActivationcode)
-	ginServer.Handle("POST", "/api/account/deactivate", model.CheckAuth, model.CheckReadonly, deactivateUser)
-	ginServer.Handle("POST", "/api/account/startFreeTrial", model.CheckAuth, model.CheckReadonly, startFreeTrial)
+	ginServer.Handle("POST", "/api/account/login", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, login)
+	ginServer.Handle("POST", "/api/account/checkActivationcode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkActivationcode)
+	ginServer.Handle("POST", "/api/account/useActivationcode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, useActivationcode)
+	ginServer.Handle("POST", "/api/account/deactivate", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, deactivateUser)
+	ginServer.Handle("POST", "/api/account/startFreeTrial", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, startFreeTrial)
 
 	ginServer.Handle("POST", "/api/notebook/lsNotebooks", model.CheckAuth, lsNotebooks)
-	ginServer.Handle("POST", "/api/notebook/openNotebook", model.CheckAuth, model.CheckReadonly, openNotebook)
-	ginServer.Handle("POST", "/api/notebook/closeNotebook", model.CheckAuth, model.CheckReadonly, closeNotebook)
+	ginServer.Handle("POST", "/api/notebook/openNotebook", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, openNotebook)
+	ginServer.Handle("POST", "/api/notebook/closeNotebook", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, closeNotebook)
 	ginServer.Handle("POST", "/api/notebook/getNotebookConf", model.CheckAuth, getNotebookConf)
-	ginServer.Handle("POST", "/api/notebook/setNotebookConf", model.CheckAuth, model.CheckReadonly, setNotebookConf)
-	ginServer.Handle("POST", "/api/notebook/createNotebook", model.CheckAuth, model.CheckReadonly, createNotebook)
-	ginServer.Handle("POST", "/api/notebook/removeNotebook", model.CheckAuth, model.CheckReadonly, removeNotebook)
-	ginServer.Handle("POST", "/api/notebook/renameNotebook", model.CheckAuth, model.CheckReadonly, renameNotebook)
-	ginServer.Handle("POST", "/api/notebook/changeSortNotebook", model.CheckAuth, model.CheckReadonly, changeSortNotebook)
-	ginServer.Handle("POST", "/api/notebook/setNotebookIcon", model.CheckAuth, model.CheckReadonly, setNotebookIcon)
+	ginServer.Handle("POST", "/api/notebook/setNotebookConf", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setNotebookConf)
+	ginServer.Handle("POST", "/api/notebook/createNotebook", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createNotebook)
+	ginServer.Handle("POST", "/api/notebook/removeNotebook", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeNotebook)
+	ginServer.Handle("POST", "/api/notebook/renameNotebook", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameNotebook)
+	ginServer.Handle("POST", "/api/notebook/changeSortNotebook", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, changeSortNotebook)
+	ginServer.Handle("POST", "/api/notebook/setNotebookIcon", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setNotebookIcon)
 
 	ginServer.Handle("POST", "/api/filetree/searchDocs", model.CheckAuth, searchDocs)
 	ginServer.Handle("POST", "/api/filetree/listDocsByPath", model.CheckAuth, listDocsByPath)
 	ginServer.Handle("POST", "/api/filetree/getDoc", model.CheckAuth, getDoc)
 	ginServer.Handle("POST", "/api/filetree/getDocCreateSavePath", model.CheckAuth, getDocCreateSavePath)
 	ginServer.Handle("POST", "/api/filetree/getRefCreateSavePath", model.CheckAuth, getRefCreateSavePath)
-	ginServer.Handle("POST", "/api/filetree/changeSort", model.CheckAuth, model.CheckReadonly, changeSort)
-	ginServer.Handle("POST", "/api/filetree/createDocWithMd", model.CheckAuth, model.CheckReadonly, createDocWithMd)
-	ginServer.Handle("POST", "/api/filetree/createDailyNote", model.CheckAuth, model.CheckReadonly, createDailyNote)
-	ginServer.Handle("POST", "/api/filetree/createDoc", model.CheckAuth, model.CheckReadonly, createDoc)
-	ginServer.Handle("POST", "/api/filetree/renameDoc", model.CheckAuth, model.CheckReadonly, renameDoc)
-	ginServer.Handle("POST", "/api/filetree/removeDoc", model.CheckAuth, model.CheckReadonly, removeDoc)
-	ginServer.Handle("POST", "/api/filetree/removeDocs", model.CheckAuth, model.CheckReadonly, removeDocs)
-	ginServer.Handle("POST", "/api/filetree/moveDocs", model.CheckAuth, model.CheckReadonly, moveDocs)
-	ginServer.Handle("POST", "/api/filetree/duplicateDoc", model.CheckAuth, model.CheckReadonly, duplicateDoc)
+	ginServer.Handle("POST", "/api/filetree/changeSort", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, changeSort)
+	ginServer.Handle("POST", "/api/filetree/createDocWithMd", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createDocWithMd)
+	ginServer.Handle("POST", "/api/filetree/createDailyNote", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createDailyNote)
+	ginServer.Handle("POST", "/api/filetree/createDoc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createDoc)
+	ginServer.Handle("POST", "/api/filetree/renameDoc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameDoc)
+	ginServer.Handle("POST", "/api/filetree/removeDoc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeDoc)
+	ginServer.Handle("POST", "/api/filetree/removeDocs", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeDocs)
+	ginServer.Handle("POST", "/api/filetree/moveDocs", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, moveDocs)
+	ginServer.Handle("POST", "/api/filetree/duplicateDoc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, duplicateDoc)
 	ginServer.Handle("POST", "/api/filetree/getHPathByPath", model.CheckAuth, getHPathByPath)
 	ginServer.Handle("POST", "/api/filetree/getHPathsByPaths", model.CheckAuth, getHPathsByPaths)
 	ginServer.Handle("POST", "/api/filetree/getHPathByID", model.CheckAuth, getHPathByID)
 	ginServer.Handle("POST", "/api/filetree/getFullHPathByID", model.CheckAuth, getFullHPathByID)
 	ginServer.Handle("POST", "/api/filetree/getIDsByHPath", model.CheckAuth, getIDsByHPath)
-	ginServer.Handle("POST", "/api/filetree/doc2Heading", model.CheckAuth, model.CheckReadonly, doc2Heading)
-	ginServer.Handle("POST", "/api/filetree/heading2Doc", model.CheckAuth, model.CheckReadonly, heading2Doc)
-	ginServer.Handle("POST", "/api/filetree/li2Doc", model.CheckAuth, model.CheckReadonly, li2Doc)
-	ginServer.Handle("POST", "/api/filetree/refreshFiletree", model.CheckAuth, model.CheckReadonly, refreshFiletree)
-	ginServer.Handle("POST", "/api/filetree/upsertIndexes", model.CheckAuth, model.CheckReadonly, upsertIndexes)
-	ginServer.Handle("POST", "/api/filetree/removeIndexes", model.CheckAuth, model.CheckReadonly, removeIndexes)
-	ginServer.Handle("POST", "/api/filetree/listDocTree", model.CheckAuth, model.CheckReadonly, listDocTree)
-
-	ginServer.Handle("POST", "/api/format/autoSpace", model.CheckAuth, model.CheckReadonly, autoSpace)
-	ginServer.Handle("POST", "/api/format/netImg2LocalAssets", model.CheckAuth, model.CheckReadonly, netImg2LocalAssets)
-	ginServer.Handle("POST", "/api/format/netAssets2LocalAssets", model.CheckAuth, model.CheckReadonly, netAssets2LocalAssets)
-
-	ginServer.Handle("POST", "/api/history/getNotebookHistory", model.CheckAuth, getNotebookHistory)
-	ginServer.Handle("POST", "/api/history/rollbackNotebookHistory", model.CheckAuth, model.CheckReadonly, rollbackNotebookHistory)
-	ginServer.Handle("POST", "/api/history/rollbackAssetsHistory", model.CheckAuth, model.CheckReadonly, rollbackAssetsHistory)
-	ginServer.Handle("POST", "/api/history/getDocHistoryContent", model.CheckAuth, getDocHistoryContent)
-	ginServer.Handle("POST", "/api/history/rollbackDocHistory", model.CheckAuth, model.CheckReadonly, rollbackDocHistory)
-	ginServer.Handle("POST", "/api/history/clearWorkspaceHistory", model.CheckAuth, model.CheckReadonly, clearWorkspaceHistory)
-	ginServer.Handle("POST", "/api/history/reindexHistory", model.CheckAuth, model.CheckReadonly, reindexHistory)
-	ginServer.Handle("POST", "/api/history/searchHistory", model.CheckAuth, searchHistory)
-	ginServer.Handle("POST", "/api/history/getHistoryItems", model.CheckAuth, getHistoryItems)
+	ginServer.Handle("POST", "/api/filetree/doc2Heading", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, doc2Heading)
+	ginServer.Handle("POST", "/api/filetree/heading2Doc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, heading2Doc)
+	ginServer.Handle("POST", "/api/filetree/li2Doc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, li2Doc)
+	ginServer.Handle("POST", "/api/filetree/refreshFiletree", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, refreshFiletree)
+	ginServer.Handle("POST", "/api/filetree/upsertIndexes", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, upsertIndexes)
+	ginServer.Handle("POST", "/api/filetree/removeIndexes", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeIndexes)
+	ginServer.Handle("POST", "/api/filetree/listDocTree", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, listDocTree)
+
+	ginServer.Handle("POST", "/api/format/autoSpace", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, autoSpace)
+	ginServer.Handle("POST", "/api/format/netImg2LocalAssets", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, netImg2LocalAssets)
+	ginServer.Handle("POST", "/api/format/netAssets2LocalAssets", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, netAssets2LocalAssets)
+
+	ginServer.Handle("POST", "/api/history/getNotebookHistory", model.CheckAuth, model.CheckAdminRole, getNotebookHistory)
+	ginServer.Handle("POST", "/api/history/rollbackNotebookHistory", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, rollbackNotebookHistory)
+	ginServer.Handle("POST", "/api/history/rollbackAssetsHistory", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, rollbackAssetsHistory)
+	ginServer.Handle("POST", "/api/history/getDocHistoryContent", model.CheckAuth, model.CheckAdminRole, getDocHistoryContent)
+	ginServer.Handle("POST", "/api/history/rollbackDocHistory", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, rollbackDocHistory)
+	ginServer.Handle("POST", "/api/history/clearWorkspaceHistory", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, clearWorkspaceHistory)
+	ginServer.Handle("POST", "/api/history/reindexHistory", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, reindexHistory)
+	ginServer.Handle("POST", "/api/history/searchHistory", model.CheckAuth, model.CheckAdminRole, searchHistory)
+	ginServer.Handle("POST", "/api/history/getHistoryItems", model.CheckAuth, model.CheckAdminRole, getHistoryItems)
 
 	ginServer.Handle("POST", "/api/outline/getDocOutline", model.CheckAuth, getDocOutline)
 	ginServer.Handle("POST", "/api/bookmark/getBookmark", model.CheckAuth, getBookmark)
-	ginServer.Handle("POST", "/api/bookmark/renameBookmark", model.CheckAuth, model.CheckReadonly, renameBookmark)
-	ginServer.Handle("POST", "/api/bookmark/removeBookmark", model.CheckAuth, model.CheckReadonly, removeBookmark)
+	ginServer.Handle("POST", "/api/bookmark/renameBookmark", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameBookmark)
+	ginServer.Handle("POST", "/api/bookmark/removeBookmark", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeBookmark)
 	ginServer.Handle("POST", "/api/tag/getTag", model.CheckAuth, getTag)
-	ginServer.Handle("POST", "/api/tag/renameTag", model.CheckAuth, model.CheckReadonly, renameTag)
-	ginServer.Handle("POST", "/api/tag/removeTag", model.CheckAuth, model.CheckReadonly, removeTag)
+	ginServer.Handle("POST", "/api/tag/renameTag", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameTag)
+	ginServer.Handle("POST", "/api/tag/removeTag", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeTag)
 
 	ginServer.Handle("POST", "/api/lute/spinBlockDOM", model.CheckAuth, spinBlockDOM) // 未测试
 	ginServer.Handle("POST", "/api/lute/html2BlockDOM", model.CheckAuth, html2BlockDOM)
 	ginServer.Handle("POST", "/api/lute/copyStdMarkdown", model.CheckAuth, copyStdMarkdown)
 
 	ginServer.Handle("POST", "/api/query/sql", model.CheckAuth, SQL)
-	ginServer.Handle("POST", "/api/sqlite/flushTransaction", model.CheckAuth, model.CheckReadonly, flushTransaction)
+	ginServer.Handle("POST", "/api/sqlite/flushTransaction", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, flushTransaction)
 
 	ginServer.Handle("POST", "/api/search/searchTag", model.CheckAuth, searchTag)
 	ginServer.Handle("POST", "/api/search/searchTemplate", model.CheckAuth, searchTemplate)
-	ginServer.Handle("POST", "/api/search/removeTemplate", model.CheckAuth, model.CheckReadonly, removeTemplate)
+	ginServer.Handle("POST", "/api/search/removeTemplate", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeTemplate)
 	ginServer.Handle("POST", "/api/search/searchWidget", model.CheckAuth, searchWidget)
 	ginServer.Handle("POST", "/api/search/searchRefBlock", model.CheckAuth, searchRefBlock)
 	ginServer.Handle("POST", "/api/search/searchEmbedBlock", model.CheckAuth, searchEmbedBlock)
@@ -181,33 +180,33 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/block/getDocInfo", model.CheckAuth, getDocInfo)
 	ginServer.Handle("POST", "/api/block/checkBlockExist", model.CheckAuth, checkBlockExist)
 	ginServer.Handle("POST", "/api/block/checkBlockFold", model.CheckAuth, checkBlockFold)
-	ginServer.Handle("POST", "/api/block/insertBlock", model.CheckAuth, model.CheckReadonly, insertBlock)
-	ginServer.Handle("POST", "/api/block/prependBlock", model.CheckAuth, model.CheckReadonly, prependBlock)
-	ginServer.Handle("POST", "/api/block/appendBlock", model.CheckAuth, model.CheckReadonly, appendBlock)
-	ginServer.Handle("POST", "/api/block/appendDailyNoteBlock", model.CheckAuth, model.CheckReadonly, appendDailyNoteBlock)
-	ginServer.Handle("POST", "/api/block/prependDailyNoteBlock", model.CheckAuth, model.CheckReadonly, prependDailyNoteBlock)
-	ginServer.Handle("POST", "/api/block/updateBlock", model.CheckAuth, model.CheckReadonly, updateBlock)
-	ginServer.Handle("POST", "/api/block/deleteBlock", model.CheckAuth, model.CheckReadonly, deleteBlock)
-	ginServer.Handle("POST", "/api/block/moveBlock", model.CheckAuth, model.CheckReadonly, moveBlock)
-	ginServer.Handle("POST", "/api/block/moveOutlineHeading", model.CheckAuth, model.CheckReadonly, moveOutlineHeading)
-	ginServer.Handle("POST", "/api/block/foldBlock", model.CheckAuth, model.CheckReadonly, foldBlock)
-	ginServer.Handle("POST", "/api/block/unfoldBlock", model.CheckAuth, model.CheckReadonly, unfoldBlock)
-	ginServer.Handle("POST", "/api/block/setBlockReminder", model.CheckAuth, model.CheckReadonly, setBlockReminder)
+	ginServer.Handle("POST", "/api/block/insertBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, insertBlock)
+	ginServer.Handle("POST", "/api/block/prependBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, prependBlock)
+	ginServer.Handle("POST", "/api/block/appendBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, appendBlock)
+	ginServer.Handle("POST", "/api/block/appendDailyNoteBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, appendDailyNoteBlock)
+	ginServer.Handle("POST", "/api/block/prependDailyNoteBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, prependDailyNoteBlock)
+	ginServer.Handle("POST", "/api/block/updateBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, updateBlock)
+	ginServer.Handle("POST", "/api/block/deleteBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, deleteBlock)
+	ginServer.Handle("POST", "/api/block/moveBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, moveBlock)
+	ginServer.Handle("POST", "/api/block/moveOutlineHeading", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, moveOutlineHeading)
+	ginServer.Handle("POST", "/api/block/foldBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, foldBlock)
+	ginServer.Handle("POST", "/api/block/unfoldBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, unfoldBlock)
+	ginServer.Handle("POST", "/api/block/setBlockReminder", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setBlockReminder)
 	ginServer.Handle("POST", "/api/block/getHeadingLevelTransaction", model.CheckAuth, getHeadingLevelTransaction)
 	ginServer.Handle("POST", "/api/block/getHeadingDeleteTransaction", model.CheckAuth, getHeadingDeleteTransaction)
 	ginServer.Handle("POST", "/api/block/getHeadingChildrenIDs", model.CheckAuth, getHeadingChildrenIDs)
 	ginServer.Handle("POST", "/api/block/getHeadingChildrenDOM", model.CheckAuth, getHeadingChildrenDOM)
-	ginServer.Handle("POST", "/api/block/swapBlockRef", model.CheckAuth, model.CheckReadonly, swapBlockRef)
-	ginServer.Handle("POST", "/api/block/transferBlockRef", model.CheckAuth, model.CheckReadonly, transferBlockRef)
+	ginServer.Handle("POST", "/api/block/swapBlockRef", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, swapBlockRef)
+	ginServer.Handle("POST", "/api/block/transferBlockRef", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, transferBlockRef)
 	ginServer.Handle("POST", "/api/block/getBlockSiblingID", model.CheckAuth, getBlockSiblingID)
 	ginServer.Handle("POST", "/api/block/getBlockTreeInfos", model.CheckAuth, getBlockTreeInfos)
 
 	ginServer.Handle("POST", "/api/file/getFile", model.CheckAuth, getFile)
-	ginServer.Handle("POST", "/api/file/putFile", model.CheckAuth, model.CheckReadonly, putFile)
-	ginServer.Handle("POST", "/api/file/copyFile", model.CheckAuth, model.CheckReadonly, copyFile)
-	ginServer.Handle("POST", "/api/file/globalCopyFiles", model.CheckAuth, model.CheckReadonly, globalCopyFiles)
-	ginServer.Handle("POST", "/api/file/removeFile", model.CheckAuth, model.CheckReadonly, removeFile)
-	ginServer.Handle("POST", "/api/file/renameFile", model.CheckAuth, model.CheckReadonly, renameFile)
+	ginServer.Handle("POST", "/api/file/putFile", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, putFile)
+	ginServer.Handle("POST", "/api/file/copyFile", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, copyFile)
+	ginServer.Handle("POST", "/api/file/globalCopyFiles", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, globalCopyFiles)
+	ginServer.Handle("POST", "/api/file/removeFile", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeFile)
+	ginServer.Handle("POST", "/api/file/renameFile", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameFile)
 	ginServer.Handle("POST", "/api/file/readDir", model.CheckAuth, readDir)
 	ginServer.Handle("POST", "/api/file/getUniqueFilename", model.CheckAuth, getUniqueFilename)
 
@@ -218,57 +217,57 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/ref/getBackmentionDoc", model.CheckAuth, getBackmentionDoc)
 
 	ginServer.Handle("POST", "/api/attr/getBookmarkLabels", model.CheckAuth, getBookmarkLabels)
-	ginServer.Handle("POST", "/api/attr/resetBlockAttrs", model.CheckAuth, model.CheckReadonly, resetBlockAttrs)
-	ginServer.Handle("POST", "/api/attr/setBlockAttrs", model.CheckAuth, model.CheckReadonly, setBlockAttrs)
-	ginServer.Handle("POST", "/api/attr/batchSetBlockAttrs", model.CheckAuth, model.CheckReadonly, batchSetBlockAttrs)
+	ginServer.Handle("POST", "/api/attr/resetBlockAttrs", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, resetBlockAttrs)
+	ginServer.Handle("POST", "/api/attr/setBlockAttrs", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setBlockAttrs)
+	ginServer.Handle("POST", "/api/attr/batchSetBlockAttrs", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, batchSetBlockAttrs)
 	ginServer.Handle("POST", "/api/attr/getBlockAttrs", model.CheckAuth, getBlockAttrs)
 
-	ginServer.Handle("POST", "/api/cloud/getCloudSpace", model.CheckAuth, getCloudSpace)
-
-	ginServer.Handle("POST", "/api/sync/setSyncEnable", model.CheckAuth, model.CheckReadonly, setSyncEnable)
-	ginServer.Handle("POST", "/api/sync/setSyncPerception", model.CheckAuth, model.CheckReadonly, setSyncPerception)
-	ginServer.Handle("POST", "/api/sync/setSyncGenerateConflictDoc", model.CheckAuth, model.CheckReadonly, setSyncGenerateConflictDoc)
-	ginServer.Handle("POST", "/api/sync/setSyncMode", model.CheckAuth, model.CheckReadonly, setSyncMode)
-	ginServer.Handle("POST", "/api/sync/setSyncProvider", model.CheckAuth, model.CheckReadonly, setSyncProvider)
-	ginServer.Handle("POST", "/api/sync/setSyncProviderS3", model.CheckAuth, model.CheckReadonly, setSyncProviderS3)
-	ginServer.Handle("POST", "/api/sync/setSyncProviderWebDAV", model.CheckAuth, model.CheckReadonly, setSyncProviderWebDAV)
-	ginServer.Handle("POST", "/api/sync/setCloudSyncDir", model.CheckAuth, model.CheckReadonly, setCloudSyncDir)
-	ginServer.Handle("POST", "/api/sync/createCloudSyncDir", model.CheckAuth, model.CheckReadonly, createCloudSyncDir)
-	ginServer.Handle("POST", "/api/sync/removeCloudSyncDir", model.CheckAuth, model.CheckReadonly, removeCloudSyncDir)
-	ginServer.Handle("POST", "/api/sync/listCloudSyncDir", model.CheckAuth, listCloudSyncDir)
-	ginServer.Handle("POST", "/api/sync/performSync", model.CheckAuth, model.CheckReadonly, performSync)
-	ginServer.Handle("POST", "/api/sync/performBootSync", model.CheckAuth, model.CheckReadonly, performBootSync)
-	ginServer.Handle("POST", "/api/sync/getBootSync", model.CheckAuth, getBootSync)
-	ginServer.Handle("POST", "/api/sync/getSyncInfo", model.CheckAuth, getSyncInfo)
-	ginServer.Handle("POST", "/api/sync/exportSyncProviderS3", model.CheckAuth, exportSyncProviderS3)
-	ginServer.Handle("POST", "/api/sync/importSyncProviderS3", model.CheckAuth, model.CheckReadonly, importSyncProviderS3)
-	ginServer.Handle("POST", "/api/sync/exportSyncProviderWebDAV", model.CheckAuth, exportSyncProviderWebDAV)
-	ginServer.Handle("POST", "/api/sync/importSyncProviderWebDAV", model.CheckAuth, model.CheckReadonly, importSyncProviderWebDAV)
-
-	ginServer.Handle("POST", "/api/inbox/getShorthands", model.CheckAuth, getShorthands)
-	ginServer.Handle("POST", "/api/inbox/getShorthand", model.CheckAuth, getShorthand)
-	ginServer.Handle("POST", "/api/inbox/removeShorthands", model.CheckAuth, model.CheckReadonly, removeShorthands)
-
-	ginServer.Handle("POST", "/api/extension/copy", model.CheckAuth, model.CheckReadonly, extensionCopy)
-
-	ginServer.Handle("POST", "/api/clipboard/readFilePaths", model.CheckAuth, readFilePaths)
-
-	ginServer.Handle("POST", "/api/asset/uploadCloud", model.CheckAuth, model.CheckReadonly, uploadCloud)
-	ginServer.Handle("POST", "/api/asset/insertLocalAssets", model.CheckAuth, model.CheckReadonly, insertLocalAssets)
+	ginServer.Handle("POST", "/api/cloud/getCloudSpace", model.CheckAuth, model.CheckAdminRole, getCloudSpace)
+
+	ginServer.Handle("POST", "/api/sync/setSyncEnable", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncEnable)
+	ginServer.Handle("POST", "/api/sync/setSyncPerception", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncPerception)
+	ginServer.Handle("POST", "/api/sync/setSyncGenerateConflictDoc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncGenerateConflictDoc)
+	ginServer.Handle("POST", "/api/sync/setSyncMode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncMode)
+	ginServer.Handle("POST", "/api/sync/setSyncProvider", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncProvider)
+	ginServer.Handle("POST", "/api/sync/setSyncProviderS3", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncProviderS3)
+	ginServer.Handle("POST", "/api/sync/setSyncProviderWebDAV", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSyncProviderWebDAV)
+	ginServer.Handle("POST", "/api/sync/setCloudSyncDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setCloudSyncDir)
+	ginServer.Handle("POST", "/api/sync/createCloudSyncDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createCloudSyncDir)
+	ginServer.Handle("POST", "/api/sync/removeCloudSyncDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeCloudSyncDir)
+	ginServer.Handle("POST", "/api/sync/listCloudSyncDir", model.CheckAuth, model.CheckAdminRole, listCloudSyncDir)
+	ginServer.Handle("POST", "/api/sync/performSync", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, performSync)
+	ginServer.Handle("POST", "/api/sync/performBootSync", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, performBootSync)
+	ginServer.Handle("POST", "/api/sync/getBootSync", model.CheckAuth, model.CheckAdminRole, getBootSync)
+	ginServer.Handle("POST", "/api/sync/getSyncInfo", model.CheckAuth, model.CheckAdminRole, getSyncInfo)
+	ginServer.Handle("POST", "/api/sync/exportSyncProviderS3", model.CheckAuth, model.CheckAdminRole, exportSyncProviderS3)
+	ginServer.Handle("POST", "/api/sync/importSyncProviderS3", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importSyncProviderS3)
+	ginServer.Handle("POST", "/api/sync/exportSyncProviderWebDAV", model.CheckAuth, model.CheckAdminRole, exportSyncProviderWebDAV)
+	ginServer.Handle("POST", "/api/sync/importSyncProviderWebDAV", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importSyncProviderWebDAV)
+
+	ginServer.Handle("POST", "/api/inbox/getShorthands", model.CheckAuth, model.CheckAdminRole, getShorthands)
+	ginServer.Handle("POST", "/api/inbox/getShorthand", model.CheckAuth, model.CheckAdminRole, getShorthand)
+	ginServer.Handle("POST", "/api/inbox/removeShorthands", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeShorthands)
+
+	ginServer.Handle("POST", "/api/extension/copy", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, extensionCopy)
+
+	ginServer.Handle("POST", "/api/clipboard/readFilePaths", model.CheckAuth, model.CheckAdminRole, readFilePaths)
+
+	ginServer.Handle("POST", "/api/asset/uploadCloud", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uploadCloud)
+	ginServer.Handle("POST", "/api/asset/insertLocalAssets", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, insertLocalAssets)
 	ginServer.Handle("POST", "/api/asset/resolveAssetPath", model.CheckAuth, resolveAssetPath)
-	ginServer.Handle("POST", "/api/asset/upload", model.CheckAuth, model.CheckReadonly, model.Upload)
-	ginServer.Handle("POST", "/api/asset/setFileAnnotation", model.CheckAuth, model.CheckReadonly, setFileAnnotation)
+	ginServer.Handle("POST", "/api/asset/upload", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, model.Upload)
+	ginServer.Handle("POST", "/api/asset/setFileAnnotation", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setFileAnnotation)
 	ginServer.Handle("POST", "/api/asset/getFileAnnotation", model.CheckAuth, getFileAnnotation)
 	ginServer.Handle("POST", "/api/asset/getUnusedAssets", model.CheckAuth, getUnusedAssets)
 	ginServer.Handle("POST", "/api/asset/getMissingAssets", model.CheckAuth, getMissingAssets)
-	ginServer.Handle("POST", "/api/asset/removeUnusedAsset", model.CheckAuth, model.CheckReadonly, removeUnusedAsset)
-	ginServer.Handle("POST", "/api/asset/removeUnusedAssets", model.CheckAuth, model.CheckReadonly, removeUnusedAssets)
+	ginServer.Handle("POST", "/api/asset/removeUnusedAsset", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeUnusedAsset)
+	ginServer.Handle("POST", "/api/asset/removeUnusedAssets", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeUnusedAssets)
 	ginServer.Handle("POST", "/api/asset/getDocImageAssets", model.CheckAuth, getDocImageAssets)
-	ginServer.Handle("POST", "/api/asset/renameAsset", model.CheckAuth, model.CheckReadonly, renameAsset)
-	ginServer.Handle("POST", "/api/asset/getImageOCRText", model.CheckAuth, model.CheckReadonly, getImageOCRText)
-	ginServer.Handle("POST", "/api/asset/setImageOCRText", model.CheckAuth, model.CheckReadonly, setImageOCRText)
-	ginServer.Handle("POST", "/api/asset/fullReindexAssetContent", model.CheckAuth, model.CheckReadonly, fullReindexAssetContent)
-	ginServer.Handle("POST", "/api/asset/statAsset", model.CheckAuth, statAsset)
+	ginServer.Handle("POST", "/api/asset/renameAsset", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameAsset)
+	ginServer.Handle("POST", "/api/asset/getImageOCRText", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getImageOCRText)
+	ginServer.Handle("POST", "/api/asset/setImageOCRText", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setImageOCRText)
+	ginServer.Handle("POST", "/api/asset/fullReindexAssetContent", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, fullReindexAssetContent)
+	ginServer.Handle("POST", "/api/asset/statAsset", model.CheckAuth, model.CheckAdminRole, statAsset)
 
 	ginServer.Handle("POST", "/api/export/batchExportMd", model.CheckAuth, batchExportMd)
 	ginServer.Handle("POST", "/api/export/exportMd", model.CheckAuth, exportMd)
@@ -286,7 +285,7 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/export/exportData", model.CheckAuth, exportData)
 	ginServer.Handle("POST", "/api/export/exportDataInFolder", model.CheckAuth, exportDataInFolder)
 	ginServer.Handle("POST", "/api/export/exportTempContent", model.CheckAuth, exportTempContent)
-	ginServer.Handle("POST", "/api/export/export2Liandi", model.CheckAuth, export2Liandi)
+	ginServer.Handle("POST", "/api/export/export2Liandi", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, export2Liandi)
 	ginServer.Handle("POST", "/api/export/exportReStructuredText", model.CheckAuth, exportReStructuredText)
 	ginServer.Handle("POST", "/api/export/exportAsciiDoc", model.CheckAuth, exportAsciiDoc)
 	ginServer.Handle("POST", "/api/export/exportTextile", model.CheckAuth, exportTextile)
@@ -298,150 +297,152 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/export/exportEPUB", model.CheckAuth, exportEPUB)
 	ginServer.Handle("POST", "/api/export/exportAttributeView", model.CheckAuth, exportAttributeView)
 
-	ginServer.Handle("POST", "/api/import/importStdMd", model.CheckAuth, model.CheckReadonly, importStdMd)
-	ginServer.Handle("POST", "/api/import/importData", model.CheckAuth, model.CheckReadonly, importData)
-	ginServer.Handle("POST", "/api/import/importSY", model.CheckAuth, model.CheckReadonly, importSY)
+	ginServer.Handle("POST", "/api/import/importStdMd", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importStdMd)
+	ginServer.Handle("POST", "/api/import/importData", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importData)
+	ginServer.Handle("POST", "/api/import/importSY", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importSY)
 
-	ginServer.Handle("POST", "/api/convert/pandoc", model.CheckAuth, model.CheckReadonly, pandoc)
+	ginServer.Handle("POST", "/api/convert/pandoc", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, pandoc)
 
-	ginServer.Handle("POST", "/api/template/render", model.CheckAuth, renderTemplate)
-	ginServer.Handle("POST", "/api/template/docSaveAsTemplate", model.CheckAuth, model.CheckReadonly, docSaveAsTemplate)
+	ginServer.Handle("POST", "/api/template/render", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renderTemplate)
+	ginServer.Handle("POST", "/api/template/docSaveAsTemplate", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, docSaveAsTemplate)
 	ginServer.Handle("POST", "/api/template/renderSprig", model.CheckAuth, renderSprig)
 
-	ginServer.Handle("POST", "/api/transactions", model.CheckAuth, model.CheckReadonly, performTransactions)
-
-	ginServer.Handle("POST", "/api/setting/setAccount", model.CheckAuth, model.CheckReadonly, setAccount)
-	ginServer.Handle("POST", "/api/setting/setEditor", model.CheckAuth, model.CheckReadonly, setEditor)
-	ginServer.Handle("POST", "/api/setting/setExport", model.CheckAuth, model.CheckReadonly, setExport)
-	ginServer.Handle("POST", "/api/setting/setFiletree", model.CheckAuth, model.CheckReadonly, setFiletree)
-	ginServer.Handle("POST", "/api/setting/setSearch", model.CheckAuth, model.CheckReadonly, setSearch)
-	ginServer.Handle("POST", "/api/setting/setKeymap", model.CheckAuth, model.CheckReadonly, setKeymap)
-	ginServer.Handle("POST", "/api/setting/setAppearance", model.CheckAuth, model.CheckReadonly, setAppearance)
-	ginServer.Handle("POST", "/api/setting/getCloudUser", model.CheckAuth, getCloudUser)
-	ginServer.Handle("POST", "/api/setting/logoutCloudUser", model.CheckAuth, model.CheckReadonly, logoutCloudUser)
-	ginServer.Handle("POST", "/api/setting/login2faCloudUser", model.CheckAuth, model.CheckReadonly, login2faCloudUser)
-	ginServer.Handle("POST", "/api/setting/setEmoji", model.CheckAuth, model.CheckReadonly, setEmoji)
-	ginServer.Handle("POST", "/api/setting/setFlashcard", model.CheckAuth, model.CheckReadonly, setFlashcard)
-	ginServer.Handle("POST", "/api/setting/setAI", model.CheckAuth, model.CheckReadonly, setAI)
-	ginServer.Handle("POST", "/api/setting/setBazaar", model.CheckAuth, model.CheckReadonly, setBazaar)
-	ginServer.Handle("POST", "/api/setting/refreshVirtualBlockRef", model.CheckAuth, model.CheckReadonly, refreshVirtualBlockRef)
-	ginServer.Handle("POST", "/api/setting/addVirtualBlockRefInclude", model.CheckAuth, model.CheckReadonly, addVirtualBlockRefInclude)
-	ginServer.Handle("POST", "/api/setting/addVirtualBlockRefExclude", model.CheckAuth, model.CheckReadonly, addVirtualBlockRefExclude)
-	ginServer.Handle("POST", "/api/setting/setSnippet", model.CheckAuth, model.CheckReadonly, setConfSnippet)
-	ginServer.Handle("POST", "/api/setting/setEditorReadOnly", model.CheckAuth, model.CheckReadonly, setEditorReadOnly)
-
-	ginServer.Handle("POST", "/api/graph/resetGraph", model.CheckAuth, model.CheckReadonly, resetGraph)
-	ginServer.Handle("POST", "/api/graph/resetLocalGraph", model.CheckAuth, model.CheckReadonly, resetLocalGraph)
+	ginServer.Handle("POST", "/api/transactions", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, performTransactions)
+
+	ginServer.Handle("POST", "/api/setting/setAccount", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAccount)
+	ginServer.Handle("POST", "/api/setting/setEditor", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setEditor)
+	ginServer.Handle("POST", "/api/setting/setExport", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setExport)
+	ginServer.Handle("POST", "/api/setting/setFiletree", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setFiletree)
+	ginServer.Handle("POST", "/api/setting/setSearch", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSearch)
+	ginServer.Handle("POST", "/api/setting/setKeymap", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setKeymap)
+	ginServer.Handle("POST", "/api/setting/setAppearance", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAppearance)
+	ginServer.Handle("POST", "/api/setting/getCloudUser", model.CheckAuth, model.CheckAdminRole, getCloudUser)
+	ginServer.Handle("POST", "/api/setting/logoutCloudUser", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, logoutCloudUser)
+	ginServer.Handle("POST", "/api/setting/login2faCloudUser", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, login2faCloudUser)
+	ginServer.Handle("POST", "/api/setting/setEmoji", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setEmoji)
+	ginServer.Handle("POST", "/api/setting/setFlashcard", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setFlashcard)
+	ginServer.Handle("POST", "/api/setting/setAI", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAI)
+	ginServer.Handle("POST", "/api/setting/setBazaar", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setBazaar)
+	ginServer.Handle("POST", "/api/setting/setPublish", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setPublish)
+	ginServer.Handle("POST", "/api/setting/getPublish", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getPublish)
+	ginServer.Handle("POST", "/api/setting/refreshVirtualBlockRef", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, refreshVirtualBlockRef)
+	ginServer.Handle("POST", "/api/setting/addVirtualBlockRefInclude", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, addVirtualBlockRefInclude)
+	ginServer.Handle("POST", "/api/setting/addVirtualBlockRefExclude", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, addVirtualBlockRefExclude)
+	ginServer.Handle("POST", "/api/setting/setSnippet", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setConfSnippet)
+	ginServer.Handle("POST", "/api/setting/setEditorReadOnly", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setEditorReadOnly)
+
+	ginServer.Handle("POST", "/api/graph/resetGraph", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, resetGraph)
+	ginServer.Handle("POST", "/api/graph/resetLocalGraph", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, resetLocalGraph)
 	ginServer.Handle("POST", "/api/graph/getGraph", model.CheckAuth, getGraph)
 	ginServer.Handle("POST", "/api/graph/getLocalGraph", model.CheckAuth, getLocalGraph)
 
 	ginServer.Handle("POST", "/api/bazaar/getBazaarPlugin", model.CheckAuth, getBazaarPlugin)
 	ginServer.Handle("POST", "/api/bazaar/getInstalledPlugin", model.CheckAuth, getInstalledPlugin)
-	ginServer.Handle("POST", "/api/bazaar/installBazaarPlugin", model.CheckAuth, model.CheckReadonly, installBazaarPlugin)
-	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarPlugin", model.CheckAuth, model.CheckReadonly, uninstallBazaarPlugin)
+	ginServer.Handle("POST", "/api/bazaar/installBazaarPlugin", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, installBazaarPlugin)
+	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarPlugin", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uninstallBazaarPlugin)
 	ginServer.Handle("POST", "/api/bazaar/getBazaarWidget", model.CheckAuth, getBazaarWidget)
 	ginServer.Handle("POST", "/api/bazaar/getInstalledWidget", model.CheckAuth, getInstalledWidget)
-	ginServer.Handle("POST", "/api/bazaar/installBazaarWidget", model.CheckAuth, model.CheckReadonly, installBazaarWidget)
-	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarWidget", model.CheckAuth, model.CheckReadonly, uninstallBazaarWidget)
+	ginServer.Handle("POST", "/api/bazaar/installBazaarWidget", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, installBazaarWidget)
+	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarWidget", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uninstallBazaarWidget)
 	ginServer.Handle("POST", "/api/bazaar/getBazaarIcon", model.CheckAuth, getBazaarIcon)
 	ginServer.Handle("POST", "/api/bazaar/getInstalledIcon", model.CheckAuth, getInstalledIcon)
-	ginServer.Handle("POST", "/api/bazaar/installBazaarIcon", model.CheckAuth, model.CheckReadonly, installBazaarIcon)
-	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarIcon", model.CheckAuth, model.CheckReadonly, uninstallBazaarIcon)
+	ginServer.Handle("POST", "/api/bazaar/installBazaarIcon", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, installBazaarIcon)
+	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarIcon", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uninstallBazaarIcon)
 	ginServer.Handle("POST", "/api/bazaar/getBazaarTemplate", model.CheckAuth, getBazaarTemplate)
 	ginServer.Handle("POST", "/api/bazaar/getInstalledTemplate", model.CheckAuth, getInstalledTemplate)
-	ginServer.Handle("POST", "/api/bazaar/installBazaarTemplate", model.CheckAuth, model.CheckReadonly, installBazaarTemplate)
-	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarTemplate", model.CheckAuth, model.CheckReadonly, uninstallBazaarTemplate)
+	ginServer.Handle("POST", "/api/bazaar/installBazaarTemplate", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, installBazaarTemplate)
+	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarTemplate", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uninstallBazaarTemplate)
 	ginServer.Handle("POST", "/api/bazaar/getBazaarTheme", model.CheckAuth, getBazaarTheme)
 	ginServer.Handle("POST", "/api/bazaar/getInstalledTheme", model.CheckAuth, getInstalledTheme)
-	ginServer.Handle("POST", "/api/bazaar/installBazaarTheme", model.CheckAuth, model.CheckReadonly, installBazaarTheme)
-	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarTheme", model.CheckAuth, model.CheckReadonly, uninstallBazaarTheme)
+	ginServer.Handle("POST", "/api/bazaar/installBazaarTheme", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, installBazaarTheme)
+	ginServer.Handle("POST", "/api/bazaar/uninstallBazaarTheme", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uninstallBazaarTheme)
 	ginServer.Handle("POST", "/api/bazaar/getBazaarPackageREAME", model.CheckAuth, getBazaarPackageREAME)
 	ginServer.Handle("POST", "/api/bazaar/getUpdatedPackage", model.CheckAuth, getUpdatedPackage)
-	ginServer.Handle("POST", "/api/bazaar/batchUpdatePackage", model.CheckAuth, batchUpdatePackage)
-
-	ginServer.Handle("POST", "/api/repo/initRepoKey", model.CheckAuth, model.CheckReadonly, initRepoKey)
-	ginServer.Handle("POST", "/api/repo/initRepoKeyFromPassphrase", model.CheckAuth, model.CheckReadonly, initRepoKeyFromPassphrase)
-	ginServer.Handle("POST", "/api/repo/resetRepo", model.CheckAuth, model.CheckReadonly, resetRepo)
-	ginServer.Handle("POST", "/api/repo/purgeRepo", model.CheckAuth, model.CheckReadonly, purgeRepo)
-	ginServer.Handle("POST", "/api/repo/purgeCloudRepo", model.CheckAuth, model.CheckReadonly, purgeCloudRepo)
-	ginServer.Handle("POST", "/api/repo/importRepoKey", model.CheckAuth, model.CheckReadonly, importRepoKey)
-	ginServer.Handle("POST", "/api/repo/createSnapshot", model.CheckAuth, model.CheckReadonly, createSnapshot)
-	ginServer.Handle("POST", "/api/repo/tagSnapshot", model.CheckAuth, model.CheckReadonly, tagSnapshot)
-	ginServer.Handle("POST", "/api/repo/checkoutRepo", model.CheckAuth, model.CheckReadonly, checkoutRepo)
-	ginServer.Handle("POST", "/api/repo/getRepoSnapshots", model.CheckAuth, getRepoSnapshots)
-	ginServer.Handle("POST", "/api/repo/getRepoTagSnapshots", model.CheckAuth, getRepoTagSnapshots)
-	ginServer.Handle("POST", "/api/repo/removeRepoTagSnapshot", model.CheckAuth, model.CheckReadonly, removeRepoTagSnapshot)
-	ginServer.Handle("POST", "/api/repo/getCloudRepoTagSnapshots", model.CheckAuth, getCloudRepoTagSnapshots)
-	ginServer.Handle("POST", "/api/repo/getCloudRepoSnapshots", model.CheckAuth, getCloudRepoSnapshots)
-	ginServer.Handle("POST", "/api/repo/removeCloudRepoTagSnapshot", model.CheckAuth, model.CheckReadonly, removeCloudRepoTagSnapshot)
-	ginServer.Handle("POST", "/api/repo/uploadCloudSnapshot", model.CheckAuth, model.CheckReadonly, uploadCloudSnapshot)
-	ginServer.Handle("POST", "/api/repo/downloadCloudSnapshot", model.CheckAuth, model.CheckReadonly, downloadCloudSnapshot)
-	ginServer.Handle("POST", "/api/repo/diffRepoSnapshots", model.CheckAuth, diffRepoSnapshots)
-	ginServer.Handle("POST", "/api/repo/openRepoSnapshotDoc", model.CheckAuth, openRepoSnapshotDoc)
-	ginServer.Handle("POST", "/api/repo/getRepoFile", model.CheckAuth, getRepoFile)
-
-	ginServer.Handle("POST", "/api/riff/createRiffDeck", model.CheckAuth, model.CheckReadonly, createRiffDeck)
-	ginServer.Handle("POST", "/api/riff/renameRiffDeck", model.CheckAuth, model.CheckReadonly, renameRiffDeck)
-	ginServer.Handle("POST", "/api/riff/removeRiffDeck", model.CheckAuth, model.CheckReadonly, removeRiffDeck)
-	ginServer.Handle("POST", "/api/riff/getRiffDecks", model.CheckAuth, getRiffDecks)
-	ginServer.Handle("POST", "/api/riff/addRiffCards", model.CheckAuth, model.CheckReadonly, addRiffCards)
-	ginServer.Handle("POST", "/api/riff/removeRiffCards", model.CheckAuth, model.CheckReadonly, removeRiffCards)
-	ginServer.Handle("POST", "/api/riff/getRiffDueCards", model.CheckAuth, getRiffDueCards)
-	ginServer.Handle("POST", "/api/riff/getTreeRiffDueCards", model.CheckAuth, getTreeRiffDueCards)
-	ginServer.Handle("POST", "/api/riff/getNotebookRiffDueCards", model.CheckAuth, getNotebookRiffDueCards)
-	ginServer.Handle("POST", "/api/riff/reviewRiffCard", model.CheckAuth, model.CheckReadonly, reviewRiffCard)
-	ginServer.Handle("POST", "/api/riff/skipReviewRiffCard", model.CheckAuth, model.CheckReadonly, skipReviewRiffCard)
-	ginServer.Handle("POST", "/api/riff/getRiffCards", model.CheckAuth, getRiffCards)
-	ginServer.Handle("POST", "/api/riff/getTreeRiffCards", model.CheckAuth, getTreeRiffCards)
-	ginServer.Handle("POST", "/api/riff/getNotebookRiffCards", model.CheckAuth, getNotebookRiffCards)
-	ginServer.Handle("POST", "/api/riff/resetRiffCards", model.CheckAuth, model.CheckReadonly, resetRiffCards)
-	ginServer.Handle("POST", "/api/riff/batchSetRiffCardsDueTime", model.CheckAuth, model.CheckReadonly, batchSetRiffCardsDueTime)
-	ginServer.Handle("POST", "/api/riff/getRiffCardsByBlockIDs", model.CheckAuth, model.CheckReadonly, getRiffCardsByBlockIDs)
-
-	ginServer.Handle("POST", "/api/notification/pushMsg", model.CheckAuth, pushMsg)
-	ginServer.Handle("POST", "/api/notification/pushErrMsg", model.CheckAuth, pushErrMsg)
+	ginServer.Handle("POST", "/api/bazaar/batchUpdatePackage", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, batchUpdatePackage)
+
+	ginServer.Handle("POST", "/api/repo/initRepoKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, initRepoKey)
+	ginServer.Handle("POST", "/api/repo/initRepoKeyFromPassphrase", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, initRepoKeyFromPassphrase)
+	ginServer.Handle("POST", "/api/repo/resetRepo", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, resetRepo)
+	ginServer.Handle("POST", "/api/repo/purgeRepo", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, purgeRepo)
+	ginServer.Handle("POST", "/api/repo/purgeCloudRepo", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, purgeCloudRepo)
+	ginServer.Handle("POST", "/api/repo/importRepoKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importRepoKey)
+	ginServer.Handle("POST", "/api/repo/createSnapshot", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createSnapshot)
+	ginServer.Handle("POST", "/api/repo/tagSnapshot", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, tagSnapshot)
+	ginServer.Handle("POST", "/api/repo/checkoutRepo", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkoutRepo)
+	ginServer.Handle("POST", "/api/repo/getRepoSnapshots", model.CheckAuth, model.CheckAdminRole, getRepoSnapshots)
+	ginServer.Handle("POST", "/api/repo/getRepoTagSnapshots", model.CheckAuth, model.CheckAdminRole, getRepoTagSnapshots)
+	ginServer.Handle("POST", "/api/repo/removeRepoTagSnapshot", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeRepoTagSnapshot)
+	ginServer.Handle("POST", "/api/repo/getCloudRepoTagSnapshots", model.CheckAuth, model.CheckAdminRole, getCloudRepoTagSnapshots)
+	ginServer.Handle("POST", "/api/repo/getCloudRepoSnapshots", model.CheckAuth, model.CheckAdminRole, getCloudRepoSnapshots)
+	ginServer.Handle("POST", "/api/repo/removeCloudRepoTagSnapshot", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeCloudRepoTagSnapshot)
+	ginServer.Handle("POST", "/api/repo/uploadCloudSnapshot", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uploadCloudSnapshot)
+	ginServer.Handle("POST", "/api/repo/downloadCloudSnapshot", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, downloadCloudSnapshot)
+	ginServer.Handle("POST", "/api/repo/diffRepoSnapshots", model.CheckAuth, model.CheckAdminRole, diffRepoSnapshots)
+	ginServer.Handle("POST", "/api/repo/openRepoSnapshotDoc", model.CheckAuth, model.CheckAdminRole, openRepoSnapshotDoc)
+	ginServer.Handle("POST", "/api/repo/getRepoFile", model.CheckAuth, model.CheckAdminRole, getRepoFile)
+
+	ginServer.Handle("POST", "/api/riff/createRiffDeck", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createRiffDeck)
+	ginServer.Handle("POST", "/api/riff/renameRiffDeck", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, renameRiffDeck)
+	ginServer.Handle("POST", "/api/riff/removeRiffDeck", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeRiffDeck)
+	ginServer.Handle("POST", "/api/riff/getRiffDecks", model.CheckAuth, model.CheckAdminRole, getRiffDecks)
+	ginServer.Handle("POST", "/api/riff/addRiffCards", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, addRiffCards)
+	ginServer.Handle("POST", "/api/riff/removeRiffCards", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeRiffCards)
+	ginServer.Handle("POST", "/api/riff/getRiffDueCards", model.CheckAuth, model.CheckAdminRole, getRiffDueCards)
+	ginServer.Handle("POST", "/api/riff/getTreeRiffDueCards", model.CheckAuth, model.CheckAdminRole, getTreeRiffDueCards)
+	ginServer.Handle("POST", "/api/riff/getNotebookRiffDueCards", model.CheckAuth, model.CheckAdminRole, getNotebookRiffDueCards)
+	ginServer.Handle("POST", "/api/riff/reviewRiffCard", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, reviewRiffCard)
+	ginServer.Handle("POST", "/api/riff/skipReviewRiffCard", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, skipReviewRiffCard)
+	ginServer.Handle("POST", "/api/riff/getRiffCards", model.CheckAuth, model.CheckAdminRole, getRiffCards)
+	ginServer.Handle("POST", "/api/riff/getTreeRiffCards", model.CheckAuth, model.CheckAdminRole, getTreeRiffCards)
+	ginServer.Handle("POST", "/api/riff/getNotebookRiffCards", model.CheckAuth, model.CheckAdminRole, getNotebookRiffCards)
+	ginServer.Handle("POST", "/api/riff/resetRiffCards", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, resetRiffCards)
+	ginServer.Handle("POST", "/api/riff/batchSetRiffCardsDueTime", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, batchSetRiffCardsDueTime)
+	ginServer.Handle("POST", "/api/riff/getRiffCardsByBlockIDs", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getRiffCardsByBlockIDs)
+
+	ginServer.Handle("POST", "/api/notification/pushMsg", model.CheckAuth, model.CheckAdminRole, pushMsg)
+	ginServer.Handle("POST", "/api/notification/pushErrMsg", model.CheckAuth, model.CheckAdminRole, pushErrMsg)
 
 	ginServer.Handle("POST", "/api/snippet/getSnippet", model.CheckAuth, getSnippet)
-	ginServer.Handle("POST", "/api/snippet/setSnippet", model.CheckAuth, model.CheckReadonly, setSnippet)
-	ginServer.Handle("POST", "/api/snippet/removeSnippet", model.CheckAuth, model.CheckReadonly, removeSnippet)
+	ginServer.Handle("POST", "/api/snippet/setSnippet", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSnippet)
+	ginServer.Handle("POST", "/api/snippet/removeSnippet", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeSnippet)
 
 	ginServer.Handle("POST", "/api/av/renderAttributeView", model.CheckAuth, renderAttributeView)
-	ginServer.Handle("POST", "/api/av/renderHistoryAttributeView", model.CheckAuth, renderHistoryAttributeView)
-	ginServer.Handle("POST", "/api/av/renderSnapshotAttributeView", model.CheckAuth, renderSnapshotAttributeView)
+	ginServer.Handle("POST", "/api/av/renderHistoryAttributeView", model.CheckAuth, model.CheckAdminRole, renderHistoryAttributeView)
+	ginServer.Handle("POST", "/api/av/renderSnapshotAttributeView", model.CheckAuth, model.CheckAdminRole, renderSnapshotAttributeView)
 	ginServer.Handle("POST", "/api/av/getAttributeViewKeys", model.CheckAuth, getAttributeViewKeys)
-	ginServer.Handle("POST", "/api/av/setAttributeViewBlockAttr", model.CheckAuth, model.CheckReadonly, setAttributeViewBlockAttr)
+	ginServer.Handle("POST", "/api/av/setAttributeViewBlockAttr", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAttributeViewBlockAttr)
 	ginServer.Handle("POST", "/api/av/searchAttributeView", model.CheckAuth, model.CheckReadonly, searchAttributeView)
 	ginServer.Handle("POST", "/api/av/getAttributeView", model.CheckAuth, model.CheckReadonly, getAttributeView)
-	ginServer.Handle("POST", "/api/av/searchAttributeViewRelationKey", model.CheckAuth, model.CheckReadonly, searchAttributeViewRelationKey)
-	ginServer.Handle("POST", "/api/av/searchAttributeViewNonRelationKey", model.CheckAuth, model.CheckReadonly, searchAttributeViewNonRelationKey)
-	ginServer.Handle("POST", "/api/av/getAttributeViewFilterSort", model.CheckAuth, model.CheckReadonly, getAttributeViewFilterSort)
-	ginServer.Handle("POST", "/api/av/addAttributeViewKey", model.CheckAuth, model.CheckReadonly, addAttributeViewKey)
-	ginServer.Handle("POST", "/api/av/removeAttributeViewKey", model.CheckAuth, model.CheckReadonly, removeAttributeViewKey)
-	ginServer.Handle("POST", "/api/av/sortAttributeViewViewKey", model.CheckAuth, model.CheckReadonly, sortAttributeViewViewKey)
-	ginServer.Handle("POST", "/api/av/sortAttributeViewKey", model.CheckAuth, model.CheckReadonly, sortAttributeViewKey)
-	ginServer.Handle("POST", "/api/av/addAttributeViewBlocks", model.CheckAuth, model.CheckReadonly, addAttributeViewBlocks)
-	ginServer.Handle("POST", "/api/av/removeAttributeViewBlocks", model.CheckAuth, model.CheckReadonly, removeAttributeViewBlocks)
-	ginServer.Handle("POST", "/api/av/getAttributeViewPrimaryKeyValues", model.CheckAuth, model.CheckReadonly, getAttributeViewPrimaryKeyValues)
-	ginServer.Handle("POST", "/api/av/setDatabaseBlockView", model.CheckAuth, model.CheckReadonly, setDatabaseBlockView)
-	ginServer.Handle("POST", "/api/av/getMirrorDatabaseBlocks", model.CheckAuth, model.CheckReadonly, getMirrorDatabaseBlocks)
-	ginServer.Handle("POST", "/api/av/getAttributeViewKeysByAvID", model.CheckAuth, model.CheckReadonly, getAttributeViewKeysByAvID)
-	ginServer.Handle("POST", "/api/av/duplicateAttributeViewBlock", model.CheckAuth, model.CheckReadonly, duplicateAttributeViewBlock)
-	ginServer.Handle("POST", "/api/av/appendAttributeViewDetachedBlocksWithValues", model.CheckAuth, model.CheckReadonly, appendAttributeViewDetachedBlocksWithValues)
-
-	ginServer.Handle("POST", "/api/ai/chatGPT", model.CheckAuth, chatGPT)
-	ginServer.Handle("POST", "/api/ai/chatGPTWithAction", model.CheckAuth, chatGPTWithAction)
+	ginServer.Handle("POST", "/api/av/searchAttributeViewRelationKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, searchAttributeViewRelationKey)
+	ginServer.Handle("POST", "/api/av/searchAttributeViewNonRelationKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, searchAttributeViewNonRelationKey)
+	ginServer.Handle("POST", "/api/av/getAttributeViewFilterSort", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getAttributeViewFilterSort)
+	ginServer.Handle("POST", "/api/av/addAttributeViewKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, addAttributeViewKey)
+	ginServer.Handle("POST", "/api/av/removeAttributeViewKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeAttributeViewKey)
+	ginServer.Handle("POST", "/api/av/sortAttributeViewViewKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, sortAttributeViewViewKey)
+	ginServer.Handle("POST", "/api/av/sortAttributeViewKey", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, sortAttributeViewKey)
+	ginServer.Handle("POST", "/api/av/addAttributeViewBlocks", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, addAttributeViewBlocks)
+	ginServer.Handle("POST", "/api/av/removeAttributeViewBlocks", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeAttributeViewBlocks)
+	ginServer.Handle("POST", "/api/av/getAttributeViewPrimaryKeyValues", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getAttributeViewPrimaryKeyValues)
+	ginServer.Handle("POST", "/api/av/setDatabaseBlockView", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setDatabaseBlockView)
+	ginServer.Handle("POST", "/api/av/getMirrorDatabaseBlocks", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getMirrorDatabaseBlocks)
+	ginServer.Handle("POST", "/api/av/getAttributeViewKeysByAvID", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, getAttributeViewKeysByAvID)
+	ginServer.Handle("POST", "/api/av/duplicateAttributeViewBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, duplicateAttributeViewBlock)
+	ginServer.Handle("POST", "/api/av/appendAttributeViewDetachedBlocksWithValues", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, appendAttributeViewDetachedBlocksWithValues)
+
+	ginServer.Handle("POST", "/api/ai/chatGPT", model.CheckAuth, model.CheckAdminRole, chatGPT)
+	ginServer.Handle("POST", "/api/ai/chatGPTWithAction", model.CheckAuth, model.CheckAdminRole, chatGPTWithAction)
 
 	ginServer.Handle("POST", "/api/petal/loadPetals", model.CheckAuth, loadPetals)
-	ginServer.Handle("POST", "/api/petal/setPetalEnabled", model.CheckAuth, model.CheckReadonly, setPetalEnabled)
+	ginServer.Handle("POST", "/api/petal/setPetalEnabled", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setPetalEnabled)
 
-	ginServer.Any("/api/network/echo", model.CheckAuth, echo)
-	ginServer.Handle("POST", "/api/network/forwardProxy", model.CheckAuth, forwardProxy)
+	ginServer.Any("/api/network/echo", model.CheckAuth, model.CheckAdminRole, echo)
+	ginServer.Handle("POST", "/api/network/forwardProxy", model.CheckAuth, model.CheckAdminRole, forwardProxy)
 
-	ginServer.Handle("GET", "/ws/broadcast", model.CheckAuth, broadcast)
-	ginServer.Handle("POST", "/api/broadcast/postMessage", model.CheckAuth, postMessage)
-	ginServer.Handle("POST", "/api/broadcast/getChannels", model.CheckAuth, getChannels)
-	ginServer.Handle("POST", "/api/broadcast/getChannelInfo", model.CheckAuth, getChannelInfo)
+	ginServer.Handle("GET", "/ws/broadcast", model.CheckAuth, model.CheckAdminRole, broadcast)
+	ginServer.Handle("POST", "/api/broadcast/postMessage", model.CheckAuth, model.CheckAdminRole, postMessage)
+	ginServer.Handle("POST", "/api/broadcast/getChannels", model.CheckAuth, model.CheckAdminRole, getChannels)
+	ginServer.Handle("POST", "/api/broadcast/getChannelInfo", model.CheckAuth, model.CheckAdminRole, getChannelInfo)
 
-	ginServer.Handle("POST", "/api/archive/zip", model.CheckAuth, model.CheckReadonly, zip)
-	ginServer.Handle("POST", "/api/archive/unzip", model.CheckAuth, model.CheckReadonly, unzip)
+	ginServer.Handle("POST", "/api/archive/zip", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, zip)
+	ginServer.Handle("POST", "/api/archive/unzip", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, unzip)
 }

+ 53 - 0
kernel/api/setting.go

@@ -25,6 +25,7 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/siyuan-note/siyuan/kernel/conf"
 	"github.com/siyuan-note/siyuan/kernel/model"
+	"github.com/siyuan-note/siyuan/kernel/server/proxy"
 	"github.com/siyuan-note/siyuan/kernel/sql"
 	"github.com/siyuan-note/siyuan/kernel/util"
 )
@@ -533,6 +534,58 @@ func setAppearance(c *gin.Context) {
 	ret.Data = model.Conf.Appearance
 }
 
+func setPublish(c *gin.Context) {
+	ret := gulu.Ret.NewResult()
+	defer c.JSON(http.StatusOK, ret)
+
+	arg, ok := util.JsonArg(c, ret)
+	if !ok {
+		return
+	}
+
+	param, err := gulu.JSON.MarshalJSON(arg)
+	if nil != err {
+		ret.Code = -1
+		ret.Msg = err.Error()
+		return
+	}
+
+	publish := &conf.Publish{}
+	if err = gulu.JSON.UnmarshalJSON(param, publish); nil != err {
+		ret.Code = -1
+		ret.Msg = err.Error()
+		return
+	}
+
+	model.Conf.Publish = publish
+	model.Conf.Save()
+
+	if port, err := proxy.InitPublishService(); err != nil {
+		ret.Code = -1
+		ret.Msg = err.Error()
+	} else {
+		ret.Data = map[string]any{
+			"port":    port,
+			"publish": model.Conf.Publish,
+		}
+	}
+}
+
+func getPublish(c *gin.Context) {
+	ret := gulu.Ret.NewResult()
+	defer c.JSON(http.StatusOK, ret)
+
+	if port, err := proxy.InitPublishService(); err != nil {
+		ret.Code = -1
+		ret.Msg = err.Error()
+	} else {
+		ret.Data = map[string]any{
+			"port":    port,
+			"publish": model.Conf.Publish,
+		}
+	}
+}
+
 func getCloudUser(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)

+ 0 - 27
kernel/api/snippet.go

@@ -17,44 +17,17 @@
 package api
 
 import (
-	"mime"
 	"net/http"
-	"path/filepath"
 	"strings"
 
 	"github.com/88250/gulu"
 	"github.com/88250/lute/ast"
 	"github.com/gin-gonic/gin"
-	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/siyuan/kernel/conf"
 	"github.com/siyuan-note/siyuan/kernel/model"
 	"github.com/siyuan-note/siyuan/kernel/util"
 )
 
-func serveSnippets(c *gin.Context) {
-	filePath := strings.TrimPrefix(c.Request.URL.Path, "/snippets/")
-	ext := filepath.Ext(filePath)
-	name := strings.TrimSuffix(filePath, ext)
-	confSnippets, err := model.LoadSnippets()
-	if nil != err {
-		logging.LogErrorf("load snippets failed: %s", err)
-		c.Status(404)
-		return
-	}
-
-	for _, s := range confSnippets {
-		if s.Name == name && ("" != ext && s.Type == ext[1:]) {
-			c.Header("Content-Type", mime.TypeByExtension(ext))
-			c.String(http.StatusOK, s.Content)
-			return
-		}
-	}
-
-	// 没有在配置文件中命中时在文件系统上查找
-	filePath = filepath.Join(util.SnippetsPath, filePath)
-	c.File(filePath)
-}
-
 func getSnippet(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)

+ 13 - 1
kernel/api/system.go

@@ -17,7 +17,6 @@
 package api
 
 import (
-	"github.com/88250/lute"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -25,6 +24,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/88250/lute"
+
 	"github.com/88250/gulu"
 	"github.com/gin-gonic/gin"
 	"github.com/siyuan-note/logging"
@@ -218,6 +219,17 @@ func getConf(c *gin.Context) {
 		maskedConf.Sync.Stat = model.Conf.Language(53)
 	}
 
+	// REF: https://github.com/siyuan-note/siyuan/issues/11364
+	role := model.GetGinContextRole(c)
+	if model.IsReadOnlyRole(role) {
+		maskedConf.ReadOnly = true
+	}
+	if !model.IsValidRole(role, []model.Role{
+		model.RoleAdministrator,
+	}) {
+		model.HideConfSecret(maskedConf)
+	}
+
 	ret.Data = map[string]interface{}{
 		"conf":  maskedConf,
 		"start": !util.IsUILoaded,

+ 45 - 0
kernel/conf/publish.go

@@ -0,0 +1,45 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package conf
+
+type Publish struct {
+	Enable bool       `json:"enable"` // 是否启用发布服务
+	Port   uint16     `json:"port"`   // 发布服务端口
+	Auth   *BasicAuth `json:"auth"`   // Basic 认证
+}
+
+type BasicAuth struct {
+	Enable   bool                `json:"enable"`   // 是否启用基础认证
+	Accounts []*BasicAuthAccount `json:"accounts"` // 账户列表
+}
+
+type BasicAuthAccount struct {
+	Username string `json:"username"` // 用户名
+	Password string `json:"password"` // 密码
+	Memo     string `json:"memo"`     // 备注
+}
+
+func NewPublish() *Publish {
+	return &Publish{
+		Enable: false,
+		Port:   6808,
+		Auth: &BasicAuth{
+			Enable:   true,
+			Accounts: []*BasicAuthAccount{},
+		},
+	}
+}

+ 1 - 0
kernel/go.mod

@@ -33,6 +33,7 @@ require (
 	github.com/go-ole/go-ole v1.3.0
 	github.com/goccy/go-json v0.10.3
 	github.com/gofrs/flock v0.8.1
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/gorilla/css v1.0.1
 	github.com/gorilla/websocket v1.5.1
 	github.com/imroc/req/v3 v3.43.7

+ 2 - 0
kernel/go.sum

@@ -154,6 +154,8 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
 github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

+ 133 - 0
kernel/model/auth.go

@@ -0,0 +1,133 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package model
+
+import (
+	"crypto/rand"
+	"net/http"
+
+	"github.com/golang-jwt/jwt/v5"
+	"github.com/siyuan-note/logging"
+)
+
+type Account struct {
+	Username string
+	Password string
+	Token    string
+}
+type AccountsMap map[string]*Account
+type ClaimsKeyType string
+
+const (
+	XAuthTokenKey = "X-Auth-Token"
+
+	ClaimsContextKey = "claims"
+
+	iss = "siyuan-publish-reverse-proxy-server"
+	sub = "publish"
+	aud = "siyuan-kernel"
+
+	ClaimsKeyRole string = "role"
+)
+
+var (
+	accountsMap = AccountsMap{}
+
+	key = make([]byte, 32)
+)
+
+func GetBasicAuthAccount(username string) *Account {
+	return accountsMap[username]
+}
+
+func InitAccounts() {
+	accountsMap = AccountsMap{
+		"": &Account{}, // 匿名用户
+	}
+	for _, account := range Conf.Publish.Auth.Accounts {
+		accountsMap[account.Username] = &Account{
+			Username: account.Username,
+			Password: account.Password,
+		}
+	}
+
+	InitJWT()
+}
+
+func InitJWT() {
+	if _, err := rand.Read(key); err != nil {
+		logging.LogErrorf("generate JWT signing key failed: %s", err)
+		return
+	}
+
+	for username, account := range accountsMap {
+		// REF: https://golang-jwt.github.io/jwt/usage/create/
+		t := jwt.NewWithClaims(
+			jwt.SigningMethodHS256,
+			jwt.MapClaims{
+				"iss": iss,
+				"sub": sub,
+				"aud": aud,
+				"jti": username,
+
+				ClaimsKeyRole: RoleReader,
+			},
+		)
+		if token, err := t.SignedString(key); err != nil {
+			logging.LogErrorf("JWT signature failed: %s", err)
+			return
+		} else {
+			account.Token = token
+		}
+	}
+}
+
+func ParseJWT(tokenString string) (*jwt.Token, error) {
+	// REF: https://golang-jwt.github.io/jwt/usage/parse/
+	return jwt.Parse(
+		tokenString,
+		func(token *jwt.Token) (interface{}, error) {
+			return key, nil
+		},
+		jwt.WithIssuer(iss),
+		jwt.WithSubject(sub),
+		jwt.WithAudience(aud),
+	)
+}
+
+func ParseXAuthToken(r *http.Request) *jwt.Token {
+	tokenString := r.Header.Get(XAuthTokenKey)
+	if tokenString != "" {
+		if token, err := ParseJWT(tokenString); err != nil {
+			logging.LogErrorf("JWT parse failed: %s", err)
+		} else {
+			return token
+		}
+	}
+	return nil
+}
+
+func GetTokenClaims(token *jwt.Token) jwt.MapClaims {
+	return token.Claims.(jwt.MapClaims)
+}
+
+func GetClaimRole(claims jwt.MapClaims) Role {
+	if role := claims[ClaimsKeyRole]; role != nil {
+		return Role(role.(float64))
+	}
+	return RoleVisitor
+}

+ 23 - 0
kernel/model/conf.go

@@ -76,6 +76,7 @@ type AppConf struct {
 	Stat           *conf.Stat       `json:"stat"`           // 统计
 	Api            *conf.API        `json:"api"`            // API
 	Repo           *conf.Repo       `json:"repo"`           // 数据仓库
+	Publish        *conf.Publish    `json:"publish"`        // 发布服务
 	OpenHelp       bool             `json:"openHelp"`       // 启动后是否需要打开用户指南
 	ShowChangelog  bool             `json:"showChangelog"`  // 是否显示版本更新日志
 	CloudRegion    int              `json:"cloudRegion"`    // 云端区域,0:中国大陆,1:北美
@@ -357,6 +358,10 @@ func InitConf() {
 		Conf.Bazaar = conf.NewBazaar()
 	}
 
+	if nil == Conf.Publish {
+		Conf.Publish = conf.NewPublish()
+	}
+
 	if nil == Conf.Repo {
 		Conf.Repo = conf.NewRepo()
 	}
@@ -894,6 +899,24 @@ func GetMaskedConf() (ret *AppConf, err error) {
 	return
 }
 
+// REF: https://github.com/siyuan-note/siyuan/issues/11364
+// HideConfSecret 隐藏设置中的秘密信息
+func HideConfSecret(c *AppConf) {
+	c.AI = &conf.AI{}
+	c.Api = &conf.API{}
+	c.Flashcard = &conf.Flashcard{}
+	c.LocalIPs = []string{}
+	c.Publish = &conf.Publish{}
+	c.Repo = &conf.Repo{}
+	c.Sync = &conf.Sync{}
+	c.System.AppDir = ""
+	c.System.ConfDir = ""
+	c.System.DataDir = ""
+	c.System.HomeDir = ""
+	c.System.Name = ""
+	c.System.NetworkProxy = &conf.NetworkProxy{}
+}
+
 func clearPortJSON() {
 	pid := fmt.Sprintf("%d", os.Getpid())
 	portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json")

+ 56 - 0
kernel/model/role.go

@@ -0,0 +1,56 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package model
+
+import "github.com/gin-gonic/gin"
+
+type Role uint
+
+const (
+	RoleContextKey = "role"
+)
+
+const (
+	RoleAdministrator Role = iota // 管理员
+	RoleEditor                    // 编辑者
+	RoleReader                    // 读者
+	RoleVisitor                   // 匿名访问者
+)
+
+func IsValidRole(role Role, roles []Role) bool {
+	for _, role_ := range roles {
+		if role == role_ {
+			return true
+		}
+	}
+	return false
+}
+
+func IsReadOnlyRole(role Role) bool {
+	return IsValidRole(role, []Role{
+		RoleReader,
+		RoleVisitor,
+	})
+}
+
+func GetGinContextRole(c *gin.Context) Role {
+	if role, exists := c.Get(RoleContextKey); exists {
+		return role.(Role)
+	} else {
+		return RoleVisitor
+	}
+}

+ 53 - 0
kernel/model/session.go

@@ -164,6 +164,16 @@ func CheckReadonly(c *gin.Context) {
 }
 
 func CheckAuth(c *gin.Context) {
+	// 已通过 JWT 认证
+	if role := GetGinContextRole(c); IsValidRole(role, []Role{
+		RoleAdministrator,
+		RoleEditor,
+		RoleReader,
+	}) {
+		c.Next()
+		return
+	}
+
 	//logging.LogInfof("check auth for [%s]", c.Request.RequestURI)
 	localhost := util.IsLocalHost(c.Request.RemoteAddr)
 
@@ -171,6 +181,7 @@ func CheckAuth(c *gin.Context) {
 	if "" == Conf.AccessAuthCode {
 		// Skip the empty access authorization code check https://github.com/siyuan-note/siyuan/issues/9709
 		if util.SiyuanAccessAuthCodeBypass {
+			c.Set(RoleContextKey, RoleAdministrator)
 			c.Next()
 			return
 		}
@@ -190,6 +201,7 @@ func CheckAuth(c *gin.Context) {
 			return
 		}
 
+		c.Set(RoleContextKey, RoleAdministrator)
 		c.Next()
 		return
 	}
@@ -206,19 +218,23 @@ func CheckAuth(c *gin.Context) {
 	// 放过来自本机的某些请求
 	if localhost {
 		if strings.HasPrefix(c.Request.RequestURI, "/assets/") {
+			c.Set(RoleContextKey, RoleAdministrator)
 			c.Next()
 			return
 		}
 		if strings.HasPrefix(c.Request.RequestURI, "/api/system/exit") {
+			c.Set(RoleContextKey, RoleAdministrator)
 			c.Next()
 			return
 		}
 		if strings.HasPrefix(c.Request.RequestURI, "/api/system/getNetwork") {
+			c.Set(RoleContextKey, RoleAdministrator)
 			c.Next()
 			return
 		}
 		if strings.HasPrefix(c.Request.RequestURI, "/api/sync/performSync") {
 			if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container {
+				c.Set(RoleContextKey, RoleAdministrator)
 				c.Next()
 				return
 			}
@@ -229,6 +245,7 @@ func CheckAuth(c *gin.Context) {
 	session := util.GetSession(c)
 	workspaceSession := util.GetWorkspaceSession(session)
 	if workspaceSession.AccessAuthCode == Conf.AccessAuthCode {
+		c.Set(RoleContextKey, RoleAdministrator)
 		c.Next()
 		return
 	}
@@ -248,6 +265,7 @@ func CheckAuth(c *gin.Context) {
 
 		if "" != token {
 			if Conf.Api.Token == token {
+				c.Set(RoleContextKey, RoleAdministrator)
 				c.Next()
 				return
 			}
@@ -261,6 +279,7 @@ func CheckAuth(c *gin.Context) {
 	// 通过 API token (query-params: token)
 	if token := c.Query("token"); "" != token {
 		if Conf.Api.Token == token {
+			c.Set(RoleContextKey, RoleAdministrator)
 			c.Next()
 			return
 		}
@@ -300,9 +319,43 @@ func CheckAuth(c *gin.Context) {
 		return
 	}
 
+	c.Set(RoleContextKey, RoleAdministrator)
 	c.Next()
 }
 
+func CheckAdminRole(c *gin.Context) {
+	if IsValidRole(GetGinContextRole(c), []Role{
+		RoleAdministrator,
+	}) {
+		c.Next()
+	} else {
+		c.AbortWithStatus(http.StatusForbidden)
+	}
+}
+
+func CheckEditRole(c *gin.Context) {
+	if IsValidRole(GetGinContextRole(c), []Role{
+		RoleAdministrator,
+		RoleEditor,
+	}) {
+		c.Next()
+	} else {
+		c.AbortWithStatus(http.StatusForbidden)
+	}
+}
+
+func CheckReadRole(c *gin.Context) {
+	if IsValidRole(GetGinContextRole(c), []Role{
+		RoleAdministrator,
+		RoleEditor,
+		RoleReader,
+	}) {
+		c.Next()
+	} else {
+		c.AbortWithStatus(http.StatusForbidden)
+	}
+}
+
 var timingAPIs = map[string]int{
 	"/api/search/fullTextSearchBlock": 200, // Monitor the search performance and suggest solutions https://github.com/siyuan-note/siyuan/issues/7873
 }

+ 1 - 15
kernel/server/port.go

@@ -18,7 +18,6 @@ package server
 
 import (
 	"fmt"
-	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -65,7 +64,7 @@ func killRunningKernel() {
 }
 
 func killByPort(port string) {
-	if !isPortOpen(port) {
+	if !util.IsPortOpen(port) {
 		return
 	}
 
@@ -87,19 +86,6 @@ func killByPort(port string) {
 	logging.LogInfof("killed process [name=%s, pid=%s]", name, pid)
 }
 
-func isPortOpen(port string) bool {
-	timeout := time.Second
-	conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", port), timeout)
-	if nil != err {
-		return false
-	}
-	if nil != conn {
-		conn.Close()
-		return true
-	}
-	return false
-}
-
 func kill(pid string) {
 	var killCmd *exec.Cmd
 	if gulu.OS.IsWindows() {

+ 41 - 0
kernel/server/proxy/fixedport.go

@@ -0,0 +1,41 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package proxy
+
+import (
+	"net/http"
+	"net/http/httputil"
+
+	"github.com/siyuan-note/logging"
+	"github.com/siyuan-note/siyuan/kernel/util"
+)
+
+func InitFixedPortService(host string) {
+	if util.FixedPort != util.ServerPort {
+		if util.IsPortOpen(util.FixedPort) {
+			return
+		}
+
+		// 启动一个固定 6806 端口的反向代理服务器,这样浏览器扩展才能直接使用 127.0.0.1:6806,不用配置端口
+		proxy := httputil.NewSingleHostReverseProxy(util.ServerURL)
+		logging.LogInfof("fixed port service [%s:%s] is running", host, util.FixedPort)
+		if proxyErr := http.ListenAndServe(host+":"+util.FixedPort, proxy); nil != proxyErr {
+			logging.LogWarnf("boot fixed port service [%s] failed: %s", util.ServerURL, proxyErr)
+		}
+		logging.LogInfof("fixed port service [%s:%s] is stopped", host, util.FixedPort)
+	}
+}

+ 161 - 0
kernel/server/proxy/publish.go

@@ -0,0 +1,161 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"net/http/httputil"
+	"strconv"
+
+	"github.com/siyuan-note/logging"
+	"github.com/siyuan-note/siyuan/kernel/model"
+	"github.com/siyuan-note/siyuan/kernel/util"
+)
+
+type PublishServiceTransport struct{}
+
+var (
+	Host = "0.0.0.0"
+	Port = "0"
+
+	listener  net.Listener
+	transport = PublishServiceTransport{}
+	proxy     = &httputil.ReverseProxy{
+		Rewrite:   rewrite,
+		Transport: transport,
+	}
+)
+
+func InitPublishService() (uint16, error) {
+	model.InitAccounts()
+
+	if listener != nil {
+		if !model.Conf.Publish.Enable {
+			// 关闭发布服务
+			closePublishListener()
+			return 0, nil
+		}
+
+		if port, err := util.ParsePort(Port); err != nil {
+			return 0, err
+		} else if port != model.Conf.Publish.Port {
+			// 关闭原端口的发布服务
+			if err = closePublishListener(); err != nil {
+				return 0, err
+			}
+
+			// 重新启动新端口的发布服务
+			initPublishService()
+		}
+	} else {
+		if !model.Conf.Publish.Enable {
+			return 0, nil
+		}
+
+		// 启动新端口的发布服务
+		initPublishService()
+	}
+	return util.ParsePort(Port)
+}
+
+func initPublishService() (err error) {
+	if err = initPublishListener(); err == nil {
+		go startPublishReverseProxyService()
+	}
+	return
+}
+
+func initPublishListener() (err error) {
+	// Start new listener
+	listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", Host, model.Conf.Publish.Port))
+	if err != nil {
+		logging.LogErrorf("start listener failed: %s", err)
+		return
+	}
+
+	_, Port, err = net.SplitHostPort(listener.Addr().String())
+	if nil != err {
+		logging.LogErrorf("split host and port failed: %s", err)
+		return
+	}
+	return
+}
+
+func closePublishListener() (err error) {
+	listener_ := listener
+	listener = nil
+	if err = listener_.Close(); err != nil {
+		logging.LogErrorf("close listener %s failed: %s", listener_.Addr().String(), err)
+		listener = listener_
+	}
+	return
+}
+
+func startPublishReverseProxyService() {
+	logging.LogInfof("publish service [%s:%s] is running", Host, Port)
+	// 服务进行时一直阻塞
+	if err := http.Serve(listener, proxy); nil != err {
+		if listener != nil {
+			logging.LogErrorf("boot publish service failed: %s", err)
+		}
+	}
+	logging.LogInfof("publish service [%s:%s] is stopped", Host, Port)
+}
+
+func rewrite(r *httputil.ProxyRequest) {
+	r.SetURL(util.ServerURL)
+	r.SetXForwarded()
+	// r.Out.Host = r.In.Host // if desired
+}
+
+func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http.Response, err error) {
+	if model.Conf.Publish.Auth.Enable {
+		// Basic Auth
+		username, password, ok := request.BasicAuth()
+		account := model.GetBasicAuthAccount(username)
+
+		if !ok ||
+			account == nil ||
+			account.Username == "" || // 匿名用户
+			account.Password != password {
+
+			return &http.Response{
+				StatusCode: http.StatusUnauthorized,
+				Status:     http.StatusText(http.StatusUnauthorized),
+				Proto:      request.Proto,
+				ProtoMajor: request.ProtoMajor,
+				ProtoMinor: request.ProtoMinor,
+				Request:    request,
+				Header: http.Header{
+					"WWW-Authenticate": {"Basic realm=" + strconv.Quote("Authorization Required")},
+				},
+				Close:         false,
+				ContentLength: -1,
+			}, nil
+		} else {
+			// set JWT
+			request.Header.Set(model.XAuthTokenKey, account.Token)
+		}
+	} else {
+		request.Header.Set(model.XAuthTokenKey, model.GetBasicAuthAccount("").Token)
+	}
+
+	response, err = http.DefaultTransport.RoundTrip(request)
+	return
+}

+ 109 - 36
kernel/server/serve.go

@@ -20,9 +20,9 @@ import (
 	"bytes"
 	"fmt"
 	"html/template"
+	"mime"
 	"net"
 	"net/http"
-	"net/http/httputil"
 	"net/http/pprof"
 	"net/url"
 	"os"
@@ -42,10 +42,13 @@ import (
 	"github.com/siyuan-note/siyuan/kernel/api"
 	"github.com/siyuan-note/siyuan/kernel/cmd"
 	"github.com/siyuan-note/siyuan/kernel/model"
+	"github.com/siyuan-note/siyuan/kernel/server/proxy"
 	"github.com/siyuan-note/siyuan/kernel/util"
 )
 
-var cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
+var (
+	cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
+)
 
 func Serve(fastMode bool) {
 	gin.SetMode(gin.ReleaseMode)
@@ -57,6 +60,7 @@ func Serve(fastMode bool) {
 		model.Timing,
 		model.Recover,
 		corsMiddleware(), // 后端服务支持 CORS 预检请求验证 https://github.com/siyuan-note/siyuan/pull/5593
+		jwtMiddleware,    // 解析 JWT https://github.com/siyuan-note/siyuan/issues/11364
 		gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp3", ".wav", ".ogg", ".mov", ".weba", ".mkv", ".mp4", ".webm"})),
 	)
 
@@ -78,7 +82,10 @@ func Serve(fastMode bool) {
 	serveEmojis(ginServer)
 	serveTemplates(ginServer)
 	servePublic(ginServer)
+	serveSnippets(ginServer)
 	serveRepoDiff(ginServer)
+	serveCheckAuth(ginServer)
+	serveFixedStaticFiles(ginServer)
 	api.ServeAPI(ginServer)
 
 	var host string
@@ -108,34 +115,27 @@ func Serve(fastMode bool) {
 	}
 	util.ServerPort = port
 
+	util.ServerURL, err = url.Parse("http://127.0.0.1:" + port)
+	if err != nil {
+		logging.LogErrorf("parse server url failed: %s", err)
+	}
+
 	pid := fmt.Sprintf("%d", os.Getpid())
 	if !fastMode {
 		rewritePortJSON(pid, port)
 	}
-
 	logging.LogInfof("kernel [pid=%s] http server [%s] is booting", pid, host+":"+port)
 	util.HttpServing = true
 
+	go util.HookUILoaded()
+
 	go func() {
 		time.Sleep(1 * time.Second)
-		if util.FixedPort != port {
-			if isPortOpen(util.FixedPort) {
-				return
-			}
-
-			// 启动一个 6806 端口的反向代理服务器,这样浏览器扩展才能直接使用 127.0.0.1:6806,不用配置端口
-			serverURL, _ := url.Parse("http://127.0.0.1:" + port)
-			proxy := httputil.NewSingleHostReverseProxy(serverURL)
-			logging.LogInfof("reverse proxy server [%s] is booting", host+":"+util.FixedPort)
-			if proxyErr := http.ListenAndServe(host+":"+util.FixedPort, proxy); nil != proxyErr {
-				logging.LogWarnf("boot reverse proxy server [%s] failed: %s", serverURL, proxyErr)
-			}
-			// 反代服务器启动失败不影响核心服务器启动
-		}
+		go proxy.InitFixedPortService(host)
+		go proxy.InitPublishService()
+		// 反代服务器启动失败不影响核心服务器启动
 	}()
 
-	go util.HookUILoaded()
-
 	if err = http.Serve(ln, ginServer.Handler()); nil != err {
 		if !fastMode {
 			logging.LogErrorf("boot kernel failed: %s", err)
@@ -196,11 +196,33 @@ func servePublic(ginServer *gin.Engine) {
 	ginServer.Static("/public/", filepath.Join(util.DataDir, "public"))
 }
 
-func serveAppearance(ginServer *gin.Engine) {
-	ginServer.StaticFile("favicon.ico", filepath.Join(util.WorkingDir, "stage", "icon.png"))
-	ginServer.StaticFile("manifest.json", filepath.Join(util.WorkingDir, "stage", "manifest.webmanifest"))
-	ginServer.StaticFile("manifest.webmanifest", filepath.Join(util.WorkingDir, "stage", "manifest.webmanifest"))
+func serveSnippets(ginServer *gin.Engine) {
+	ginServer.Handle("GET", "/snippets/*filepath", func(c *gin.Context) {
+		filePath := strings.TrimPrefix(c.Request.URL.Path, "/snippets/")
+		ext := filepath.Ext(filePath)
+		name := strings.TrimSuffix(filePath, ext)
+		confSnippets, err := model.LoadSnippets()
+		if nil != err {
+			logging.LogErrorf("load snippets failed: %s", err)
+			c.Status(http.StatusNotFound)
+			return
+		}
+
+		for _, s := range confSnippets {
+			if s.Name == name && ("" != ext && s.Type == ext[1:]) {
+				c.Header("Content-Type", mime.TypeByExtension(ext))
+				c.String(http.StatusOK, s.Content)
+				return
+			}
+		}
 
+		// 没有在配置文件中命中时在文件系统上查找
+		filePath = filepath.Join(util.SnippetsPath, filePath)
+		c.File(filePath)
+	})
+}
+
+func serveAppearance(ginServer *gin.Engine) {
 	siyuan := ginServer.Group("", model.CheckAuth)
 
 	siyuan.Handle("GET", "/", func(c *gin.Context) {
@@ -295,12 +317,13 @@ func serveAppearance(ginServer *gin.Engine) {
 	})
 
 	siyuan.Static("/stage/", filepath.Join(util.WorkingDir, "stage"))
-	ginServer.StaticFile("service-worker.js", filepath.Join(util.WorkingDir, "stage", "service-worker.js"))
+}
 
-	siyuan.GET("/check-auth", serveCheckAuth)
+func serveCheckAuth(ginServer *gin.Engine) {
+	ginServer.GET("/check-auth", serveAuthPage)
 }
 
-func serveCheckAuth(c *gin.Context) {
+func serveAuthPage(c *gin.Context) {
 	data, err := os.ReadFile(filepath.Join(util.WorkingDir, "stage/auth.html"))
 	if nil != err {
 		logging.LogErrorf("load auth page failed: %s", err)
@@ -363,20 +386,20 @@ func serveCheckAuth(c *gin.Context) {
 }
 
 func serveAssets(ginServer *gin.Engine) {
-	ginServer.POST("/upload", model.CheckAuth, model.Upload)
+	ginServer.POST("/upload", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, model.Upload)
 
 	ginServer.GET("/assets/*path", model.CheckAuth, func(context *gin.Context) {
 		requestPath := context.Param("path")
 		relativePath := path.Join("assets", requestPath)
 		p, err := model.GetAssetAbsPath(relativePath)
 		if nil != err {
-			context.Status(404)
+			context.Status(http.StatusNotFound)
 			return
 		}
 		http.ServeFile(context.Writer, context.Request, p)
 		return
 	})
-	ginServer.GET("/history/*path", model.CheckAuth, func(context *gin.Context) {
+	ginServer.GET("/history/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {
 		p := filepath.Join(util.HistoryDir, context.Param("path"))
 		http.ServeFile(context.Writer, context.Request, p)
 		return
@@ -384,7 +407,7 @@ func serveAssets(ginServer *gin.Engine) {
 }
 
 func serveRepoDiff(ginServer *gin.Engine) {
-	ginServer.GET("/repo/diff/*path", model.CheckAuth, func(context *gin.Context) {
+	ginServer.GET("/repo/diff/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {
 		requestPath := context.Param("path")
 		p := filepath.Join(util.TempDir, "repo", "diff", requestPath)
 		http.ServeFile(context.Writer, context.Request, p)
@@ -451,6 +474,17 @@ func serveWebSocket(ginServer *gin.Engine) {
 			}
 		}
 
+		// REF: https://github.com/siyuan-note/siyuan/issues/11364
+		if !authOk {
+			if token := model.ParseXAuthToken(s.Request); token != nil {
+				authOk = token.Valid && model.IsValidRole(model.GetClaimRole(model.GetTokenClaims(token)), []model.Role{
+					model.RoleAdministrator,
+					model.RoleEditor,
+					model.RoleReader,
+				})
+			}
+		}
+
 		if !authOk {
 			// 用于授权页保持连接,避免非常驻内存内核自动退出 https://github.com/siyuan-note/insider/issues/1099
 			authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan&id=auth")
@@ -516,12 +550,24 @@ func serveWebSocket(ginServer *gin.Engine) {
 			s.Write(result.Bytes())
 			return
 		}
-		if util.ReadOnly && !command.IsRead() {
-			result := util.NewResult()
-			result.Code = -1
-			result.Msg = model.Conf.Language(34)
-			s.Write(result.Bytes())
-			return
+		if !command.IsRead() {
+			readonly := util.ReadOnly
+			if !readonly {
+				if token := model.ParseXAuthToken(s.Request); token != nil {
+					readonly = token.Valid && model.IsValidRole(model.GetClaimRole(model.GetTokenClaims(token)), []model.Role{
+						model.RoleReader,
+						model.RoleVisitor,
+					})
+				}
+			}
+
+			if readonly {
+				result := util.NewResult()
+				result.Code = -1
+				result.Msg = model.Conf.Language(34)
+				s.Write(result.Bytes())
+				return
+			}
 		}
 
 		end := time.Now()
@@ -564,3 +610,30 @@ func corsMiddleware() gin.HandlerFunc {
 		c.Next()
 	}
 }
+
+// jwtMiddleware is a middleware to check jwt token
+// REF: https://github.com/siyuan-note/siyuan/issues/11364
+func jwtMiddleware(c *gin.Context) {
+	if token := model.ParseXAuthToken(c.Request); token != nil {
+		// c.Request.Header.Del(model.XAuthTokenKey)
+		if token.Valid {
+			claims := model.GetTokenClaims(token)
+			c.Set(model.ClaimsContextKey, claims)
+			c.Set(model.RoleContextKey, model.GetClaimRole(claims))
+			c.Next()
+			return
+		}
+	}
+	c.Set(model.RoleContextKey, model.RoleVisitor)
+	c.Next()
+	return
+}
+
+func serveFixedStaticFiles(ginServer *gin.Engine) {
+	ginServer.StaticFile("favicon.ico", filepath.Join(util.WorkingDir, "stage", "icon.png"))
+
+	ginServer.StaticFile("manifest.json", filepath.Join(util.WorkingDir, "stage", "manifest.webmanifest"))
+	ginServer.StaticFile("manifest.webmanifest", filepath.Join(util.WorkingDir, "stage", "manifest.webmanifest"))
+
+	ginServer.StaticFile("service-worker.js", filepath.Join(util.WorkingDir, "stage", "service-worker.js"))
+}

+ 2 - 1
kernel/sql/asset.go

@@ -18,10 +18,11 @@ package sql
 
 import (
 	"database/sql"
-	"github.com/siyuan-note/filelock"
 	"path/filepath"
 	"strings"
 
+	"github.com/siyuan-note/filelock"
+
 	"github.com/88250/lute/ast"
 	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/siyuan/kernel/treenode"

+ 23 - 0
kernel/util/net.go

@@ -20,6 +20,7 @@ import (
 	"net"
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 	"time"
 
@@ -90,6 +91,19 @@ func IsOnline(checkURL string, skipTlsVerify bool) bool {
 	return false
 }
 
+func IsPortOpen(port string) bool {
+	timeout := time.Second
+	conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", port), timeout)
+	if nil != err {
+		return false
+	}
+	if nil != conn {
+		conn.Close()
+		return true
+	}
+	return false
+}
+
 func isOnline(checkURL string, skipTlsVerify bool) (ret bool) {
 	c := req.C().SetTimeout(3 * time.Second)
 	if skipTlsVerify {
@@ -168,3 +182,12 @@ func initHttpClient() {
 	http.DefaultClient = httpclient.GetCloudFileClient2Min()
 	http.DefaultTransport = httpclient.NewTransport(false)
 }
+
+func ParsePort(portString string) (uint16, error) {
+	if port, err := strconv.ParseUint(portString, 10, 16); err != nil {
+		logging.LogErrorf("parse port [%s] failed: %s", portString, err)
+		return 0, err
+	} else {
+		return uint16(port), nil
+	}
+}

+ 1 - 0
kernel/util/path.go

@@ -269,6 +269,7 @@ func IsDisplayableAsset(p string) bool {
 
 func GetAbsPathInWorkspace(relPath string) (string, error) {
 	absPath := filepath.Join(WorkspaceDir, relPath)
+	absPath = filepath.Clean(absPath)
 	if WorkspaceDir == absPath {
 		return absPath, nil
 	}

+ 4 - 1
kernel/util/working.go

@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"math/rand"
 	"mime"
+	"net/url"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -341,7 +342,9 @@ func WriteWorkspacePaths(workspacePaths []string) (err error) {
 }
 
 var (
-	ServerPort     = "0" // HTTP/WebSocket 端口,0 为使用随机端口
+	ServerURL  *url.URL // 内核服务 URL
+	ServerPort = "0"    // HTTP/WebSocket 端口,0 为使用随机端口
+
 	ReadOnly       bool
 	AccessAuthCode string
 	Lang           = ""