saltcorn-samba 0.3.4 → 0.3.5

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 (4) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +43 -11
  3. package/index.js +285 -25
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to `saltcorn-samba` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.3.5] – 2026-07-05
8
+
9
+ ### Added
10
+ - **Testverbindung im Konfigurations-Wizard.**
11
+ Auf Schritt 1 „Samba-Server“ gibt es jetzt einen Button
12
+ „→ Verbindung jetzt testen“. Er öffnet eine SMB-Sitzung mit den
13
+ aktuell im Formular stehenden Werten (ohne zu speichern), meldet den
14
+ Benutzer an und listet die Wurzel bzw. den Basispfad des Shares auf.
15
+ Bei Erfolg werden Dauer, Anzahl Einträge und die ersten 20 Namen
16
+ angezeigt; bei Fehlschlag der genaue SMB-Fehler plus konkreter
17
+ Handlungshinweis auf Deutsch (z. B. DNS, ECONNREFUSED, LOGON_FAILURE,
18
+ BAD_NETWORK_NAME, SMBv1).
19
+ - **Neue Route `POST /sambatest` (nur Admin).** Nimmt die Serverdaten
20
+ als JSON entgegen, gibt strukturiertes Ergebnis mit `ok`, `error`,
21
+ `code`, `hint`, `attempted` zurück. Verändert keine Konfiguration.
22
+
23
+ ### Changed
24
+ - **Alle Feld-Beschreibungen in Schritt 1 komplett neu, ausführlich
25
+ auf Deutsch.** Jedes Feld (Server, Freigabe, Domäne, Benutzer,
26
+ Passwort, Basispfad, Port) erklärt Format, Docker-Fallstricke,
27
+ Standardwerte und typische Fehlerquellen. Schritt 2 ebenfalls
28
+ vollständig lokalisiert.
29
+ - Schritt-Namen: „Samba server“ → „Samba-Server“, „Access & permissions“
30
+ → „Zugriff & Berechtigungen“.
31
+
7
32
  ## [0.3.4] – 2026-07-05
8
33
 
9
34
  ### Fixed
package/README.md CHANGED
@@ -98,20 +98,22 @@ docker exec -it <saltcorn-container> \
98
98
  Nach der Installation unter *Einstellungen → Plugins → saltcorn-samba →
99
99
  Configure* ausfüllen. Die Konfiguration ist zweistufig:
100
100
 
101
- ### Schritt 1 — *Samba server*
101
+ ### Schritt 1 — *Samba-Server*
102
102
 
103
103
  | Feld | Beispiel | Beschreibung |
104
104
  |---|---|---|
105
- | Server | `192.168.1.10` | Hostname oder IP des Samba-Servers |
106
- | Share name | `documents` | Name des Shares (ohne Slashes) |
107
- | Domain / Workgroup | `WORKGROUP` | optional |
108
- | Username | `saltcorn-user` | SMB-Benutzer |
109
- | Password | *(secret)* | wird im Saltcorn-Konfigstore gespeichert |
110
- | Base path | `projects` | *optional* beschränkt jeden Zugriff auf dieses Unterverzeichnis |
111
- | Port | `445` | Standard-SMB2-Port |
112
- | SMB host visible to clients | `fileserver.lan` | *optional* — Host, der in `smb://`-Links auftaucht (nützlich in Docker) |
105
+ | **Server** | `192.168.1.20`, `nas01`, `fileserver.local` | Hostname oder IP des Samba-Servers, **ohne** `smb://`-Präfix und **ohne** Backslashes. Muss aus Sicht des Saltcorn-Prozesses auflösbar sein. **In Docker:** nicht `localhost` verwenden – stattdessen LAN-IP des Hosts oder Container ins passende Netzwerk hängen. |
106
+ | **Freigabe / Share-Name** | `daten`, `public`, `projekte` | Name der SMB-Freigabe **ohne** Slashes. Auf dem Server als `[NAME]`-Abschnitt in `smb.conf` bzw. unter Windows als Freigabename sichtbar. Nicht die Ordner-Bezeichnung. |
107
+ | **Domäne / Arbeitsgruppe** | `WORKGROUP`, `CONTOSO` | Meist `WORKGROUP` (Standard). Für Active Directory: NetBIOS-Name der Domäne, nicht der FQDN. |
108
+ | **Benutzername** | `saltcorn` | Samba-/AD-Benutzer, **nicht** im Format `DOMAIN\user` (Domäne gehört in das eigene Feld). Leer lassen für anonymen Zugriff (nur bei `guest ok = yes`). |
109
+ | **Passwort** | *(secret)* | Samba nutzt ein eigenes Passwort (`smbpasswd`), nicht zwingend das Linux-Login. Moderne Server lehnen leere Passwörter ab. |
110
+ | **Basispfad** | `projekte/2026` | Optional. Relativ, mit Slashes, **ohne** führenden `/`. Beschränkt jeden Zugriff auf dieses Unterverzeichnis der Freigabe. `..` und absolute Pfade werden abgelehnt. |
111
+ | **TCP-Port** | `445` | Standard SMB2/3 über TCP. **SMBv1 (139) wird nicht unterstützt** – auf dem Server `min protocol = SMB2` setzen. |
113
112
 
114
- ### Schritt 2 *Access & permissions*
113
+ > **Tipp:** Bevor Sie speichern, klicken Sie auf **„→ Verbindung jetzt
114
+ > testen“** – siehe Abschnitt [Verbindung testen](#verbindung-testen).
115
+
116
+ ### Schritt 2 — *Zugriff & Berechtigungen*
115
117
 
116
118
  | Feld | Default | Beschreibung |
117
119
  |---|---|---|
@@ -129,7 +131,37 @@ Configure* ausfüllen. Die Konfiguration ist zweistufig:
129
131
  - Nur die Features aktivieren, die wirklich gebraucht werden.
130
132
  - Auf dem Samba-Server einen separaten Nutzer mit passenden Rechten anlegen
131
133
  (read-only wenn nur gelesen werden soll).
132
- - Zusätzlich mit *Base path* den Zugriff auf ein Unterverzeichnis begrenzen.
134
+ - Zusätzlich mit *Basispfad* den Zugriff auf ein Unterverzeichnis begrenzen.
135
+
136
+ ### Verbindung testen
137
+
138
+ Direkt im Konfigurations-Wizard (Schritt 1) gibt es den Button
139
+ **„→ Verbindung jetzt testen“**. Er sendet die aktuell im Formular
140
+ stehenden Werte an die interne Route `POST /sambatest` (nur für Admins),
141
+ baut eine SMB-Verbindung auf und listet den Basispfad bzw. die
142
+ Share-Wurzel auf. Es werden dabei **keine** Daten gespeichert oder
143
+ geschrieben.
144
+
145
+ - **Erfolg (grün):** Dauer der Verbindung, Anzahl gefundener Einträge
146
+ und die ersten 20 Namen (Datei/Ordner) werden angezeigt.
147
+ - **Fehler (rot):** Der SMB-/Netzwerk-Fehler wird im Klartext gezeigt,
148
+ zusammen mit einem konkreten Handlungshinweis auf Deutsch, z. B.:
149
+ - `ECONNREFUSED` → Samba läuft nicht oder Firewall/Docker blockt 445
150
+ - `ETIMEDOUT` → Host nicht erreichbar (Ping/`nc -vz`)
151
+ - `ENOTFOUND` → DNS-Auflösung fehlgeschlagen (IP verwenden)
152
+ - `LOGON_FAILURE` / `ACCESS_DENIED` → Benutzer, Passwort oder Domäne falsch
153
+ - `BAD_NETWORK_NAME` → Share-Name existiert so nicht auf dem Server
154
+ - `SMB1 / protocol` → Server bietet nur SMBv1 an (nicht unterstützt)
155
+
156
+ Die Route kann zusätzlich auch von Skripten aufgerufen werden:
157
+
158
+ ```bash
159
+ curl -X POST -H 'Content-Type: application/json' \
160
+ -H "X-CSRF-Token: $CSRF" \
161
+ --cookie "$COOKIES" \
162
+ -d '{"server":"192.168.1.20","share":"daten","username":"u","password":"p"}' \
163
+ https://saltcorn.example.com/sambatest
164
+ ```
133
165
 
134
166
  ---
135
167
 
package/index.js CHANGED
@@ -54,103 +54,262 @@ const DEFAULT_DENIED_EXT = [
54
54
  // Plugin configuration
55
55
  // ---------------------------------------------------------------------------
56
56
 
57
+ // HTML block injected as first "field" of step 1 — Saltcorn renders fields
58
+ // with type="String" and input_type="custom_html" as raw HTML. This gives
59
+ // admins a Test-Verbindung button next to the form itself.
60
+ const CONNECTION_TEST_HTML = `
61
+ <div class="card mb-3" style="border-left:4px solid #0d6efd">
62
+ <div class="card-body">
63
+ <h5 class="card-title" style="margin-top:0">Verbindung testen</h5>
64
+ <p class="card-text" style="margin-bottom:.6rem">
65
+ Prüfen Sie die eingegebenen Zugangsdaten <b>bevor</b> Sie speichern.
66
+ Der Test öffnet eine SMB-Verbindung zum Server, meldet den angegebenen
67
+ Benutzer an und listet das Wurzel-Verzeichnis des Shares (bzw. den
68
+ Basispfad, falls angegeben) auf. Es werden keine Daten geschrieben.
69
+ </p>
70
+ <button type="button" class="btn btn-primary" onclick="sambaTestConn(this)">
71
+ → Verbindung jetzt testen
72
+ </button>
73
+ <div id="sambaTestOut" style="margin-top:.8rem"></div>
74
+ </div>
75
+ </div>
76
+ <script>
77
+ window.sambaTestConn = async function(btn) {
78
+ var out = document.getElementById('sambaTestOut');
79
+ var form = btn.closest('form');
80
+ if (!form) { out.innerHTML = '<div class="alert alert-danger">Formular nicht gefunden.</div>'; return; }
81
+ function v(n){ var el = form.querySelector('[name="'+n+'"]'); return el ? el.value : ''; }
82
+ var payload = {
83
+ server: v('server'),
84
+ share: v('share'),
85
+ domain: v('domain'),
86
+ username: v('username'),
87
+ password: v('password'),
88
+ base_path: v('base_path'),
89
+ port: v('port')
90
+ };
91
+ if (!payload.server || !payload.share) {
92
+ out.innerHTML = '<div class="alert alert-warning">Bitte mindestens <b>Server</b> und <b>Share</b> ausfüllen.</div>';
93
+ return;
94
+ }
95
+ var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
96
+ btn.disabled = true;
97
+ var oldTxt = btn.textContent;
98
+ btn.textContent = 'Teste …';
99
+ out.innerHTML = '<div class="text-muted">Verbindung wird aufgebaut … (bis zu 30 s)</div>';
100
+ try {
101
+ var r = await fetch('/sambatest', {
102
+ method: 'POST',
103
+ credentials: 'same-origin',
104
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'CSRF-Token': csrf },
105
+ body: JSON.stringify(payload)
106
+ });
107
+ var data = await r.json();
108
+ if (data && data.ok) {
109
+ var rows = (data.entries||[]).map(function(e){
110
+ return '<li>'+ (e.isDirectory?'📁 ':'📄 ') + String(e.name).replace(/[<>&]/g,'?') +'</li>';
111
+ }).join('');
112
+ out.innerHTML =
113
+ '<div class="alert alert-success">' +
114
+ '<b>✓ Verbindung erfolgreich</b> (' + data.duration_ms + ' ms)<br>' +
115
+ 'Server: <code>' + data.server + ':' + data.port + '</code>, Share: <code>' + data.share + '</code>, ' +
116
+ 'Basispfad: <code>' + data.base_path + '</code>, Benutzer: <code>' + data.username + '</code><br>' +
117
+ 'Einträge gefunden: <b>' + data.entry_count + '</b>' + (data.truncated ? ' (erste 20 unten)' : '') +
118
+ (rows ? '<ul style="margin:.5rem 0 0 1rem">' + rows + '</ul>' : '') +
119
+ '</div>';
120
+ } else {
121
+ var a = data && data.attempted || {};
122
+ out.innerHTML =
123
+ '<div class="alert alert-danger">' +
124
+ '<b>✗ Verbindung fehlgeschlagen</b><br>' +
125
+ 'Fehler: <code>' + String(data && data.error || 'Unbekannt').replace(/[<>&]/g,'?') + '</code>' +
126
+ (data && data.code ? ' <span class="text-muted">(' + data.code + ')</span>' : '') + '<br>' +
127
+ (data && data.hint ? '<div style="margin-top:.4rem"><b>Hinweis:</b> ' + String(data.hint).replace(/[<>&]/g,'?') + '</div>' : '') +
128
+ '<details style="margin-top:.4rem"><summary>Versuchte Verbindungsdaten</summary>' +
129
+ '<table class="table table-sm" style="margin-top:.4rem">' +
130
+ '<tr><td>Server</td><td><code>' + (a.server||'') + ':' + (a.port||'') + '</code></td></tr>' +
131
+ '<tr><td>Share</td><td><code>' + (a.share||'') + '</code></td></tr>' +
132
+ '<tr><td>Basispfad</td><td><code>' + (a.base_path||'') + '</code></td></tr>' +
133
+ '<tr><td>Domäne</td><td><code>' + (a.domain||'') + '</code></td></tr>' +
134
+ '<tr><td>Benutzer</td><td><code>' + (a.username||'') + '</code></td></tr>' +
135
+ '</table></details>' +
136
+ '</div>';
137
+ }
138
+ } catch (e) {
139
+ out.innerHTML = '<div class="alert alert-danger">Fehler beim Aufruf von /sambatest: ' +
140
+ String(e && e.message || e).replace(/[<>&]/g,'?') + '</div>';
141
+ } finally {
142
+ btn.disabled = false;
143
+ btn.textContent = oldTxt;
144
+ }
145
+ };
146
+ </script>
147
+ `;
148
+
57
149
  const configuration_workflow = () =>
58
150
  new Workflow({
59
151
  steps: [
60
152
  {
61
- name: "Samba server",
153
+ name: "Samba-Server",
62
154
  form: () =>
63
155
  new Form({
64
156
  fields: [
65
157
  new Field({
66
158
  name: "server",
67
- label: "Server",
68
- sublabel: "Hostname or IP of the Samba server (no smb:// prefix)",
159
+ label: "Server (Hostname oder IP)",
160
+ sublabel:
161
+ "Adresse des Samba-Servers <b>ohne</b> smb://-Präfix und ohne Backslashes. " +
162
+ "Beispiele: <code>192.168.1.20</code>, <code>fileserver.local</code>, <code>nas01</code>. " +
163
+ "Läuft Saltcorn in Docker, ist <code>localhost</code> → der Container selbst; nutzen Sie die LAN-IP " +
164
+ "des Host-Systems (nicht 127.0.0.1) oder verbinden Sie den Container ins gleiche Bridge/Host-Netzwerk. " +
165
+ "Der Hostname muss <b>aus Sicht des Saltcorn-Prozesses</b> auflösbar sein.",
69
166
  type: "String",
70
167
  required: true,
71
168
  }),
72
169
  new Field({
73
170
  name: "share",
74
- label: "Share name",
75
- sublabel: "The share to connect to (without slashes)",
171
+ label: "Freigabe / Share-Name",
172
+ sublabel:
173
+ "Name der SMB-Freigabe <b>ohne</b> Schrägstriche. Auf dem Server sichtbar als <code>[NAME]</code>-Abschnitt " +
174
+ "in <code>/etc/samba/smb.conf</code> oder unter Windows als Freigabe-Name. " +
175
+ "Beispiele: <code>daten</code>, <code>public</code>, <code>projekte</code>. " +
176
+ "Nicht die Ordner-Bezeichnung, sondern der Freigabe-Name eingeben.",
76
177
  type: "String",
77
178
  required: true,
78
179
  }),
79
- new Field({ name: "domain", label: "Domain / Workgroup", type: "String", default: "WORKGROUP" }),
80
- new Field({ name: "username", label: "Username", type: "String" }),
81
- new Field({ name: "password", label: "Password", type: "String", input_type: "password" }),
180
+ new Field({
181
+ name: "domain",
182
+ label: "Domäne / Arbeitsgruppe",
183
+ sublabel:
184
+ "Windows-Domäne oder Arbeitsgruppe des Benutzers. " +
185
+ "Für klassische Samba-Server im Heim-/Firmennetz meist <code>WORKGROUP</code> (Standard). " +
186
+ "Für Active Directory: NetBIOS-Name der Domäne, z. B. <code>CONTOSO</code>. Nicht der FQDN.",
187
+ type: "String",
188
+ default: "WORKGROUP",
189
+ }),
190
+ new Field({
191
+ name: "username",
192
+ label: "Benutzername",
193
+ sublabel:
194
+ "Samba-/AD-Benutzer, unter dem gelesen und (je nach Rechten) geschrieben wird. " +
195
+ "<b>Nicht</b> im Format <code>DOMÄNE\\user</code> – die Domäne gehört oben ins eigene Feld. " +
196
+ "Leer lassen für anonymen Zugriff (nur möglich, wenn der Share <code>guest ok = yes</code> erlaubt).",
197
+ type: "String",
198
+ }),
199
+ new Field({
200
+ name: "password",
201
+ label: "Passwort",
202
+ sublabel:
203
+ "Passwort des Samba-Benutzers. Wird verschlüsselt in der Saltcorn-Datenbank abgelegt. " +
204
+ "<b>Wichtig:</b> Samba nutzt ein eigenes Passwort (<code>smbpasswd</code>), nicht zwingend das Linux-Login-Passwort. " +
205
+ "Moderne Samba-Server lehnen leere Passwörter ab.",
206
+ type: "String",
207
+ input_type: "password",
208
+ }),
82
209
  new Field({
83
210
  name: "base_path",
84
- label: "Base path",
85
- sublabel: "Optional. All access is restricted to this sub-directory of the share.",
211
+ label: "Basispfad (optional)",
212
+ sublabel:
213
+ "Beschränkt sämtlichen Zugriff auf ein Unterverzeichnis der Freigabe. " +
214
+ "Relativ, mit Schrägstrichen, <b>ohne</b> führenden Slash. " +
215
+ "Beispiel: <code>projekte/2026</code> → der File-Manager sieht nur diesen Unterordner und alles darin. " +
216
+ "Leer lassen für die komplette Share-Wurzel. <code>..</code> und absolute Pfade werden abgelehnt.",
86
217
  type: "String",
87
218
  }),
88
- new Field({ name: "port", label: "Port", type: "Integer", default: 445 }),
219
+ new Field({
220
+ name: "port",
221
+ label: "TCP-Port",
222
+ sublabel:
223
+ "SMB-Port des Servers. Standard: <code>445</code> (SMB2/3 über TCP). " +
224
+ "Nur ändern, wenn Ihr Server bewusst auf einem anderen Port läuft. " +
225
+ "<b>SMBv1 (Port 139/NetBIOS) wird nicht unterstützt</b> – aktivieren Sie SMB2+ auf dem Server " +
226
+ "(<code>min protocol = SMB2</code> in smb.conf).",
227
+ type: "Integer",
228
+ default: 445,
229
+ }),
230
+ new Field({
231
+ name: "_test_html",
232
+ label: " ",
233
+ input_type: "custom_html",
234
+ attributes: { html: CONNECTION_TEST_HTML },
235
+ }),
89
236
  ],
90
237
  }),
91
238
  },
92
239
  {
93
- name: "Access & permissions",
240
+ name: "Zugriff & Berechtigungen",
94
241
  form: () =>
95
242
  new Form({
96
243
  fields: [
97
244
  new Field({
98
245
  name: "min_role_read",
99
- label: "Minimum role to read files",
100
- sublabel: "Saltcorn role level. 1=admin, 40=staff, 80=user, 100=public. Default: 80.",
246
+ label: "Mindestrolle für Lesen",
247
+ sublabel:
248
+ "Saltcorn-Rolle, die mindestens nötig ist, um Dateien und Verzeichnisse anzusehen. " +
249
+ "Werte: <code>1</code>=Admin, <code>40</code>=Staff, <code>80</code>=User, <code>100</code>=Public. " +
250
+ "Kleinere Zahl = höhere Rolle. Standard: <code>80</code> (alle angemeldeten Benutzer).",
101
251
  type: "Integer",
102
252
  default: 80,
103
253
  }),
104
254
  new Field({
105
255
  name: "min_role_write",
106
- label: "Minimum role to upload / delete / rename",
256
+ label: "Mindestrolle für Upload / Löschen / Umbenennen",
107
257
  sublabel:
108
- "Set to 100 to disable all write actions completely. Default: 40 (staff and admins).",
258
+ "Setzen Sie auf <code>100</code>, um <b>alle</b> Schreibaktionen komplett zu deaktivieren auch wenn die " +
259
+ "Checkboxen unten aktiv sind. Standard: <code>40</code> (nur Staff und Admin).",
109
260
  type: "Integer",
110
261
  default: 40,
111
262
  }),
112
263
  new Field({
113
264
  name: "allow_upload",
114
- label: "Allow file upload",
265
+ label: "Datei-Upload erlauben",
266
+ sublabel: "Zeigt den Upload-Button und aktiviert POST /sambaupload.",
115
267
  type: "Bool",
116
268
  default: false,
117
269
  }),
118
270
  new Field({
119
271
  name: "allow_delete",
120
- label: "Allow delete",
272
+ label: "Löschen erlauben",
273
+ sublabel: "Erlaubt das Löschen von Dateien und leeren Verzeichnissen.",
121
274
  type: "Bool",
122
275
  default: false,
123
276
  }),
124
277
  new Field({
125
278
  name: "allow_rename",
126
- label: "Allow rename / move",
279
+ label: "Umbenennen / Verschieben erlauben",
280
+ sublabel: "Erlaubt Umbenennen und Verschieben innerhalb des Basispfads.",
127
281
  type: "Bool",
128
282
  default: false,
129
283
  }),
130
284
  new Field({
131
285
  name: "allow_mkdir",
132
- label: "Allow creating new directories",
286
+ label: "Neue Verzeichnisse anlegen erlauben",
287
+ sublabel: "Zeigt den „Neuer Ordner“-Button in der File-Manager-Ansicht.",
133
288
  type: "Bool",
134
289
  default: false,
135
290
  }),
136
291
  new Field({
137
292
  name: "max_upload_mb",
138
- label: "Max upload size per file (MiB)",
293
+ label: "Max. Upload-Größe pro Datei (MiB)",
294
+ sublabel: "Serverseitige Obergrenze pro Datei. Standard: 50 MiB.",
139
295
  type: "Integer",
140
296
  default: 50,
141
297
  }),
142
298
  new Field({
143
299
  name: "denied_extensions",
144
- label: "Blocked file extensions (comma-separated, no dots)",
300
+ label: "Gesperrte Datei-Endungen (komma-getrennt, ohne Punkte)",
145
301
  sublabel:
146
- "Default: exe,bat,cmd,com,msi,scr,vbs,js,jse,wsf,wsh,ps1,ps1xml,psm1,sh,bash,zsh. Leave empty for the default set.",
302
+ "Standard: <code>exe,bat,cmd,com,msi,scr,vbs,js,jse,wsf,wsh,ps1,ps1xml,psm1,sh,bash,zsh</code>. " +
303
+ "Leer lassen für die Standard-Liste.",
147
304
  type: "String",
148
305
  }),
149
306
  new Field({
150
307
  name: "public_smb_host",
151
- label: "SMB host visible to clients",
308
+ label: "Für Clients sichtbarer SMB-Hostname (optional)",
152
309
  sublabel:
153
- "Optional. Host used in generated smb:// links. Defaults to the server field. Useful when Saltcorn runs in Docker but clients should see the LAN name.",
310
+ "Wird in erzeugten <code>smb://</code>-Links verwendet. Standard: der Wert aus dem Feld <b>Server</b>. " +
311
+ "Nützlich, wenn Saltcorn in Docker läuft (Server = interne IP), Clients aber den LAN-Namen sehen sollen " +
312
+ "(z. B. <code>fileserver.local</code>).",
154
313
  type: "String",
155
314
  }),
156
315
  ],
@@ -369,6 +528,107 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
369
528
  },
370
529
  },
371
530
 
531
+ // ---- Connection test (admin only) --------------------------------------
532
+ //
533
+ // POST /sambatest
534
+ // Body (JSON): { server, share, domain, username, password, base_path, port }
535
+ // Returns: { ok, server, share, base_path, entries?: [{name,isDirectory}],
536
+ // error?, code?, hint? }
537
+ //
538
+ // The route accepts values straight from the config form so admins can
539
+ // verify the connection BEFORE saving. It never touches the persisted
540
+ // configuration and is restricted to role_id === 1 (admin).
541
+ {
542
+ url: "/sambatest",
543
+ method: "post",
544
+ callback: async ({ req, res }) => {
545
+ if (roleOf(req) !== 1) {
546
+ return jsonError(res, 403, "Only admins can test the connection.");
547
+ }
548
+ const body = (req.body && typeof req.body === "object") ? req.body : {};
549
+ const testCfg = {
550
+ server: String(body.server || "").trim(),
551
+ share: String(body.share || "").trim(),
552
+ domain: String(body.domain || "").trim() || "WORKGROUP",
553
+ username: String(body.username || "").trim(),
554
+ password: String(body.password || ""),
555
+ base_path: String(body.base_path || "").trim(),
556
+ port: Number(body.port) || 445,
557
+ };
558
+
559
+ if (!testCfg.server) return jsonError(res, 400, "Please enter a Server (hostname or IP).");
560
+ if (!testCfg.share) return jsonError(res, 400, "Please enter a Share name.");
561
+
562
+ const started = Date.now();
563
+ try {
564
+ const listing = await withClient(testCfg, async (client) => {
565
+ // If base_path is set, list it — that also verifies traversal.
566
+ const rel = testCfg.base_path ? sanitizeRelativePath(testCfg.base_path) : "";
567
+ return await client.readdir(rel);
568
+ });
569
+ const took = Date.now() - started;
570
+ return res.json({
571
+ ok: true,
572
+ server: testCfg.server,
573
+ share: testCfg.share,
574
+ base_path: testCfg.base_path || "(share root)",
575
+ port: testCfg.port,
576
+ domain: testCfg.domain,
577
+ username: testCfg.username || "(anonymous)",
578
+ duration_ms: took,
579
+ entry_count: Array.isArray(listing) ? listing.length : 0,
580
+ entries: (Array.isArray(listing) ? listing : []).slice(0, 20).map((e) => ({
581
+ name: e && (e.name || e),
582
+ isDirectory: !!(e && (e.isDirectory === true || (e.stats && e.stats.isDirectory && e.stats.isDirectory()))),
583
+ })),
584
+ truncated: Array.isArray(listing) && listing.length > 20,
585
+ });
586
+ } catch (err) {
587
+ // Turn opaque SMB / socket errors into actionable hints.
588
+ const msg = String((err && err.message) || err || "Unknown error");
589
+ const code = (err && (err.code || err.errno)) || null;
590
+ let hint = null;
591
+ const m = msg.toLowerCase();
592
+ if (code === "ECONNREFUSED" || m.includes("econnrefused"))
593
+ hint = "The server refused the connection on port " + testCfg.port +
594
+ ". Check that Samba is running and that a firewall (or Docker) does not block TCP/445.";
595
+ else if (code === "ETIMEDOUT" || m.includes("etimedout") || m.includes("timed out"))
596
+ hint = "Timeout — the host did not respond. Verify Server address and that it is reachable from the Saltcorn host (try: ping / nc -vz " + testCfg.server + " " + testCfg.port + ").";
597
+ else if (code === "ENOTFOUND" || code === "EAI_AGAIN" || m.includes("getaddrinfo"))
598
+ hint = "DNS lookup failed for '" + testCfg.server + "'. Use an IP address or make sure the hostname resolves from inside the Saltcorn container.";
599
+ else if (code === "EHOSTUNREACH" || m.includes("ehostunreach"))
600
+ hint = "No route to host. Check network / VPN / bridged Docker network.";
601
+ else if (m.includes("logon_failure") || m.includes("nt_status_logon_failure") || m.includes("access_denied") || m.includes("nt_status_access_denied"))
602
+ hint = "Login rejected. Check Username, Password and Domain / Workgroup. On modern Samba servers, empty passwords are usually disabled.";
603
+ else if (m.includes("bad_network_name") || m.includes("nt_status_bad_network_name"))
604
+ hint = "The share '" + testCfg.share + "' does not exist on " + testCfg.server + ". Check spelling and the [share] section in smb.conf.";
605
+ else if (m.includes("account_disabled") || m.includes("account_locked"))
606
+ hint = "The user account is disabled or locked on the Samba server.";
607
+ else if (m.includes("password_expired") || m.includes("password_must_change"))
608
+ hint = "The password must be changed before this account can be used.";
609
+ else if (m.includes("smb1") || m.includes("protocol"))
610
+ hint = "The server may be offering only SMBv1 which this plugin does not support. Enable SMB2 / SMB3 on the Samba server (min protocol = SMB2 in smb.conf).";
611
+ else if (m.includes("traversal") || m.includes("path"))
612
+ hint = "The Base path could not be validated. It must be a relative sub-directory (e.g. 'projects/2026'), never start with / or \\, and must not contain '..'.";
613
+
614
+ return res.status(200).json({
615
+ ok: false,
616
+ error: msg,
617
+ code,
618
+ hint,
619
+ attempted: {
620
+ server: testCfg.server,
621
+ share: testCfg.share,
622
+ port: testCfg.port,
623
+ domain: testCfg.domain,
624
+ username: testCfg.username || "(anonymous)",
625
+ base_path: testCfg.base_path || "(share root)",
626
+ },
627
+ });
628
+ }
629
+ },
630
+ },
631
+
372
632
  // ---- Write routes (v0.3.0) --------------------------------------------
373
633
 
374
634
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share. File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
5
5
  "main": "index.js",
6
6
  "scripts": {