boneIO 1.6.0.dev29__py3-none-any.whl → 1.6.0.dev30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. boneio/core/auth/jwt_secret.py +85 -15
  2. boneio/core/cloud/registration.py +40 -6
  3. boneio/core/nodered_guard.py +141 -0
  4. boneio/migrations/plans/1.6.34.json +1 -0
  5. boneio/migrations/plans/1.6.34.sig +2 -0
  6. boneio/migrations/plans/manifest.json +1 -1
  7. boneio/migrations/plans/manifest.sig +0 -0
  8. boneio/migrations/versions/v1_6_34_nodered_admin_auth_again.py +51 -0
  9. boneio/runner.py +11 -0
  10. boneio/version.py +1 -1
  11. boneio/webui/app.py +10 -1
  12. boneio/webui/frontend-dist/assets/{AccountsView-hSkFhGZn.js → AccountsView-C5bNw-zJ.js} +1 -1
  13. boneio/webui/frontend-dist/assets/AccountsView-C5bNw-zJ.js.gz +0 -0
  14. boneio/webui/frontend-dist/assets/{AppInitContext-XnlT3sSY.js → AppInitContext-D7KZMwlj.js} +1 -1
  15. boneio/webui/frontend-dist/assets/AppInitContext-D7KZMwlj.js.gz +0 -0
  16. boneio/webui/frontend-dist/assets/{BackupSection-CdrNqwc2.js → BackupSection-2ysBmrB6.js} +1 -1
  17. boneio/webui/frontend-dist/assets/BackupSection-2ysBmrB6.js.gz +0 -0
  18. boneio/webui/frontend-dist/assets/{BindingMatrix-Dyyc2Plw.js → BindingMatrix-DU8StgbR.js} +1 -1
  19. boneio/webui/frontend-dist/assets/BindingMatrix-DU8StgbR.js.gz +0 -0
  20. boneio/webui/frontend-dist/assets/{DeviceControlSection-BI2Gx_ht.js → DeviceControlSection-Bg_lorqf.js} +1 -1
  21. boneio/webui/frontend-dist/assets/DeviceControlSection-Bg_lorqf.js.gz +0 -0
  22. boneio/webui/frontend-dist/assets/{DiagnosticsView-DVFMNJGF.js → DiagnosticsView-Crd07ZtC.js} +1 -1
  23. boneio/webui/frontend-dist/assets/{DiagnosticsView-DVFMNJGF.js.gz → DiagnosticsView-Crd07ZtC.js.gz} +0 -0
  24. boneio/webui/frontend-dist/assets/{EntityGrid-D1gJhSPK.js → EntityGrid-C1NLtpsV.js} +1 -1
  25. boneio/webui/frontend-dist/assets/EntityGrid-C1NLtpsV.js.gz +0 -0
  26. boneio/webui/frontend-dist/assets/{FactoryResetSection-B6zeFfLF.js → FactoryResetSection-Ba0cts2m.js} +1 -1
  27. boneio/webui/frontend-dist/assets/FactoryResetSection-Ba0cts2m.js.gz +0 -0
  28. boneio/webui/frontend-dist/assets/{FormInputToggle-B9vctUNt.js → FormInputToggle-B9f9N5Uz.js} +1 -1
  29. boneio/webui/frontend-dist/assets/FormInputToggle-B9f9N5Uz.js.gz +0 -0
  30. boneio/webui/frontend-dist/assets/{HaDashboardWizard-BtouQnje.js → HaDashboardWizard-CotGhViV.js} +1 -1
  31. boneio/webui/frontend-dist/assets/HaDashboardWizard-CotGhViV.js.gz +0 -0
  32. boneio/webui/frontend-dist/assets/{HostnameSection-h9UyWAS0.js → HostnameSection-CifdFFk0.js} +1 -1
  33. boneio/webui/frontend-dist/assets/HostnameSection-CifdFFk0.js.gz +0 -0
  34. boneio/webui/frontend-dist/assets/{InputsView-DHMMyEtN.js → InputsView-CoQNyLSo.js} +1 -1
  35. boneio/webui/frontend-dist/assets/InputsView-CoQNyLSo.js.gz +0 -0
  36. boneio/webui/frontend-dist/assets/{LoadingState-BnMjvLnH.js → LoadingState-hl_hkPPN.js} +1 -1
  37. boneio/webui/frontend-dist/assets/{LocationSection-BWJEPzOe.js → LocationSection-CoI4mDq2.js} +2 -2
  38. boneio/webui/frontend-dist/assets/LocationSection-CoI4mDq2.js.gz +0 -0
  39. boneio/webui/frontend-dist/assets/{LoggerForm-B4wgrOCg.js → LoggerForm-YsXzk4kc.js} +1 -1
  40. boneio/webui/frontend-dist/assets/LoggerForm-YsXzk4kc.js.gz +0 -0
  41. boneio/webui/frontend-dist/assets/{LongPressWrapper-BuYVn91a.js → LongPressWrapper-DBJJqzCe.js} +1 -1
  42. boneio/webui/frontend-dist/assets/LongPressWrapper-DBJJqzCe.js.gz +0 -0
  43. boneio/webui/frontend-dist/assets/{MigrationsSection-CyxjY5qf.js → MigrationsSection-DbfVG02Q.js} +1 -1
  44. boneio/webui/frontend-dist/assets/MigrationsSection-DbfVG02Q.js.gz +0 -0
  45. boneio/webui/frontend-dist/assets/{ModbusView-3TzfMJLo.js → ModbusView-WZiw4AM-.js} +1 -1
  46. boneio/webui/frontend-dist/assets/ModbusView-WZiw4AM-.js.gz +0 -0
  47. boneio/webui/frontend-dist/assets/{MqttPasswordsSection-BJUmLN6j.js → MqttPasswordsSection-CJLQXxi4.js} +1 -1
  48. boneio/webui/frontend-dist/assets/MqttPasswordsSection-CJLQXxi4.js.gz +0 -0
  49. boneio/webui/frontend-dist/assets/{NodeRedManagement-BVfo9HUs.js → NodeRedManagement-Bu0aGhmZ.js} +1 -1
  50. boneio/webui/frontend-dist/assets/NodeRedManagement-Bu0aGhmZ.js.gz +0 -0
  51. boneio/webui/frontend-dist/assets/{OutputsView-hB9mXMLS.js → OutputsView-duD5bn63.js} +1 -1
  52. boneio/webui/frontend-dist/assets/OutputsView-duD5bn63.js.gz +0 -0
  53. boneio/webui/frontend-dist/assets/{SecurityView-DxbLaDjP.js → SecurityView-_p_elrwR.js} +1 -1
  54. boneio/webui/frontend-dist/assets/SecurityView-_p_elrwR.js.gz +0 -0
  55. boneio/webui/frontend-dist/assets/{SensorView-BA_6vwE1.js → SensorView-D6vJZlEc.js} +1 -1
  56. boneio/webui/frontend-dist/assets/SensorView-D6vJZlEc.js.gz +0 -0
  57. boneio/webui/frontend-dist/assets/{SystemState-CLMEarw1.js → SystemState-DjwV_OCE.js} +1 -1
  58. boneio/webui/frontend-dist/assets/SystemState-DjwV_OCE.js.gz +0 -0
  59. boneio/webui/frontend-dist/assets/{TemplatesView-B7Hx_815.js → TemplatesView-qptHqtd1.js} +1 -1
  60. boneio/webui/frontend-dist/assets/TemplatesView-qptHqtd1.js.gz +0 -0
  61. boneio/webui/frontend-dist/assets/{TilePicker-u6F0_WYh.js → TilePicker-DABvr-ke.js} +1 -1
  62. boneio/webui/frontend-dist/assets/TilePicker-DABvr-ke.js.gz +0 -0
  63. boneio/webui/frontend-dist/assets/{TimezoneSection-D2NOMHRt.js → TimezoneSection-BRhcklV2.js} +1 -1
  64. boneio/webui/frontend-dist/assets/TimezoneSection-BRhcklV2.js.gz +0 -0
  65. boneio/webui/frontend-dist/assets/{UISettings-D09Ovoxp.js → UISettings-BruxgcVE.js} +4 -4
  66. boneio/webui/frontend-dist/assets/UISettings-BruxgcVE.js.gz +0 -0
  67. boneio/webui/frontend-dist/assets/{UpdateSafetyNotice-5anVj_YS.js → UpdateSafetyNotice-CmUMxh75.js} +1 -1
  68. boneio/webui/frontend-dist/assets/UpdateSafetyNotice-CmUMxh75.js.gz +0 -0
  69. boneio/webui/frontend-dist/assets/{aiConfig-AbhmFxmh.js → aiConfig-BD0h6rud.js} +1 -1
  70. boneio/webui/frontend-dist/assets/aiConfig-BD0h6rud.js.gz +0 -0
  71. boneio/webui/frontend-dist/assets/{aiWizardPrompt-DROK5vS0.js → aiWizardPrompt-sN_i7xtf.js} +1 -1
  72. boneio/webui/frontend-dist/assets/aiWizardPrompt-sN_i7xtf.js.gz +0 -0
  73. boneio/webui/frontend-dist/assets/{groupByArea-S-fs_0iK.js → groupByArea-ByyDsnWz.js} +1 -1
  74. boneio/webui/frontend-dist/assets/groupByArea-ByyDsnWz.js.gz +0 -0
  75. boneio/webui/frontend-dist/assets/{index-DULLFI0R.js → index-CoFvRO76.js} +11 -11
  76. boneio/webui/frontend-dist/assets/index-CoFvRO76.js.gz +0 -0
  77. boneio/webui/frontend-dist/assets/{monaco-sF_y08LI.js → monaco-BO6Nosej.js} +2 -2
  78. boneio/webui/frontend-dist/assets/monaco-BO6Nosej.js.gz +0 -0
  79. boneio/webui/frontend-dist/assets/{monaco-C_baIGg_.js → monaco-Bj_oSLV1.js} +1 -1
  80. boneio/webui/frontend-dist/assets/monaco-Bj_oSLV1.js.gz +0 -0
  81. boneio/webui/frontend-dist/index.html +2 -2
  82. boneio/webui/frontend-dist/sw.js +1 -1
  83. boneio/webui/routes/system.py +17 -6
  84. boneio/webui/security_headers.py +7 -1
  85. {boneio-1.6.0.dev29.dist-info → boneio-1.6.0.dev30.dist-info}/METADATA +1 -1
  86. {boneio-1.6.0.dev29.dist-info → boneio-1.6.0.dev30.dist-info}/RECORD +90 -86
  87. boneio/webui/frontend-dist/assets/AccountsView-hSkFhGZn.js.gz +0 -0
  88. boneio/webui/frontend-dist/assets/AppInitContext-XnlT3sSY.js.gz +0 -0
  89. boneio/webui/frontend-dist/assets/BackupSection-CdrNqwc2.js.gz +0 -0
  90. boneio/webui/frontend-dist/assets/BindingMatrix-Dyyc2Plw.js.gz +0 -0
  91. boneio/webui/frontend-dist/assets/DeviceControlSection-BI2Gx_ht.js.gz +0 -0
  92. boneio/webui/frontend-dist/assets/EntityGrid-D1gJhSPK.js.gz +0 -0
  93. boneio/webui/frontend-dist/assets/FactoryResetSection-B6zeFfLF.js.gz +0 -0
  94. boneio/webui/frontend-dist/assets/FormInputToggle-B9vctUNt.js.gz +0 -0
  95. boneio/webui/frontend-dist/assets/HaDashboardWizard-BtouQnje.js.gz +0 -0
  96. boneio/webui/frontend-dist/assets/HostnameSection-h9UyWAS0.js.gz +0 -0
  97. boneio/webui/frontend-dist/assets/InputsView-DHMMyEtN.js.gz +0 -0
  98. boneio/webui/frontend-dist/assets/LocationSection-BWJEPzOe.js.gz +0 -0
  99. boneio/webui/frontend-dist/assets/LoggerForm-B4wgrOCg.js.gz +0 -0
  100. boneio/webui/frontend-dist/assets/LongPressWrapper-BuYVn91a.js.gz +0 -0
  101. boneio/webui/frontend-dist/assets/MigrationsSection-CyxjY5qf.js.gz +0 -0
  102. boneio/webui/frontend-dist/assets/ModbusView-3TzfMJLo.js.gz +0 -0
  103. boneio/webui/frontend-dist/assets/MqttPasswordsSection-BJUmLN6j.js.gz +0 -0
  104. boneio/webui/frontend-dist/assets/NodeRedManagement-BVfo9HUs.js.gz +0 -0
  105. boneio/webui/frontend-dist/assets/OutputsView-hB9mXMLS.js.gz +0 -0
  106. boneio/webui/frontend-dist/assets/SecurityView-DxbLaDjP.js.gz +0 -0
  107. boneio/webui/frontend-dist/assets/SensorView-BA_6vwE1.js.gz +0 -0
  108. boneio/webui/frontend-dist/assets/SystemState-CLMEarw1.js.gz +0 -0
  109. boneio/webui/frontend-dist/assets/TemplatesView-B7Hx_815.js.gz +0 -0
  110. boneio/webui/frontend-dist/assets/TilePicker-u6F0_WYh.js.gz +0 -0
  111. boneio/webui/frontend-dist/assets/TimezoneSection-D2NOMHRt.js.gz +0 -0
  112. boneio/webui/frontend-dist/assets/UISettings-D09Ovoxp.js.gz +0 -0
  113. boneio/webui/frontend-dist/assets/UpdateSafetyNotice-5anVj_YS.js.gz +0 -0
  114. boneio/webui/frontend-dist/assets/aiConfig-AbhmFxmh.js.gz +0 -0
  115. boneio/webui/frontend-dist/assets/aiWizardPrompt-DROK5vS0.js.gz +0 -0
  116. boneio/webui/frontend-dist/assets/groupByArea-S-fs_0iK.js.gz +0 -0
  117. boneio/webui/frontend-dist/assets/index-DULLFI0R.js.gz +0 -0
  118. boneio/webui/frontend-dist/assets/monaco-C_baIGg_.js.gz +0 -0
  119. boneio/webui/frontend-dist/assets/monaco-sF_y08LI.js.gz +0 -0
  120. {boneio-1.6.0.dev29.dist-info → boneio-1.6.0.dev30.dist-info}/WHEEL +0 -0
  121. {boneio-1.6.0.dev29.dist-info → boneio-1.6.0.dev30.dist-info}/entry_points.txt +0 -0
  122. {boneio-1.6.0.dev29.dist-info → boneio-1.6.0.dev30.dist-info}/licenses/LICENSE +0 -0
  123. {boneio-1.6.0.dev29.dist-info → boneio-1.6.0.dev30.dist-info}/top_level.txt +0 -0
@@ -5,10 +5,22 @@ rather than in the web server so recovery mode signs with the same secret
5
5
  without importing the web server, which pulls in the whole Manager: a token
6
6
  issued in recovery keeps working once the controller is back, and the other
7
7
  way round.
8
+
9
+ The secret belongs to one controller, and the file says which: its second
10
+ line is a hash of the machine-id it was drawn on. A file that names another
11
+ machine — or none — came from somewhere else, most likely an SD card image
12
+ built on a controller that had already run, and every device flashed from that
13
+ image would otherwise sign with the same key. Anyone holding the image could
14
+ then sign an administrator's token for all of them. Such a file is replaced
15
+ rather than trusted, which signs everybody out once.
16
+
17
+ Only the hash is stored: machine-id is meant to stay on the machine, and the
18
+ file needs no more than to recognise it.
8
19
  """
9
20
 
10
21
  from __future__ import annotations
11
22
 
23
+ import hashlib
12
24
  import logging
13
25
  import os
14
26
  import secrets
@@ -18,40 +30,98 @@ _LOGGER = logging.getLogger(__name__)
18
30
 
19
31
  JWT_SECRET_FILENAME = "jwt_secret"
20
32
 
33
+ #: Where systemd keeps this installation's identity. Emptied when an image is
34
+ #: sealed and drawn afresh on the first boot, which is what makes it the thing
35
+ #: to bind to.
36
+ MACHINE_ID_PATH = Path("/etc/machine-id")
37
+
38
+ _BINDING_PREFIX = "machine:"
39
+
40
+
41
+ def _machine_binding(machine_id_path: Path) -> str | None:
42
+ """The binding line for this machine, or None when it has no identity yet.
43
+
44
+ None is not an error. On an image being built machine-id is empty on
45
+ purpose; a secret drawn then stays unbound and is replaced on the first
46
+ boot, once there is an identity to bind it to.
47
+
48
+ Args:
49
+ machine_id_path: The machine-id file.
50
+
51
+ Returns:
52
+ ``machine:<sha256 of machine-id>``, or None.
53
+ """
54
+ try:
55
+ machine_id = machine_id_path.read_text().strip()
56
+ except OSError:
57
+ return None
58
+ if not machine_id:
59
+ return None
60
+ digest = hashlib.sha256(f"boneio-jwt:{machine_id}".encode()).hexdigest()
61
+ return f"{_BINDING_PREFIX}{digest}"
62
+
63
+
64
+ def _write_secret(path: Path, secret: str, binding: str | None) -> None:
65
+ """Write the secret readable by its owner only, from the first byte.
66
+
67
+ Created 0600 rather than chmod-ed afterwards, and swapped in with a rename,
68
+ so there is never a moment when the key is on disk with looser permissions
69
+ or half written.
70
+ """
71
+ content = secret + (f"\n{binding}" if binding else "") + "\n"
72
+ tmp = path.with_name(f".{path.name}.tmp")
73
+ try:
74
+ tmp.unlink()
75
+ except FileNotFoundError:
76
+ pass
77
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
78
+ with os.fdopen(fd, "w") as f:
79
+ f.write(content)
80
+ f.flush()
81
+ os.fsync(f.fileno())
82
+ os.replace(tmp, path)
21
83
 
22
- def load_or_create_jwt_secret(config_dir: str | os.PathLike[str]) -> str:
84
+
85
+ def load_or_create_jwt_secret(
86
+ config_dir: str | os.PathLike[str],
87
+ machine_id_path: Path = MACHINE_ID_PATH,
88
+ ) -> str:
23
89
  """Read the token secret, creating it on first use.
24
90
 
91
+ A stored secret is used only if it was drawn on this machine. One with no
92
+ binding, or another machine's, is replaced — unless this machine has no
93
+ identity to compare with, in which case it is kept as it is: replacing it
94
+ then would sign everybody out on every start.
95
+
25
96
  Never raises: a secret that cannot be persisted is replaced by one that
26
97
  lives as long as the process, which logs everybody out on the next restart
27
98
  but does not keep the panel from starting.
28
99
 
29
100
  Args:
30
101
  config_dir: The directory config.yaml is in.
102
+ machine_id_path: The machine-id file. A parameter for the tests.
31
103
 
32
104
  Returns:
33
105
  The secret.
34
106
  """
35
107
  jwt_secret_file = Path(config_dir) / JWT_SECRET_FILENAME
108
+ binding = _machine_binding(machine_id_path)
36
109
 
37
110
  try:
38
111
  if jwt_secret_file.exists():
39
- # Read existing secret
40
- with open(jwt_secret_file) as f:
41
- jwt_secret = f.read().strip()
42
- if jwt_secret: # Verify it's not empty
43
- return jwt_secret
112
+ lines = jwt_secret_file.read_text().split()
113
+ stored = lines[0] if lines else ""
114
+ stored_binding = lines[1] if len(lines) > 1 else None
115
+ if stored and (binding is None or stored_binding == binding):
116
+ return stored
117
+ if stored:
118
+ _LOGGER.warning(
119
+ "The token secret was not drawn on this controller; "
120
+ "replacing it. Everybody signed in has to sign in again."
121
+ )
44
122
 
45
- # Generate new secret if file doesn't exist or is empty
46
123
  jwt_secret = secrets.token_hex(32) # 256-bit random secret
47
-
48
- # Save the secret
49
- with open(jwt_secret_file, "w") as f:
50
- f.write(jwt_secret)
51
-
52
- # Secure the file permissions (read/write only for owner)
53
- os.chmod(jwt_secret_file, 0o600)
54
-
124
+ _write_secret(jwt_secret_file, jwt_secret, binding)
55
125
  except Exception as e:
56
126
  # If we can't persist the secret, generate a temporary one
57
127
  _LOGGER.error(f"Failed to handle JWT secret file: {e}")
@@ -88,6 +88,7 @@ class CloudRegistration:
88
88
  self._task: asyncio.Task | None = None
89
89
  self._session: aiohttp.ClientSession | None = None
90
90
  self._last_error: str | None = None
91
+ self._switching = False
91
92
 
92
93
  @property
93
94
  def domain(self) -> str | None:
@@ -104,6 +105,18 @@ class CloudRegistration:
104
105
  """Get the last error message, if any."""
105
106
  return self._last_error
106
107
 
108
+ @property
109
+ def serving(self) -> bool:
110
+ """Whether Caddy is up on the cloud template with the certificate.
111
+
112
+ False while the template is being swapped and Caddy recreated: the
113
+ compose file already names the cloud script then, but the proxy — the
114
+ only way into a panel with ``web.expose: proxy`` — is down for the half
115
+ minute that takes. Anything sending somebody to the registered name
116
+ has to wait for this rather than for the template.
117
+ """
118
+ return self._domain is not None and not self._switching and self.is_cloud_config_active()
119
+
107
120
  @property
108
121
  def is_compose_writable(self) -> bool:
109
122
  """Check if docker-compose.yaml is writable without side effects."""
@@ -551,6 +564,14 @@ class CloudRegistration:
551
564
  Returns:
552
565
  True if the switch was successful.
553
566
  """
567
+ self._switching = True
568
+ try:
569
+ return await self._apply_cloud_config()
570
+ finally:
571
+ self._switching = False
572
+
573
+ async def _apply_cloud_config(self) -> bool:
574
+ """The body of :meth:`_switch_to_cloud_config`, without the flag."""
554
575
  # The init script still comes from the package; it is mounted read-only
555
576
  # into the container and is not what compose executes on the host.
556
577
  self._ensure_cloud_script()
@@ -571,7 +592,9 @@ class CloudRegistration:
571
592
  return False
572
593
 
573
594
  _LOGGER.info("Switched docker-compose.yaml to the cloud template")
574
- return await self._recreate_caddy()
595
+ # The service definition changed, so ``up`` recreates the container
596
+ # and it reads the certificate as it starts.
597
+ return await self._recreate_caddy(restart=False)
575
598
 
576
599
  async def _restore_local_config(self) -> bool:
577
600
  """Restore the plain compose template and restart Caddy.
@@ -595,14 +618,21 @@ class CloudRegistration:
595
618
  return False
596
619
 
597
620
  _LOGGER.info("Restored the local docker-compose.yaml template")
598
- return await self._recreate_caddy()
621
+ return await self._recreate_caddy(restart=False)
599
622
 
600
- async def _recreate_caddy(self) -> bool:
623
+ async def _recreate_caddy(self, restart: bool = True) -> bool:
601
624
  """Recreate and restart Caddy so it picks up new certs or compose config.
602
625
 
603
626
  Both steps go through the container helper, so neither passes anything
604
627
  from here to Docker.
605
628
 
629
+ Args:
630
+ restart: Restart after ``up``. Needed when the compose file did not
631
+ change — ``up`` then leaves the running container alone and it
632
+ keeps the certificate it read at start. Not after a template
633
+ swap: ``up`` has just recreated it, and a restart on top takes
634
+ the proxy down a second time.
635
+
606
636
  Returns:
607
637
  True if the recreate was successful.
608
638
  """
@@ -614,12 +644,16 @@ class CloudRegistration:
614
644
  )
615
645
  return False
616
646
 
647
+ if not restart:
648
+ _LOGGER.info("Caddy container recreated successfully")
649
+ return True
650
+
617
651
  # Restart as well: mounted TLS certificates are only re-read on start.
618
- restart = await loop.run_in_executor(None, containers.restart_caddy)
619
- if not restart.ok:
652
+ restarted = await loop.run_in_executor(None, containers.restart_caddy)
653
+ if not restarted.ok:
620
654
  _LOGGER.warning(
621
655
  "Caddy was recreated but the restart failed: %s",
622
- restart.stderr.strip() or "unknown error",
656
+ restarted.stderr.strip() or "unknown error",
623
657
  )
624
658
 
625
659
  _LOGGER.info("Caddy container recreated and restarted successfully")
@@ -0,0 +1,141 @@
1
+ """Make sure the running Node-RED asks for a password.
2
+
3
+ Node-RED reads ``settings.js`` once, when its container starts. The file can
4
+ say ``adminAuth`` while the editor still answers anyone: a migration installs
5
+ the file during an update, and an update restarts boneIO, not the controller,
6
+ so the container goes on with the settings it started with — an open editor,
7
+ and deploying a flow with an ``exec`` node is code execution in it.
8
+
9
+ So after boneIO starts it asks Node-RED, through the local proxy the way a
10
+ browser would, what its login looks like. An editor that wants no login while
11
+ the file on disk asks for one is restarted, once. Anything else is left alone:
12
+ Node-RED switched off, still starting, or behind a proxy that is being
13
+ recreated all look like "no answer", which is not a reason to restart.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import json
20
+ import logging
21
+ import ssl
22
+ import urllib.error
23
+ import urllib.request
24
+ from collections.abc import Callable
25
+ from pathlib import Path
26
+
27
+ from boneio.core import containers
28
+
29
+ _LOGGER = logging.getLogger(__name__)
30
+
31
+ SETTINGS_FILE = containers.PROJECT_DIR / "node-red" / "settings.js"
32
+
33
+ #: Node-RED needs a while after a boot; so does the first start of Caddy.
34
+ FIRST_CHECK_AFTER = 60.0
35
+ RETRY_EVERY = 60.0
36
+ ATTEMPTS = 10
37
+
38
+ OPEN = "open"
39
+ PROTECTED = "protected"
40
+ UNKNOWN = "unknown"
41
+
42
+
43
+ def probe(port: int, timeout: float = 5.0) -> str:
44
+ """Ask the editor how it signs people in.
45
+
46
+ ``GET /nodered/auth/login`` answers ``{}`` when ``adminAuth`` is off and
47
+ names the login type when it is on. The proxy's certificate is the
48
+ device's own, unverifiable from here, and this request never leaves the
49
+ machine, so it is not checked.
50
+
51
+ Args:
52
+ port: The HTTPS port Caddy publishes.
53
+ timeout: Seconds to wait.
54
+
55
+ Returns:
56
+ OPEN, PROTECTED, or UNKNOWN when there was no usable answer.
57
+ """
58
+ context = ssl.create_default_context()
59
+ context.check_hostname = False
60
+ context.verify_mode = ssl.CERT_NONE
61
+ url = f"https://127.0.0.1:{port}/nodered/auth/login"
62
+ try:
63
+ with urllib.request.urlopen(url, timeout=timeout, context=context) as response: # noqa: S310
64
+ body = json.loads(response.read(4096) or b"null")
65
+ except (OSError, ValueError, urllib.error.URLError):
66
+ return UNKNOWN
67
+ if not isinstance(body, dict):
68
+ return UNKNOWN
69
+ return PROTECTED if body.get("type") else OPEN
70
+
71
+
72
+ def settings_require_login(path: Path = SETTINGS_FILE) -> bool:
73
+ """Whether the settings on disk turn ``adminAuth`` on.
74
+
75
+ Args:
76
+ path: Node-RED's settings.js.
77
+
78
+ Returns:
79
+ True when the file configures it.
80
+ """
81
+ try:
82
+ return "adminAuth" in path.read_text()
83
+ except OSError:
84
+ return False
85
+
86
+
87
+ async def ensure_admin_auth(
88
+ port: int,
89
+ *,
90
+ first_check_after: float = FIRST_CHECK_AFTER,
91
+ retry_every: float = RETRY_EVERY,
92
+ attempts: int = ATTEMPTS,
93
+ probe_fn: Callable[[int], str] = probe,
94
+ settings_path: Path = SETTINGS_FILE,
95
+ restart: Callable[[], containers.Result] = containers.restart_nodered,
96
+ ) -> str:
97
+ """Restart Node-RED once if it runs open while its settings say otherwise.
98
+
99
+ Args:
100
+ port: The HTTPS port Caddy publishes.
101
+ first_check_after: Seconds before the first look.
102
+ retry_every: Seconds between looks while there is no answer.
103
+ attempts: Looks before giving up on an editor that never answers.
104
+ probe_fn: The probe. A parameter for the tests.
105
+ settings_path: Node-RED's settings.js.
106
+ restart: Restarts the container. A parameter for the tests.
107
+
108
+ Returns:
109
+ What it found or did: ``protected``, ``restarted``, ``open`` (the file
110
+ itself asks for no login), ``restart_failed`` or ``unknown``.
111
+ """
112
+ loop = asyncio.get_running_loop()
113
+ await asyncio.sleep(first_check_after)
114
+ for attempt in range(attempts):
115
+ if attempt:
116
+ await asyncio.sleep(retry_every)
117
+ state = await loop.run_in_executor(None, probe_fn, port)
118
+ if state == UNKNOWN:
119
+ continue
120
+ if state == PROTECTED:
121
+ return PROTECTED
122
+ if not settings_require_login(settings_path):
123
+ _LOGGER.error(
124
+ "Node-RED's editor accepts anyone, and %s does not configure "
125
+ "adminAuth either. Apply the pending system migrations.",
126
+ settings_path,
127
+ )
128
+ return OPEN
129
+ _LOGGER.warning(
130
+ "Node-RED is running without the login its settings ask for; "
131
+ "restarting it so it reads them."
132
+ )
133
+ result = await loop.run_in_executor(None, restart)
134
+ if not result.ok:
135
+ _LOGGER.error(
136
+ "Could not restart Node-RED: %s",
137
+ (result.stderr or result.stdout).strip() or "unknown error",
138
+ )
139
+ return "restart_failed"
140
+ return "restarted"
141
+ return UNKNOWN
@@ -0,0 +1 @@
1
+ [{"action":"install_file","dst":"/home/boneio/docker/nodered/node-red/settings.js","expected_sha256":"bff00753c72fb17609a6f7b3a58ef78b7b394da12d19fd65b875f2889695e2b4","group":"boneio","mode":420,"owner":"boneio","src":"docker/nodered/node-red/settings.js","template_vars":{}}]
@@ -0,0 +1,2 @@
1
+ /�Kb'щ��*���g��fJ< �҃wߏ
2
+ �G��)���J�ؼ���v*�� �4�>Z�z���
@@ -1 +1 @@
1
- {"plans":{"1.3.0":"645ac6bdda8dc0a73c219aa3684a29df4bdd4032aad97a833b259de489b2ce32","1.3.1":"580381a4f6f40fc1bd4e903e591ad0d5c1db27daaccafc6f79dabf8c3b84259a","1.3.2":"7a69ffaf0c41438be25a656dcdc87783daac0eae79218d023648ef9d708e373f","1.4.0":"41c3a16e7819857a4b826e944fcfb4f79c58e7192cb151b106a3acd7487d5771","1.4.3":"a8cfbabff09d21944a822c4a70f23a7ee14960c4054de151c9fbc7cdb29adff5","1.4.4":"19508f66d84f1e16bd24932aaa17d45417912ffd1c20d326b2f003b931ac2f8c","1.5.0":"52d9d118da099abda0e0153cd16892e201681f6e4d3c6e5790284d1ac469a407","1.5.1":"d283cb8c01024338fb614ae2199744d883379f59579423e4eb9234f5c59ebdd8","1.5.10":"889e2f12614e6e67d342571c1c3bab8940659bfbbf43fa30a6847e73decb85d1","1.5.11":"4906e4de2b3a9c2f0b0181817c9dc168cb910ba6941691d4e86b8387802b624a","1.5.12":"c0a0dc085bb33d9718e869ed5effc2c1581deffd7ed0d5adc119b94312181625","1.5.13":"bfe58c8756f9b5f5e55d5acbb6f7cfc469d201004d312c14e022eb7c52369b7d","1.5.14":"a0464f11e05128cba8881e6295e591fce1ed1e8d43ab0e16710effb8e543d810","1.5.15":"0687a735cb4aaf8f8f74ad6e228ca1b14e045cba8821991c7d19fc9beaeac766","1.5.16":"3544ac754f39d56ecf2b06300b7cca1b6f8723f99885cd11bd690f1a704d3728","1.5.17":"cf1e9e0ff6812cb7092a82dd588392d625decbe3680fe003dfc554f9045809d3","1.5.18":"1fd873effa66b632217d674745722f712a48b36bb79957fefae722757a74bc30","1.5.2":"39fccb7582e13e7e3436861fde7b83ddb8057788cbde792f2f0cf7003f068850","1.5.3":"39fccb7582e13e7e3436861fde7b83ddb8057788cbde792f2f0cf7003f068850","1.5.4":"e86174ba5aff6f257f8a3f4721c7942885cd203469a9d8ddc15dbe6c704e0f45","1.5.5":"f93ed2d4342e447eb9900a0150331113918f2a8b3baeeb795c07231d80f0337c","1.5.6":"e3d90e6ceb20d760f1e9fa63224c5a070a16b3ca6e7dc4cc2c594138e929cce8","1.5.7":"a0464f11e05128cba8881e6295e591fce1ed1e8d43ab0e16710effb8e543d810","1.5.8":"f075b84fb04f4043658a28402d963494dd58481f164c0b371cec94322fe6d131","1.5.9":"ac170fc924019fc2acd69a90a82ba5c2552dce7a2c01a05e5407443e7a9e5af7","1.6.0":"c8286514d1a4f1805e727e13d3c350bb390378c6f31ad6920030412d61ec9b3f","1.6.1":"aa1d3934091853fc21208993930c1bf92e17136428d1d0baaefabfa65d13e7fa","1.6.10":"41c3a16e7819857a4b826e944fcfb4f79c58e7192cb151b106a3acd7487d5771","1.6.11":"d136203e08e3b052254403815d4ccedb555b0a297686fe97952d275eb1f322eb","1.6.12":"c328d4e36bd6277e01cef0140993e23970f4dff84425f3eba236cdd9a8954355","1.6.13":"f1f8b091d054a08665f047b5ad91f1f309f729a8aa30a2992d6ffdbd7f9fa7e4","1.6.14":"f1f8b091d054a08665f047b5ad91f1f309f729a8aa30a2992d6ffdbd7f9fa7e4","1.6.15":"8cfaeae24ceefe171d40241549c1f62527ce41821e5fd2904c0df1973596c0c7","1.6.16":"4b7e00dbe218a173ee83dd9cb32103f7129520352bdb4b11172d62e55df519c1","1.6.17":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.18":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.19":"4b7e00dbe218a173ee83dd9cb32103f7129520352bdb4b11172d62e55df519c1","1.6.2":"6ef96b5e00a04a929d16f0e4a136bfcc5b8132a466d6907a99b2c8516a8952d5","1.6.20":"28027c836eef19b21b95500760961e181f6078d22cccf8cca62b77bbbb11a0c5","1.6.21":"b0d0777e81edc8532d42bcd3b6d518fecd6078153dfe9f5f859469d2c13b369d","1.6.22":"6600f96197356bdc676e9cd7d2d40b18c9d8838323f8cae0892f056175396313","1.6.23":"4b7e00dbe218a173ee83dd9cb32103f7129520352bdb4b11172d62e55df519c1","1.6.24":"701460ed5e3ed236c8c7273ba0a98a0235786b4a04bd6c7aa6088fb835549aa6","1.6.25":"efa09d1677148bdfe4639273479c9a1fa527d4cf5d2ca9af6dd876883652ba22","1.6.26":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.27":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.28":"6e219d09a06a5de8162a521c9b3c3b54067e6bb4d1d15b3b47a8ec522fd76b5c","1.6.29":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.3":"5c10db827bcb92b9322d4aa5751076b825da87c6d18d6953847ba72317cca862","1.6.30":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.31":"fe5e93397dfe4e459cb7c19bfb09153707ebe4b6c92a60a279bf61b0fbc482fb","1.6.32":"c5dbc5a513107d179d55c2f7360e66299167107acd92bc1f5c3a6b19bc7b6e31","1.6.33":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.4":"baf2bb1c2ec7c1bfccfc514ef1cef67907e056e4f7153bb5932ecb835c9cff78","1.6.5":"65a083182affb9a2beef674bb17dee390bec0007441f1ee71b92e05117c65937","1.6.6":"535bb041b99d5ac1bc9028ea88bc2fbf3dbadc80ea8dea236cc219123c194114","1.6.7":"d59af413e270e6f34b1b9a1c8c4acef35328f00e26fad447cfe214545af78d8f","1.6.8":"5c03d447037a48211f0223bb9ecc8cdc1fa84b93bec8e61b1a3073188ac7ea4e","1.6.9":"978c88cc23a2ce07fe419d40a4f9aaf1a1388d01c6b43235e6f08fc09a03a96f"},"release":"1.6.0.dev29"}
1
+ {"plans":{"1.3.0":"645ac6bdda8dc0a73c219aa3684a29df4bdd4032aad97a833b259de489b2ce32","1.3.1":"580381a4f6f40fc1bd4e903e591ad0d5c1db27daaccafc6f79dabf8c3b84259a","1.3.2":"7a69ffaf0c41438be25a656dcdc87783daac0eae79218d023648ef9d708e373f","1.4.0":"41c3a16e7819857a4b826e944fcfb4f79c58e7192cb151b106a3acd7487d5771","1.4.3":"a8cfbabff09d21944a822c4a70f23a7ee14960c4054de151c9fbc7cdb29adff5","1.4.4":"19508f66d84f1e16bd24932aaa17d45417912ffd1c20d326b2f003b931ac2f8c","1.5.0":"52d9d118da099abda0e0153cd16892e201681f6e4d3c6e5790284d1ac469a407","1.5.1":"d283cb8c01024338fb614ae2199744d883379f59579423e4eb9234f5c59ebdd8","1.5.10":"889e2f12614e6e67d342571c1c3bab8940659bfbbf43fa30a6847e73decb85d1","1.5.11":"4906e4de2b3a9c2f0b0181817c9dc168cb910ba6941691d4e86b8387802b624a","1.5.12":"c0a0dc085bb33d9718e869ed5effc2c1581deffd7ed0d5adc119b94312181625","1.5.13":"bfe58c8756f9b5f5e55d5acbb6f7cfc469d201004d312c14e022eb7c52369b7d","1.5.14":"a0464f11e05128cba8881e6295e591fce1ed1e8d43ab0e16710effb8e543d810","1.5.15":"0687a735cb4aaf8f8f74ad6e228ca1b14e045cba8821991c7d19fc9beaeac766","1.5.16":"3544ac754f39d56ecf2b06300b7cca1b6f8723f99885cd11bd690f1a704d3728","1.5.17":"cf1e9e0ff6812cb7092a82dd588392d625decbe3680fe003dfc554f9045809d3","1.5.18":"1fd873effa66b632217d674745722f712a48b36bb79957fefae722757a74bc30","1.5.2":"39fccb7582e13e7e3436861fde7b83ddb8057788cbde792f2f0cf7003f068850","1.5.3":"39fccb7582e13e7e3436861fde7b83ddb8057788cbde792f2f0cf7003f068850","1.5.4":"e86174ba5aff6f257f8a3f4721c7942885cd203469a9d8ddc15dbe6c704e0f45","1.5.5":"f93ed2d4342e447eb9900a0150331113918f2a8b3baeeb795c07231d80f0337c","1.5.6":"e3d90e6ceb20d760f1e9fa63224c5a070a16b3ca6e7dc4cc2c594138e929cce8","1.5.7":"a0464f11e05128cba8881e6295e591fce1ed1e8d43ab0e16710effb8e543d810","1.5.8":"f075b84fb04f4043658a28402d963494dd58481f164c0b371cec94322fe6d131","1.5.9":"ac170fc924019fc2acd69a90a82ba5c2552dce7a2c01a05e5407443e7a9e5af7","1.6.0":"c8286514d1a4f1805e727e13d3c350bb390378c6f31ad6920030412d61ec9b3f","1.6.1":"aa1d3934091853fc21208993930c1bf92e17136428d1d0baaefabfa65d13e7fa","1.6.10":"41c3a16e7819857a4b826e944fcfb4f79c58e7192cb151b106a3acd7487d5771","1.6.11":"d136203e08e3b052254403815d4ccedb555b0a297686fe97952d275eb1f322eb","1.6.12":"c328d4e36bd6277e01cef0140993e23970f4dff84425f3eba236cdd9a8954355","1.6.13":"f1f8b091d054a08665f047b5ad91f1f309f729a8aa30a2992d6ffdbd7f9fa7e4","1.6.14":"f1f8b091d054a08665f047b5ad91f1f309f729a8aa30a2992d6ffdbd7f9fa7e4","1.6.15":"8cfaeae24ceefe171d40241549c1f62527ce41821e5fd2904c0df1973596c0c7","1.6.16":"4b7e00dbe218a173ee83dd9cb32103f7129520352bdb4b11172d62e55df519c1","1.6.17":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.18":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.19":"4b7e00dbe218a173ee83dd9cb32103f7129520352bdb4b11172d62e55df519c1","1.6.2":"6ef96b5e00a04a929d16f0e4a136bfcc5b8132a466d6907a99b2c8516a8952d5","1.6.20":"28027c836eef19b21b95500760961e181f6078d22cccf8cca62b77bbbb11a0c5","1.6.21":"b0d0777e81edc8532d42bcd3b6d518fecd6078153dfe9f5f859469d2c13b369d","1.6.22":"6600f96197356bdc676e9cd7d2d40b18c9d8838323f8cae0892f056175396313","1.6.23":"4b7e00dbe218a173ee83dd9cb32103f7129520352bdb4b11172d62e55df519c1","1.6.24":"701460ed5e3ed236c8c7273ba0a98a0235786b4a04bd6c7aa6088fb835549aa6","1.6.25":"efa09d1677148bdfe4639273479c9a1fa527d4cf5d2ca9af6dd876883652ba22","1.6.26":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.27":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.28":"6e219d09a06a5de8162a521c9b3c3b54067e6bb4d1d15b3b47a8ec522fd76b5c","1.6.29":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.3":"5c10db827bcb92b9322d4aa5751076b825da87c6d18d6953847ba72317cca862","1.6.30":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.31":"fe5e93397dfe4e459cb7c19bfb09153707ebe4b6c92a60a279bf61b0fbc482fb","1.6.32":"c5dbc5a513107d179d55c2f7360e66299167107acd92bc1f5c3a6b19bc7b6e31","1.6.33":"7f87b537e2d2618d0fed97683c9db9b13143025f63aae7fa6bf6ee2534d9cf68","1.6.34":"c8286514d1a4f1805e727e13d3c350bb390378c6f31ad6920030412d61ec9b3f","1.6.4":"baf2bb1c2ec7c1bfccfc514ef1cef67907e056e4f7153bb5932ecb835c9cff78","1.6.5":"65a083182affb9a2beef674bb17dee390bec0007441f1ee71b92e05117c65937","1.6.6":"535bb041b99d5ac1bc9028ea88bc2fbf3dbadc80ea8dea236cc219123c194114","1.6.7":"d59af413e270e6f34b1b9a1c8c4acef35328f00e26fad447cfe214545af78d8f","1.6.8":"5c03d447037a48211f0223bb9ecc8cdc1fa84b93bec8e61b1a3073188ac7ea4e","1.6.9":"978c88cc23a2ce07fe419d40a4f9aaf1a1388d01c6b43235e6f08fc09a03a96f"},"release":"1.6.0.dev30"}
Binary file
@@ -0,0 +1,51 @@
1
+ """BoneIO 1.6.34 — install the Node-RED settings that require a login, again.
2
+
3
+ 1.6.0 installed ``settings.js`` with ``adminAuth`` delegated to boneIO, and
4
+ recorded itself as applied. On the SD card images that record did not mean the
5
+ file was still there: the images are built on a rootfs that had already been
6
+ through setup once, so 1.6.0 counted as done, and the image script's own step
7
+ then wrote its four-line ``settings.js`` over the real one — no ``adminAuth``.
8
+ Every controller flashed from such an image (seen on dev29, blk239bb2, audit
9
+ finding A-02) ran Node-RED's editor open to anyone who could reach the device,
10
+ which is F-01 back: a flow with an ``exec`` node is code execution.
11
+
12
+ The same file again, under a new version so it is applied once more on every
13
+ device. Where the file is already the right one, the helper finds the hash
14
+ matching and writes nothing.
15
+
16
+ The running container keeps the settings it started with. boneIO restarts it
17
+ on its own after starting, when the editor answers without a login while this
18
+ file asks for one (see :mod:`boneio.core.nodered_guard`), so an update is
19
+ enough and no reboot is needed.
20
+
21
+ To retry on a device, delete ``/var/lib/boneio/migrations.d/1.6.34.applied``
22
+ and restart boneIO.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from boneio.migrations.actions import InstallFile, MigrationAction
28
+
29
+ VERSION = "1.6.34"
30
+ DESCRIPTION = "Install the Node-RED settings that require a login, again (F-01, A-02)"
31
+ REQUIRES_ROOT = True
32
+
33
+ _BONEIO_HOME = "/home/boneio"
34
+ _BONEIO_USER = "boneio"
35
+
36
+
37
+ def plan() -> list[MigrationAction]:
38
+ """Return the ordered list of migration actions for version 1.6.34.
39
+
40
+ Returns:
41
+ List of :class:`MigrationAction` to apply in order.
42
+ """
43
+ return [
44
+ InstallFile(
45
+ src="docker/nodered/node-red/settings.js",
46
+ dst=f"{_BONEIO_HOME}/docker/nodered/node-red/settings.js",
47
+ mode=0o644,
48
+ owner=_BONEIO_USER,
49
+ group=_BONEIO_USER,
50
+ ),
51
+ ]
boneio/runner.py CHANGED
@@ -616,6 +616,16 @@ async def async_run(
616
616
 
617
617
  stable_task = asyncio.create_task(_forgive_earlier_crashes())
618
618
 
619
+ # An update restarts boneIO, not the controller, so a Node-RED settings
620
+ # file installed by it is not what the running editor read. Not in the
621
+ # main gather either: it finishes after a look or two.
622
+ from boneio.const import DEFAULT_PROXY_PORT
623
+ from boneio.core.nodered_guard import ensure_admin_auth
624
+
625
+ nodered_task = asyncio.create_task(
626
+ ensure_admin_auth(_config_helper.proxy_port or DEFAULT_PROXY_PORT)
627
+ )
628
+
619
629
  try:
620
630
  # Convert tasks set to list for main gather
621
631
  task_list = list(tasks)
@@ -668,6 +678,7 @@ async def async_run(
668
678
  finally:
669
679
  _LOGGER.info("Cleaning up resources...")
670
680
  stable_task.cancel()
681
+ nodered_task.cancel()
671
682
 
672
683
  # Cancel pending deferred state saves and write final state synchronously.
673
684
  # This MUST happen before event_bus.stop() which may trigger sigterm
boneio/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # flake8: noqa
2
- __version__ = "1.6.0.dev29"
2
+ __version__ = "1.6.0.dev30"
boneio/webui/app.py CHANGED
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING
14
14
  from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect
15
15
  from fastapi.middleware.cors import CORSMiddleware
16
16
  from fastapi.middleware.gzip import GZipMiddleware
17
- from fastapi.responses import FileResponse, JSONResponse
17
+ from fastapi.responses import FileResponse, JSONResponse, Response
18
18
  from fastapi.staticfiles import StaticFiles
19
19
  from starlette.types import Receive, Scope, Send
20
20
  from starlette.websockets import WebSocketState
@@ -1096,6 +1096,10 @@ if FRONTEND_DIR.exists() and (FRONTEND_DIR / "index.html").exists():
1096
1096
  headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
1097
1097
  )
1098
1098
 
1099
+ #: FastAPI's own schema and docs routes. In dev mode they are registered
1100
+ #: ahead of this catch-all and answer first; otherwise nothing should.
1101
+ _API_DOC_PATHS = frozenset({"docs", "docs/oauth2-redirect", "redoc", "openapi.json"})
1102
+
1099
1103
  @app.get("/{filename:path}")
1100
1104
  async def serve_react_app(filename: str):
1101
1105
  """Serve static files from frontend-dist, fallback to index.html for SPA routing.
@@ -1104,6 +1108,11 @@ if FRONTEND_DIR.exists() and (FRONTEND_DIR / "index.html").exists():
1104
1108
  them. index.html (SPA fallback) is never cached to prevent stale
1105
1109
  versions being served through Cloudflare Tunnel or other caching proxies.
1106
1110
  """
1111
+ if filename in _API_DOC_PATHS:
1112
+ # The schema is off outside dev mode, and these names answering
1113
+ # with the panel's index.html read to a scanner as an exposed API
1114
+ # schema. Saying "not here" is the truth.
1115
+ return Response(status_code=404)
1107
1116
  file_path = (FRONTEND_DIR / filename).resolve()
1108
1117
  if filename and file_path.is_relative_to(FRONTEND_DIR.resolve()) and file_path.exists() and file_path.is_file():
1109
1118
  return FileResponse(str(file_path))
@@ -1 +1 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{H as t,Qt as n,V as r,fn as i,ln as a,ot as o,un as s,vn as c,yn as l}from"./fa-ChDOCx3T.js";import{a as u,i as d,n as f,o as p,r as m,t as h}from"./dialog-DMaCttcv.js";import{n as g}from"./axios-DyqbZVcF.js";import{B as _,F as v,I as y,N as b,P as x,R as S,T as C,h as w,m as T,y as E}from"./index-DULLFI0R.js";import{t as D}from"./LoadingState-BnMjvLnH.js";var O=e(l(),1),k=c(),A=3;function j({id:e,label:n,value:i,onChange:a,autoComplete:o,error:s,autoFocus:c}){let{t:l}=S(),[u,d]=(0,O.useState)(!1);return(0,k.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,k.jsx)(`label`,{htmlFor:e,className:`text-sm font-medium`,children:n}),(0,k.jsxs)(`div`,{className:`input w-full pr-1 ${s?`input-error`:``}`,children:[(0,k.jsx)(`input`,{id:e,type:u?`text`:`password`,autoComplete:o,autoFocus:c,required:!0,className:`grow font-mono`,"aria-invalid":s?!0:void 0,"aria-describedby":s?`${e}-error`:void 0,value:i,onChange:e=>a(e.target.value)}),(0,k.jsx)(`button`,{type:`button`,onClick:()=>d(e=>!e),className:`btn btn-ghost btn-square btn-sm`,"aria-label":l(u?`login.hide_password`:`login.show_password`),"aria-pressed":u,title:l(u?`login.hide_password`:`login.show_password`),children:u?(0,k.jsx)(t,{className:`h-4 w-4 opacity-70`}):(0,k.jsx)(r,{className:`h-4 w-4 opacity-70`})})]}),s&&(0,k.jsx)(`p`,{id:`${e}-error`,role:`alert`,className:`text-error text-xs`,children:s})]})}function M({username:e,isSelf:t,onClose:n,onChanged:r}){let{t:i}=S(),[a,s]=(0,O.useState)(``),[c,l]=(0,O.useState)(``),[_,y]=(0,O.useState)(``),[b,C]=(0,O.useState)(null),[w,T]=(0,O.useState)(null),[E,D]=(0,O.useState)(!1),M=v(c,_,e??``),N=c&&M&&M!==`mismatch`?i(x[M],{min:8}):null,P=_&&M===`mismatch`?i(`accounts.password_mismatch`):null,F=async n=>{if(n.preventDefault(),!(!e||M||t&&!a)){C(null),T(null),D(!0);try{let{data:n}=t?await g.put(`/api/account/password`,{current_password:a,new_password:c},{timeout:6e4}):await g.put(`/api/accounts/${encodeURIComponent(e)}/password`,{password:c},{timeout:6e4});typeof n?.token==`string`&&localStorage.setItem(`token`,n.token),r(e)}catch(e){let t=e.response;if(t?.status===403&&t.data?.code===`current_password_wrong`){let e=t.data.attempts_left;C(typeof e==`number`&&e<=A?`${i(`accounts.current_password_wrong`)} ${i(`reauth.attempts_left`,{count:e})}`:i(`accounts.current_password_wrong`))}else t?.status===429?T(i(`reauth.throttled`)):t?.status!==401&&T(t?.data?.detail||i(`accounts.password_failed`))}finally{D(!1)}}},I=t?i(`accounts.change_own_password_title`):i(`accounts.change_password_title`,{username:e??``});return(0,k.jsx)(h,{open:e!==null,onOpenChange:e=>!e&&!E&&n(),children:(0,k.jsx)(f,{className:`sm:max-w-md`,children:(0,k.jsxs)(`form`,{onSubmit:F,className:`flex flex-col gap-4`,children:[(0,k.jsxs)(u,{children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,k.jsx)(`span`,{className:`stg-chip w-10 h-10 rounded-xl flex items-center justify-center shrink-0`,children:(0,k.jsx)(o,{})}),(0,k.jsx)(p,{children:I})]}),(0,k.jsx)(m,{children:i(t?`accounts.change_own_password_intro`:`accounts.change_password_intro`)})]}),t&&(0,k.jsx)(j,{id:`account-current-password`,label:i(`accounts.current_password`),value:a,onChange:e=>{s(e),C(null)},autoComplete:`current-password`,error:b,autoFocus:!0}),(0,k.jsx)(j,{id:`account-new-password`,label:i(`accounts.new_password`),value:c,onChange:l,autoComplete:`new-password`,error:N,autoFocus:!t}),(0,k.jsx)(j,{id:`account-repeat-password`,label:i(`accounts.repeat_password`),value:_,onChange:y,autoComplete:`new-password`,error:P}),(0,k.jsx)(`p`,{className:`text-xs opacity-70`,children:i(`accounts.ssh_password_separate`)}),w&&(0,k.jsx)(`p`,{role:`alert`,className:`text-error text-sm`,children:w}),(0,k.jsxs)(d,{children:[(0,k.jsx)(`button`,{type:`button`,className:`btn btn-ghost max-sm:btn-lg`,onClick:n,disabled:E,children:i(`common.cancel`)}),(0,k.jsxs)(`button`,{type:`submit`,className:`btn btn-primary max-sm:btn-lg`,disabled:E||!!M||t&&!a,children:[E&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-sm`}),i(`accounts.change_password_button`)]})]})]})})})}function N({username:e,onClose:t,onConfirm:n}){let{t:r}=S(),[i,o]=(0,O.useState)(!1);return(0,k.jsx)(h,{open:e!==null,onOpenChange:e=>!e&&!i&&t(),children:(0,k.jsxs)(f,{className:`sm:max-w-sm`,children:[(0,k.jsxs)(u,{children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,k.jsx)(`span`,{className:`stg-chip w-10 h-10 rounded-xl flex items-center justify-center shrink-0 text-error`,children:(0,k.jsx)(a,{})}),(0,k.jsx)(p,{children:r(`accounts.confirm_delete`,{username:e??``})})]}),(0,k.jsx)(m,{children:r(`accounts.confirm_delete_intro`)})]}),(0,k.jsxs)(d,{children:[(0,k.jsx)(`button`,{type:`button`,className:`btn btn-ghost max-sm:btn-lg`,onClick:t,disabled:i,children:r(`common.cancel`)}),(0,k.jsxs)(`button`,{type:`button`,className:`btn btn-error max-sm:btn-lg`,onClick:async()=>{if(e){o(!0);try{await n(e)}finally{o(!1)}}},disabled:i,children:[i&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-sm`}),r(`accounts.delete`)]})]})]})})}function P(e,t){return e===`locked`||e===`empty`?e:e===`set`||e===`shipped`?t?`form`:`outdated`:`unknown`}var F=`boneio`,I=3;function L({open:e,shipped:t,onClose:r,onChanged:i,onStale:a}){let{t:o}=S(),[s,c]=(0,O.useState)(``),[l,_]=(0,O.useState)(``),[y,b]=(0,O.useState)(``),[C,w]=(0,O.useState)(null),[T,E]=(0,O.useState)(null),[D,A]=(0,O.useState)(!1),M=v(l,y,F),N=l&&M&&M!==`mismatch`?o(x[M],{min:8}):null,P=y&&M===`mismatch`?o(`accounts.password_mismatch`):null;return(0,k.jsx)(h,{open:e,onOpenChange:e=>!e&&!D&&r(),children:(0,k.jsx)(f,{className:`sm:max-w-md`,children:(0,k.jsxs)(`form`,{onSubmit:async e=>{if(e.preventDefault(),!M&&s){w(null),E(null),A(!0);try{await g.put(`/api/accounts/ssh-password`,{current_password:s,new_password:l},{timeout:6e4}),i()}catch(e){let t=e.response,n=t?.data?.code;if(t?.status===403&&n===`current_password_wrong`){let e=t.data?.attempts_left;w(typeof e==`number`&&e<=I?`${o(`accounts.ssh_current_wrong`)} ${o(`accounts.ssh_attempts_left`,{count:e})}`:o(`accounts.ssh_current_wrong`))}else if(t?.status===429){let e=t.data?.retry_after;E(o(`accounts.ssh_throttled`,{minutes:Math.max(1,Math.ceil((typeof e==`number`?e:900)/60))}))}else n===`helper_outdated`||n===`ssh_password_not_set`?a():t?.status!==401&&E(t?.data?.detail||o(`accounts.ssh_failed`))}finally{A(!1)}}},className:`flex flex-col gap-4`,children:[(0,k.jsxs)(u,{children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,k.jsx)(`span`,{className:`stg-chip w-10 h-10 rounded-xl flex items-center justify-center shrink-0`,children:(0,k.jsx)(n,{})}),(0,k.jsx)(p,{children:o(`accounts.ssh_dialog_title`)})]}),(0,k.jsx)(m,{children:o(t?`accounts.ssh_dialog_intro_shipped`:`accounts.ssh_dialog_intro`)})]}),(0,k.jsx)(j,{id:`ssh-current-password`,label:o(`accounts.ssh_current`),value:s,onChange:e=>{c(e),w(null)},autoComplete:`current-password`,error:C,autoFocus:!0}),(0,k.jsx)(j,{id:`ssh-new-password`,label:o(`accounts.ssh_new`),value:l,onChange:_,autoComplete:`new-password`,error:N}),(0,k.jsx)(j,{id:`ssh-repeat-password`,label:o(`accounts.ssh_repeat`),value:y,onChange:b,autoComplete:`new-password`,error:P}),T&&(0,k.jsx)(`p`,{role:`alert`,className:`text-error text-sm`,children:T}),(0,k.jsxs)(d,{children:[(0,k.jsx)(`button`,{type:`button`,className:`btn btn-ghost max-sm:btn-lg`,onClick:r,disabled:D,children:o(`common.cancel`)}),(0,k.jsxs)(`button`,{type:`submit`,className:`btn btn-primary max-sm:btn-lg`,disabled:D||!!M||!s,children:[D&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-sm`}),o(`accounts.ssh_button`)]})]})]})})})}function R(){let{t:e}=S(),[t,r]=(0,O.useState)(null),[i,a]=(0,O.useState)(!1),[o,s]=(0,O.useState)(!0),[c,l]=(0,O.useState)(null),[u,d]=(0,O.useState)(0),[f,p]=(0,O.useState)(!1),m=(0,O.useCallback)(async()=>{try{let{data:e}=await g.get(`/api/accounts/ssh-password`,{timeout:3e4});r(e?.state??null),a(!!e?.supported)}catch{r(null)}finally{s(!1)}},[]);(0,O.useEffect)(()=>{m()},[m]);let h=P(t,i),_=()=>o?(0,k.jsx)(D,{className:`py-2`}):h===`form`?t===`shipped`?(0,k.jsx)(b,{variant:`warning`,message:e(`accounts.ssh_shipped`)}):null:(0,k.jsx)(b,{variant:h===`empty`?`warning`:`info`,message:e(`accounts.ssh_${h}`)});return(0,k.jsxs)(k.Fragment,{children:[(0,k.jsx)(E,{icon:(0,k.jsx)(n,{}),title:e(`accounts.ssh_title`),description:e(`accounts.ssh_intro`),action:h===`form`&&!o?(0,k.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{l(null),d(e=>e+1),p(!0)},children:e(`accounts.ssh_button`)}):void 0,children:(c||_())&&(0,k.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[c&&(0,k.jsx)(b,{variant:`success`,message:c}),_()]})}),(0,k.jsx)(L,{open:f,shipped:t===`shipped`,onClose:()=>p(!1),onChanged:()=>{p(!1),l(e(`accounts.ssh_changed`)),m()},onStale:()=>{p(!1),m()}},u)]})}function z(e,t){return e?.response?.data?.detail||t}function B(){let{t:e}=S(),{username:t,isAdmin:n}=_(),[r,a]=(0,O.useState)([]),[o,c]=(0,O.useState)(!0),[l,u]=(0,O.useState)(null),[d,f]=(0,O.useState)(null),[p,m]=(0,O.useState)(``),[h,v]=(0,O.useState)(``),[A,j]=(0,O.useState)(`viewer`),[P,F]=(0,O.useState)(!1),[I,L]=(0,O.useState)(null),[B,V]=(0,O.useState)(null),H=h?y(h,p):null,U=H?e(x[H],{min:8}):null,W=(0,O.useCallback)(async()=>{try{let{data:e}=await g.get(`/api/accounts`);a(e.accounts??[]),u(null)}catch(t){u(z(t,e(`accounts.load_failed`)))}finally{c(!1)}},[e]);(0,O.useEffect)(()=>{n&&W()},[n,W]);let G=async t=>{t.preventDefault(),u(null),f(null),F(!0);try{await g.post(`/api/accounts`,{username:p,password:h,role:A}),m(``),v(``),j(`viewer`),f(e(`accounts.created`)),await W()}catch(t){u(z(t,e(`accounts.create_failed`)))}finally{F(!1)}},K=async t=>{u(null),f(null);try{await g.delete(`/api/accounts/${encodeURIComponent(t)}`),f(e(`accounts.deleted`,{username:t})),await W()}catch(t){u(z(t,e(`accounts.delete_failed`)))}finally{V(null)}},q=async(t,n)=>{u(null),f(null);try{await g.put(`/api/accounts/${encodeURIComponent(t.username)}/role`,{role:n}),await W()}catch(t){u(z(t,e(`accounts.role_failed`))),await W()}},J=t=>{L(null),u(null),f(e(`accounts.password_reset`,{username:t}))},Y=e=>e.toLowerCase()===(t??``).toLowerCase();return n?(0,k.jsxs)(C,{width:`wide`,children:[l&&(0,k.jsx)(b,{variant:`error`,message:l}),d&&(0,k.jsx)(b,{variant:`success`,message:d}),(0,k.jsx)(E,{icon:(0,k.jsx)(i,{}),title:e(`accounts.title`),children:o?(0,k.jsx)(D,{className:`py-6`}):(0,k.jsx)(`ul`,{className:`stg-inset divide-y divide-base-content/10`,children:r.map(t=>{let n=Y(t.username);return(0,k.jsxs)(`li`,{className:`flex flex-wrap items-center gap-x-3 gap-y-2 px-3 py-2.5`,children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0 flex-1`,children:[(0,k.jsx)(`span`,{className:`font-medium font-mono truncate`,children:t.username}),n&&(0,k.jsx)(`span`,{className:`badge badge-ghost badge-xs shrink-0`,children:e(`accounts.you`)})]}),(0,k.jsxs)(`select`,{className:`select select-xs select-bordered w-auto`,"aria-label":e(`accounts.role`),value:t.role,disabled:n,onChange:e=>q(t,e.target.value),children:[(0,k.jsx)(`option`,{value:`admin`,children:e(`accounts.role_admin`)}),(0,k.jsx)(`option`,{value:`viewer`,children:e(`accounts.role_viewer`)})]}),(0,k.jsxs)(`div`,{className:`flex justify-end gap-1 w-full sm:w-auto`,children:[(0,k.jsx)(`button`,{className:`btn btn-ghost btn-xs`,onClick:()=>{f(null),L(t.username)},children:e(`accounts.reset_password`)}),(0,k.jsx)(`button`,{className:`btn btn-ghost btn-xs text-error`,disabled:n,onClick:()=>V(t.username),children:e(`accounts.delete`)})]})]},t.username)})})}),(0,k.jsx)(`form`,{onSubmit:G,children:(0,k.jsx)(E,{icon:(0,k.jsx)(s,{}),title:e(`accounts.add_title`),description:e(`accounts.add_intro`),footer:(0,k.jsx)(T,{children:(0,k.jsxs)(`button`,{type:`submit`,className:`btn btn-primary btn-sm gap-2`,disabled:P||!p||!h||!!U,children:[P&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-xs`}),e(`accounts.add_button`)]})}),children:(0,k.jsxs)(`div`,{className:`grid grid-cols-1 sm:grid-cols-[1fr_1fr_auto] gap-3`,children:[(0,k.jsx)(w,{label:e(`accounts.username`),children:(0,k.jsx)(`input`,{type:`text`,className:`input input-bordered w-full font-mono`,placeholder:e(`accounts.username`),autoComplete:`off`,required:!0,value:p,onChange:e=>m(e.target.value)})}),(0,k.jsx)(w,{label:e(`accounts.password`),error:U||void 0,children:(0,k.jsx)(`input`,{type:`password`,className:`input input-bordered w-full font-mono ${U?`input-error`:``}`,placeholder:e(`accounts.password`),autoComplete:`new-password`,required:!0,minLength:8,"aria-invalid":U?!0:void 0,"aria-describedby":U?`accounts-password-error`:void 0,value:h,onChange:e=>v(e.target.value)})}),(0,k.jsx)(w,{label:e(`accounts.role`),className:`sm:w-40`,children:(0,k.jsxs)(`select`,{className:`select select-bordered w-full`,value:A,onChange:e=>j(e.target.value),children:[(0,k.jsx)(`option`,{value:`viewer`,children:e(`accounts.role_viewer`)}),(0,k.jsx)(`option`,{value:`admin`,children:e(`accounts.role_admin`)})]})})]})})}),(0,k.jsx)(R,{}),(0,k.jsx)(M,{username:I,isSelf:I!==null&&Y(I),onClose:()=>L(null),onChanged:J},I??``),(0,k.jsx)(N,{username:B,onClose:()=>V(null),onConfirm:K})]}):(0,k.jsx)(C,{width:`wide`,children:(0,k.jsx)(b,{variant:`warning`,message:e(`accounts.admin_only`)})})}export{B as default};
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{H as t,Qt as n,V as r,fn as i,ln as a,ot as o,un as s,vn as c,yn as l}from"./fa-ChDOCx3T.js";import{a as u,i as d,n as f,o as p,r as m,t as h}from"./dialog-DMaCttcv.js";import{n as g}from"./axios-DyqbZVcF.js";import{B as _,F as v,I as y,N as b,P as x,R as S,T as C,h as w,m as T,y as E}from"./index-CoFvRO76.js";import{t as D}from"./LoadingState-hl_hkPPN.js";var O=e(l(),1),k=c(),A=3;function j({id:e,label:n,value:i,onChange:a,autoComplete:o,error:s,autoFocus:c}){let{t:l}=S(),[u,d]=(0,O.useState)(!1);return(0,k.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,k.jsx)(`label`,{htmlFor:e,className:`text-sm font-medium`,children:n}),(0,k.jsxs)(`div`,{className:`input w-full pr-1 ${s?`input-error`:``}`,children:[(0,k.jsx)(`input`,{id:e,type:u?`text`:`password`,autoComplete:o,autoFocus:c,required:!0,className:`grow font-mono`,"aria-invalid":s?!0:void 0,"aria-describedby":s?`${e}-error`:void 0,value:i,onChange:e=>a(e.target.value)}),(0,k.jsx)(`button`,{type:`button`,onClick:()=>d(e=>!e),className:`btn btn-ghost btn-square btn-sm`,"aria-label":l(u?`login.hide_password`:`login.show_password`),"aria-pressed":u,title:l(u?`login.hide_password`:`login.show_password`),children:u?(0,k.jsx)(t,{className:`h-4 w-4 opacity-70`}):(0,k.jsx)(r,{className:`h-4 w-4 opacity-70`})})]}),s&&(0,k.jsx)(`p`,{id:`${e}-error`,role:`alert`,className:`text-error text-xs`,children:s})]})}function M({username:e,isSelf:t,onClose:n,onChanged:r}){let{t:i}=S(),[a,s]=(0,O.useState)(``),[c,l]=(0,O.useState)(``),[_,y]=(0,O.useState)(``),[b,C]=(0,O.useState)(null),[w,T]=(0,O.useState)(null),[E,D]=(0,O.useState)(!1),M=v(c,_,e??``),N=c&&M&&M!==`mismatch`?i(x[M],{min:8}):null,P=_&&M===`mismatch`?i(`accounts.password_mismatch`):null,F=async n=>{if(n.preventDefault(),!(!e||M||t&&!a)){C(null),T(null),D(!0);try{let{data:n}=t?await g.put(`/api/account/password`,{current_password:a,new_password:c},{timeout:6e4}):await g.put(`/api/accounts/${encodeURIComponent(e)}/password`,{password:c},{timeout:6e4});typeof n?.token==`string`&&localStorage.setItem(`token`,n.token),r(e)}catch(e){let t=e.response;if(t?.status===403&&t.data?.code===`current_password_wrong`){let e=t.data.attempts_left;C(typeof e==`number`&&e<=A?`${i(`accounts.current_password_wrong`)} ${i(`reauth.attempts_left`,{count:e})}`:i(`accounts.current_password_wrong`))}else t?.status===429?T(i(`reauth.throttled`)):t?.status!==401&&T(t?.data?.detail||i(`accounts.password_failed`))}finally{D(!1)}}},I=t?i(`accounts.change_own_password_title`):i(`accounts.change_password_title`,{username:e??``});return(0,k.jsx)(h,{open:e!==null,onOpenChange:e=>!e&&!E&&n(),children:(0,k.jsx)(f,{className:`sm:max-w-md`,children:(0,k.jsxs)(`form`,{onSubmit:F,className:`flex flex-col gap-4`,children:[(0,k.jsxs)(u,{children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,k.jsx)(`span`,{className:`stg-chip w-10 h-10 rounded-xl flex items-center justify-center shrink-0`,children:(0,k.jsx)(o,{})}),(0,k.jsx)(p,{children:I})]}),(0,k.jsx)(m,{children:i(t?`accounts.change_own_password_intro`:`accounts.change_password_intro`)})]}),t&&(0,k.jsx)(j,{id:`account-current-password`,label:i(`accounts.current_password`),value:a,onChange:e=>{s(e),C(null)},autoComplete:`current-password`,error:b,autoFocus:!0}),(0,k.jsx)(j,{id:`account-new-password`,label:i(`accounts.new_password`),value:c,onChange:l,autoComplete:`new-password`,error:N,autoFocus:!t}),(0,k.jsx)(j,{id:`account-repeat-password`,label:i(`accounts.repeat_password`),value:_,onChange:y,autoComplete:`new-password`,error:P}),(0,k.jsx)(`p`,{className:`text-xs opacity-70`,children:i(`accounts.ssh_password_separate`)}),w&&(0,k.jsx)(`p`,{role:`alert`,className:`text-error text-sm`,children:w}),(0,k.jsxs)(d,{children:[(0,k.jsx)(`button`,{type:`button`,className:`btn btn-ghost max-sm:btn-lg`,onClick:n,disabled:E,children:i(`common.cancel`)}),(0,k.jsxs)(`button`,{type:`submit`,className:`btn btn-primary max-sm:btn-lg`,disabled:E||!!M||t&&!a,children:[E&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-sm`}),i(`accounts.change_password_button`)]})]})]})})})}function N({username:e,onClose:t,onConfirm:n}){let{t:r}=S(),[i,o]=(0,O.useState)(!1);return(0,k.jsx)(h,{open:e!==null,onOpenChange:e=>!e&&!i&&t(),children:(0,k.jsxs)(f,{className:`sm:max-w-sm`,children:[(0,k.jsxs)(u,{children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,k.jsx)(`span`,{className:`stg-chip w-10 h-10 rounded-xl flex items-center justify-center shrink-0 text-error`,children:(0,k.jsx)(a,{})}),(0,k.jsx)(p,{children:r(`accounts.confirm_delete`,{username:e??``})})]}),(0,k.jsx)(m,{children:r(`accounts.confirm_delete_intro`)})]}),(0,k.jsxs)(d,{children:[(0,k.jsx)(`button`,{type:`button`,className:`btn btn-ghost max-sm:btn-lg`,onClick:t,disabled:i,children:r(`common.cancel`)}),(0,k.jsxs)(`button`,{type:`button`,className:`btn btn-error max-sm:btn-lg`,onClick:async()=>{if(e){o(!0);try{await n(e)}finally{o(!1)}}},disabled:i,children:[i&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-sm`}),r(`accounts.delete`)]})]})]})})}function P(e,t){return e===`locked`||e===`empty`?e:e===`set`||e===`shipped`?t?`form`:`outdated`:`unknown`}var F=`boneio`,I=3;function L({open:e,shipped:t,onClose:r,onChanged:i,onStale:a}){let{t:o}=S(),[s,c]=(0,O.useState)(``),[l,_]=(0,O.useState)(``),[y,b]=(0,O.useState)(``),[C,w]=(0,O.useState)(null),[T,E]=(0,O.useState)(null),[D,A]=(0,O.useState)(!1),M=v(l,y,F),N=l&&M&&M!==`mismatch`?o(x[M],{min:8}):null,P=y&&M===`mismatch`?o(`accounts.password_mismatch`):null;return(0,k.jsx)(h,{open:e,onOpenChange:e=>!e&&!D&&r(),children:(0,k.jsx)(f,{className:`sm:max-w-md`,children:(0,k.jsxs)(`form`,{onSubmit:async e=>{if(e.preventDefault(),!M&&s){w(null),E(null),A(!0);try{await g.put(`/api/accounts/ssh-password`,{current_password:s,new_password:l},{timeout:6e4}),i()}catch(e){let t=e.response,n=t?.data?.code;if(t?.status===403&&n===`current_password_wrong`){let e=t.data?.attempts_left;w(typeof e==`number`&&e<=I?`${o(`accounts.ssh_current_wrong`)} ${o(`accounts.ssh_attempts_left`,{count:e})}`:o(`accounts.ssh_current_wrong`))}else if(t?.status===429){let e=t.data?.retry_after;E(o(`accounts.ssh_throttled`,{minutes:Math.max(1,Math.ceil((typeof e==`number`?e:900)/60))}))}else n===`helper_outdated`||n===`ssh_password_not_set`?a():t?.status!==401&&E(t?.data?.detail||o(`accounts.ssh_failed`))}finally{A(!1)}}},className:`flex flex-col gap-4`,children:[(0,k.jsxs)(u,{children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,k.jsx)(`span`,{className:`stg-chip w-10 h-10 rounded-xl flex items-center justify-center shrink-0`,children:(0,k.jsx)(n,{})}),(0,k.jsx)(p,{children:o(`accounts.ssh_dialog_title`)})]}),(0,k.jsx)(m,{children:o(t?`accounts.ssh_dialog_intro_shipped`:`accounts.ssh_dialog_intro`)})]}),(0,k.jsx)(j,{id:`ssh-current-password`,label:o(`accounts.ssh_current`),value:s,onChange:e=>{c(e),w(null)},autoComplete:`current-password`,error:C,autoFocus:!0}),(0,k.jsx)(j,{id:`ssh-new-password`,label:o(`accounts.ssh_new`),value:l,onChange:_,autoComplete:`new-password`,error:N}),(0,k.jsx)(j,{id:`ssh-repeat-password`,label:o(`accounts.ssh_repeat`),value:y,onChange:b,autoComplete:`new-password`,error:P}),T&&(0,k.jsx)(`p`,{role:`alert`,className:`text-error text-sm`,children:T}),(0,k.jsxs)(d,{children:[(0,k.jsx)(`button`,{type:`button`,className:`btn btn-ghost max-sm:btn-lg`,onClick:r,disabled:D,children:o(`common.cancel`)}),(0,k.jsxs)(`button`,{type:`submit`,className:`btn btn-primary max-sm:btn-lg`,disabled:D||!!M||!s,children:[D&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-sm`}),o(`accounts.ssh_button`)]})]})]})})})}function R(){let{t:e}=S(),[t,r]=(0,O.useState)(null),[i,a]=(0,O.useState)(!1),[o,s]=(0,O.useState)(!0),[c,l]=(0,O.useState)(null),[u,d]=(0,O.useState)(0),[f,p]=(0,O.useState)(!1),m=(0,O.useCallback)(async()=>{try{let{data:e}=await g.get(`/api/accounts/ssh-password`,{timeout:3e4});r(e?.state??null),a(!!e?.supported)}catch{r(null)}finally{s(!1)}},[]);(0,O.useEffect)(()=>{m()},[m]);let h=P(t,i),_=()=>o?(0,k.jsx)(D,{className:`py-2`}):h===`form`?t===`shipped`?(0,k.jsx)(b,{variant:`warning`,message:e(`accounts.ssh_shipped`)}):null:(0,k.jsx)(b,{variant:h===`empty`?`warning`:`info`,message:e(`accounts.ssh_${h}`)});return(0,k.jsxs)(k.Fragment,{children:[(0,k.jsx)(E,{icon:(0,k.jsx)(n,{}),title:e(`accounts.ssh_title`),description:e(`accounts.ssh_intro`),action:h===`form`&&!o?(0,k.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{l(null),d(e=>e+1),p(!0)},children:e(`accounts.ssh_button`)}):void 0,children:(c||_())&&(0,k.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[c&&(0,k.jsx)(b,{variant:`success`,message:c}),_()]})}),(0,k.jsx)(L,{open:f,shipped:t===`shipped`,onClose:()=>p(!1),onChanged:()=>{p(!1),l(e(`accounts.ssh_changed`)),m()},onStale:()=>{p(!1),m()}},u)]})}function z(e,t){return e?.response?.data?.detail||t}function B(){let{t:e}=S(),{username:t,isAdmin:n}=_(),[r,a]=(0,O.useState)([]),[o,c]=(0,O.useState)(!0),[l,u]=(0,O.useState)(null),[d,f]=(0,O.useState)(null),[p,m]=(0,O.useState)(``),[h,v]=(0,O.useState)(``),[A,j]=(0,O.useState)(`viewer`),[P,F]=(0,O.useState)(!1),[I,L]=(0,O.useState)(null),[B,V]=(0,O.useState)(null),H=h?y(h,p):null,U=H?e(x[H],{min:8}):null,W=(0,O.useCallback)(async()=>{try{let{data:e}=await g.get(`/api/accounts`);a(e.accounts??[]),u(null)}catch(t){u(z(t,e(`accounts.load_failed`)))}finally{c(!1)}},[e]);(0,O.useEffect)(()=>{n&&W()},[n,W]);let G=async t=>{t.preventDefault(),u(null),f(null),F(!0);try{await g.post(`/api/accounts`,{username:p,password:h,role:A}),m(``),v(``),j(`viewer`),f(e(`accounts.created`)),await W()}catch(t){u(z(t,e(`accounts.create_failed`)))}finally{F(!1)}},K=async t=>{u(null),f(null);try{await g.delete(`/api/accounts/${encodeURIComponent(t)}`),f(e(`accounts.deleted`,{username:t})),await W()}catch(t){u(z(t,e(`accounts.delete_failed`)))}finally{V(null)}},q=async(t,n)=>{u(null),f(null);try{await g.put(`/api/accounts/${encodeURIComponent(t.username)}/role`,{role:n}),await W()}catch(t){u(z(t,e(`accounts.role_failed`))),await W()}},J=t=>{L(null),u(null),f(e(`accounts.password_reset`,{username:t}))},Y=e=>e.toLowerCase()===(t??``).toLowerCase();return n?(0,k.jsxs)(C,{width:`wide`,children:[l&&(0,k.jsx)(b,{variant:`error`,message:l}),d&&(0,k.jsx)(b,{variant:`success`,message:d}),(0,k.jsx)(E,{icon:(0,k.jsx)(i,{}),title:e(`accounts.title`),children:o?(0,k.jsx)(D,{className:`py-6`}):(0,k.jsx)(`ul`,{className:`stg-inset divide-y divide-base-content/10`,children:r.map(t=>{let n=Y(t.username);return(0,k.jsxs)(`li`,{className:`flex flex-wrap items-center gap-x-3 gap-y-2 px-3 py-2.5`,children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0 flex-1`,children:[(0,k.jsx)(`span`,{className:`font-medium font-mono truncate`,children:t.username}),n&&(0,k.jsx)(`span`,{className:`badge badge-ghost badge-xs shrink-0`,children:e(`accounts.you`)})]}),(0,k.jsxs)(`select`,{className:`select select-xs select-bordered w-auto`,"aria-label":e(`accounts.role`),value:t.role,disabled:n,onChange:e=>q(t,e.target.value),children:[(0,k.jsx)(`option`,{value:`admin`,children:e(`accounts.role_admin`)}),(0,k.jsx)(`option`,{value:`viewer`,children:e(`accounts.role_viewer`)})]}),(0,k.jsxs)(`div`,{className:`flex justify-end gap-1 w-full sm:w-auto`,children:[(0,k.jsx)(`button`,{className:`btn btn-ghost btn-xs`,onClick:()=>{f(null),L(t.username)},children:e(`accounts.reset_password`)}),(0,k.jsx)(`button`,{className:`btn btn-ghost btn-xs text-error`,disabled:n,onClick:()=>V(t.username),children:e(`accounts.delete`)})]})]},t.username)})})}),(0,k.jsx)(`form`,{onSubmit:G,children:(0,k.jsx)(E,{icon:(0,k.jsx)(s,{}),title:e(`accounts.add_title`),description:e(`accounts.add_intro`),footer:(0,k.jsx)(T,{children:(0,k.jsxs)(`button`,{type:`submit`,className:`btn btn-primary btn-sm gap-2`,disabled:P||!p||!h||!!U,children:[P&&(0,k.jsx)(`span`,{className:`loading loading-spinner loading-xs`}),e(`accounts.add_button`)]})}),children:(0,k.jsxs)(`div`,{className:`grid grid-cols-1 sm:grid-cols-[1fr_1fr_auto] gap-3`,children:[(0,k.jsx)(w,{label:e(`accounts.username`),children:(0,k.jsx)(`input`,{type:`text`,className:`input input-bordered w-full font-mono`,placeholder:e(`accounts.username`),autoComplete:`off`,required:!0,value:p,onChange:e=>m(e.target.value)})}),(0,k.jsx)(w,{label:e(`accounts.password`),error:U||void 0,children:(0,k.jsx)(`input`,{type:`password`,className:`input input-bordered w-full font-mono ${U?`input-error`:``}`,placeholder:e(`accounts.password`),autoComplete:`new-password`,required:!0,minLength:8,"aria-invalid":U?!0:void 0,"aria-describedby":U?`accounts-password-error`:void 0,value:h,onChange:e=>v(e.target.value)})}),(0,k.jsx)(w,{label:e(`accounts.role`),className:`sm:w-40`,children:(0,k.jsxs)(`select`,{className:`select select-bordered w-full`,value:A,onChange:e=>j(e.target.value),children:[(0,k.jsx)(`option`,{value:`viewer`,children:e(`accounts.role_viewer`)}),(0,k.jsx)(`option`,{value:`admin`,children:e(`accounts.role_admin`)})]})})]})})}),(0,k.jsx)(R,{}),(0,k.jsx)(M,{username:I,isSelf:I!==null&&Y(I),onClose:()=>L(null),onChanged:J},I??``),(0,k.jsx)(N,{username:B,onClose:()=>V(null),onConfirm:K})]}):(0,k.jsx)(C,{width:`wide`,children:(0,k.jsx)(b,{variant:`warning`,message:e(`accounts.admin_only`)})})}export{B as default};
@@ -1 +1 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{vn as t,yn as n}from"./fa-ChDOCx3T.js";import{n as r}from"./axios-DyqbZVcF.js";import{i}from"./configCache-DZ0vWq_Z.js";var a=e(n(),1),o=`boneio-provisioned`;function s(e){let t=e?.match(/\/(proxy\/\d+)\/?$/);return t?`${o}-${t[1].replace(`/`,`-`)}`:o}function c(e,t){try{return e?.getItem(s(t))===`1`}catch{return!1}}function l(e,t,n){try{e?.setItem(s(t),n?`0`:`1`)}catch{}}var u=`boneio-update`,d=`boneio-stale-reload`;function f(e,t){let n=t?.match(/\/(proxy\/\d+)\/?$/);return n?`${e}-${n[1].replace(`/`,`-`)}`:e}function p(e){if(!e)return null;let t=e.trim();return t.startsWith(`v`)?t.slice(1):t}function m(e,t,n){try{let r=e?.getItem(f(u,t));if(!r)return null;let i=JSON.parse(r);return typeof i?.startedAt!=`number`||typeof i.fromVersion!=`string`||n-i.startedAt>36e5||i.startedAt>n+6e4?null:i}catch{return null}}function h(e,t,n){try{e?.setItem(f(u,t),JSON.stringify(n))}catch{}}function g(e,t){try{e?.removeItem(f(u,t))}catch{}}function _(e,t,n){if(t)return e.wentDown||e.downSince===void 0?e:{...e,downSince:void 0};let r=e.downSince??n,i=!!e.wentDown||n-r>=15e3;return r===e.downSince&&i===!!e.wentDown?e:{...e,downSince:r,wentDown:i}}function v(e,t){let n=p(t);if(!n)return!1;let r=p(e.toVersion);return r?n===r:n!==p(e.fromVersion)}function y(e){let{reachable:t,serverVersion:n,panelVersion:r,mark:i}=e;if(!t)return i?`updating`:`probably_updating`;let a=p(n),o=p(r);return a&&o&&a!==o?`stale_panel`:i&&i.wentDown&&a&&!v(i,a)&&a===p(i.fromVersion)?`update_failed`:`ok`}function b(e,t){try{return e?.getItem(d)===t}catch{return!0}}function x(e,t){try{e?.setItem(d,t)}catch{}}var S=t(),C=(0,a.createContext)({data:null,isLoading:!0,isApiAvailable:!0,needsOnboarding:!1,panelState:`ok`,dismissPanelState:()=>{},refetch:async()=>{}});function w(){return(0,a.useContext)(C)}function T(e,t){return e||!!t?.needs_onboarding}var E=2,D=1e3,O=3e4,k=5e3,A=`boneio-stale-bypass`,j=`1.6.0.dev29`;function M(e){try{return window.sessionStorage.getItem(e)}catch{return null}}function N({children:e}){let[t,n]=(0,a.useState)(null),[o,s]=(0,a.useState)(!0),[c,u]=(0,a.useState)(!0),[d,f]=(0,a.useState)(!1),[p,b]=(0,a.useState)(`ok`),x=(0,a.useCallback)((e,t)=>{let n=window.__BONEIO_BASE_PATH__,r=Date.now(),i=m(window.localStorage,n,r);if(i){let t=_(i,e,r);t!==i&&h(window.localStorage,n,t),i=t}i&&e&&v(i,t)&&(g(window.localStorage,n),i=null);let a=y({reachable:e,serverVersion:t,panelVersion:j,mark:i});a===`stale_panel`&&t&&M(A)===t&&(a=`ok`),b(a)},[]),w=(0,a.useCallback)(()=>{g(window.localStorage,window.__BONEIO_BASE_PATH__);let e=t?.version;if(p===`stale_panel`&&e)try{window.sessionStorage.setItem(A,e)}catch{}b(`ok`)},[t?.version,p]),N=(0,a.useCallback)(async()=>{for(let e=0;e<=E;e++)try{let{data:e}=await r.get(`/api/init`);n(t=>t&&JSON.stringify(t)===JSON.stringify(e)?t:e),f(t=>T(t,e)),l(window.localStorage,window.__BONEIO_BASE_PATH__,!!e?.needs_onboarding),x(!0,e?.version??null),u(!0),s(!1),i();return}catch{e<E&&await new Promise(e=>setTimeout(e,D))}x(!1,null),u(!1),s(!1)},[x]);(0,a.useEffect)(()=>{N()},[N]);let P=!c||p===`stale_panel`;return(0,a.useEffect)(()=>{let e=setInterval(N,P?k:O);return()=>clearInterval(e)},[N,P]),(0,a.useEffect)(()=>{let e=()=>{document.visibilityState===`visible`&&N()};return document.addEventListener(`visibilitychange`,e),()=>document.removeEventListener(`visibilitychange`,e)},[N]),(0,S.jsx)(C.Provider,{value:{data:t,isLoading:o,isApiAvailable:c,needsOnboarding:d,panelState:p,dismissPanelState:w,refetch:N},children:e})}export{p as a,v as c,g as i,h as l,w as n,x as o,b as r,m as s,N as t,c as u};
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{vn as t,yn as n}from"./fa-ChDOCx3T.js";import{n as r}from"./axios-DyqbZVcF.js";import{i}from"./configCache-DZ0vWq_Z.js";var a=e(n(),1),o=`boneio-provisioned`;function s(e){let t=e?.match(/\/(proxy\/\d+)\/?$/);return t?`${o}-${t[1].replace(`/`,`-`)}`:o}function c(e,t){try{return e?.getItem(s(t))===`1`}catch{return!1}}function l(e,t,n){try{e?.setItem(s(t),n?`0`:`1`)}catch{}}var u=`boneio-update`,d=`boneio-stale-reload`;function f(e,t){let n=t?.match(/\/(proxy\/\d+)\/?$/);return n?`${e}-${n[1].replace(`/`,`-`)}`:e}function p(e){if(!e)return null;let t=e.trim();return t.startsWith(`v`)?t.slice(1):t}function m(e,t,n){try{let r=e?.getItem(f(u,t));if(!r)return null;let i=JSON.parse(r);return typeof i?.startedAt!=`number`||typeof i.fromVersion!=`string`||n-i.startedAt>36e5||i.startedAt>n+6e4?null:i}catch{return null}}function h(e,t,n){try{e?.setItem(f(u,t),JSON.stringify(n))}catch{}}function g(e,t){try{e?.removeItem(f(u,t))}catch{}}function _(e,t,n){if(t)return e.wentDown||e.downSince===void 0?e:{...e,downSince:void 0};let r=e.downSince??n,i=!!e.wentDown||n-r>=15e3;return r===e.downSince&&i===!!e.wentDown?e:{...e,downSince:r,wentDown:i}}function v(e,t){let n=p(t);if(!n)return!1;let r=p(e.toVersion);return r?n===r:n!==p(e.fromVersion)}function y(e){let{reachable:t,serverVersion:n,panelVersion:r,mark:i}=e;if(!t)return i?`updating`:`probably_updating`;let a=p(n),o=p(r);return a&&o&&a!==o?`stale_panel`:i&&i.wentDown&&a&&!v(i,a)&&a===p(i.fromVersion)?`update_failed`:`ok`}function b(e,t){try{return e?.getItem(d)===t}catch{return!0}}function x(e,t){try{e?.setItem(d,t)}catch{}}var S=t(),C=(0,a.createContext)({data:null,isLoading:!0,isApiAvailable:!0,needsOnboarding:!1,panelState:`ok`,dismissPanelState:()=>{},refetch:async()=>{}});function w(){return(0,a.useContext)(C)}function T(e,t){return e||!!t?.needs_onboarding}var E=2,D=1e3,O=3e4,k=5e3,A=`boneio-stale-bypass`,j=`1.6.0.dev30`;function M(e){try{return window.sessionStorage.getItem(e)}catch{return null}}function N({children:e}){let[t,n]=(0,a.useState)(null),[o,s]=(0,a.useState)(!0),[c,u]=(0,a.useState)(!0),[d,f]=(0,a.useState)(!1),[p,b]=(0,a.useState)(`ok`),x=(0,a.useCallback)((e,t)=>{let n=window.__BONEIO_BASE_PATH__,r=Date.now(),i=m(window.localStorage,n,r);if(i){let t=_(i,e,r);t!==i&&h(window.localStorage,n,t),i=t}i&&e&&v(i,t)&&(g(window.localStorage,n),i=null);let a=y({reachable:e,serverVersion:t,panelVersion:j,mark:i});a===`stale_panel`&&t&&M(A)===t&&(a=`ok`),b(a)},[]),w=(0,a.useCallback)(()=>{g(window.localStorage,window.__BONEIO_BASE_PATH__);let e=t?.version;if(p===`stale_panel`&&e)try{window.sessionStorage.setItem(A,e)}catch{}b(`ok`)},[t?.version,p]),N=(0,a.useCallback)(async()=>{for(let e=0;e<=E;e++)try{let{data:e}=await r.get(`/api/init`);n(t=>t&&JSON.stringify(t)===JSON.stringify(e)?t:e),f(t=>T(t,e)),l(window.localStorage,window.__BONEIO_BASE_PATH__,!!e?.needs_onboarding),x(!0,e?.version??null),u(!0),s(!1),i();return}catch{e<E&&await new Promise(e=>setTimeout(e,D))}x(!1,null),u(!1),s(!1)},[x]);(0,a.useEffect)(()=>{N()},[N]);let P=!c||p===`stale_panel`;return(0,a.useEffect)(()=>{let e=setInterval(N,P?k:O);return()=>clearInterval(e)},[N,P]),(0,a.useEffect)(()=>{let e=()=>{document.visibilityState===`visible`&&N()};return document.addEventListener(`visibilitychange`,e),()=>document.removeEventListener(`visibilitychange`,e)},[N]),(0,S.jsx)(C.Provider,{value:{data:t,isLoading:o,isApiAvailable:c,needsOnboarding:d,panelState:p,dismissPanelState:w,refetch:N},children:e})}export{p as a,v as c,g as i,h as l,w as n,x as o,b as r,m as s,N as t,c as u};