saltcorn-samba 0.3.4 → 0.3.6

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 +48 -0
  2. package/README.md +43 -11
  3. package/index.js +333 -25
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ 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.6] – 2026-07-05
8
+
9
+ ### Fixed
10
+ - **`JSON.parse: unexpected character at line 1 column 1` beim Test-Button behoben.**
11
+ Der Aufruf von `POST /sambatest` lief zuvor gegen die globale
12
+ CSRF-Middleware von Saltcorn, die bei ungültigem/fehlendem Token eine
13
+ HTML-Seite zurückliefert – der Browser konnte sie nicht als JSON parsen.
14
+ Fix in drei Schichten:
15
+ 1. Route ist jetzt mit `noCsrf: true` markiert (Sicherheit weiterhin durch
16
+ die harte Admin-Prüfung `roleOf(req) !== 1` im Handler garantiert;
17
+ siehe [saltcorn plugin_routes_handler.js](https://github.com/saltcorn/saltcorn/blob/master/packages/server/plugin_routes_handler.js)).
18
+ 2. Der Client sendet nun als `application/x-www-form-urlencoded`
19
+ (statt JSON) und legt das `_csrf`-Token direkt in den Body – so
20
+ würde auch ein aktiver CSRF-Check passieren.
21
+ 3. Das Token wird jetzt zuverlässig aus drei Quellen gesucht:
22
+ dem versteckten `<input name="_csrf">` des Formulars, dem
23
+ `<meta name="csrf-token">`-Tag und `window._sc_globalCsrf`.
24
+ - **Klartext-Fehler statt kryptischem Parser-Crash.**
25
+ Wenn der Server doch mal HTML statt JSON liefert (z. B. Login-Redirect,
26
+ Plugin nicht neu geladen), zeigt der Test-Button jetzt eine deutliche
27
+ Meldung mit HTTP-Status und einer aufklappbaren Rohantwort statt eines
28
+ hilflosen „unexpected character“.
29
+
30
+ ## [0.3.5] – 2026-07-05
31
+
32
+ ### Added
33
+ - **Testverbindung im Konfigurations-Wizard.**
34
+ Auf Schritt 1 „Samba-Server“ gibt es jetzt einen Button
35
+ „→ Verbindung jetzt testen“. Er öffnet eine SMB-Sitzung mit den
36
+ aktuell im Formular stehenden Werten (ohne zu speichern), meldet den
37
+ Benutzer an und listet die Wurzel bzw. den Basispfad des Shares auf.
38
+ Bei Erfolg werden Dauer, Anzahl Einträge und die ersten 20 Namen
39
+ angezeigt; bei Fehlschlag der genaue SMB-Fehler plus konkreter
40
+ Handlungshinweis auf Deutsch (z. B. DNS, ECONNREFUSED, LOGON_FAILURE,
41
+ BAD_NETWORK_NAME, SMBv1).
42
+ - **Neue Route `POST /sambatest` (nur Admin).** Nimmt die Serverdaten
43
+ als JSON entgegen, gibt strukturiertes Ergebnis mit `ok`, `error`,
44
+ `code`, `hint`, `attempted` zurück. Verändert keine Konfiguration.
45
+
46
+ ### Changed
47
+ - **Alle Feld-Beschreibungen in Schritt 1 komplett neu, ausführlich
48
+ auf Deutsch.** Jedes Feld (Server, Freigabe, Domäne, Benutzer,
49
+ Passwort, Basispfad, Port) erklärt Format, Docker-Fallstricke,
50
+ Standardwerte und typische Fehlerquellen. Schritt 2 ebenfalls
51
+ vollständig lokalisiert.
52
+ - Schritt-Namen: „Samba server“ → „Samba-Server“, „Access & permissions“
53
+ → „Zugriff & Berechtigungen“.
54
+
7
55
  ## [0.3.4] – 2026-07-05
8
56
 
9
57
  ### 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,299 @@ 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
+ // Robust CSRF-Token-Suche: erst das versteckte Feld des Formulars,
96
+ // dann Meta-Tag, dann globales window._sc_globalCsrf.
97
+ var csrfEl = form.querySelector('input[name="_csrf"]');
98
+ var csrf =
99
+ (csrfEl && csrfEl.value) ||
100
+ ((document.querySelector('meta[name="csrf-token"]') || {}).content) ||
101
+ (window._sc_globalCsrf) ||
102
+ '';
103
+ btn.disabled = true;
104
+ var oldTxt = btn.textContent;
105
+ btn.textContent = 'Teste …';
106
+ out.innerHTML = '<div class="text-muted">Verbindung wird aufgebaut … (bis zu 30 s)</div>';
107
+ try {
108
+ // Als URL-encoded senden – so findet Saltcorns CSRF-Middleware das
109
+ // Token direkt im Body (req.body._csrf), unabhängig von der
110
+ // Header-Konfiguration. Zusätzlich das Token als Header schicken.
111
+ var params = new URLSearchParams();
112
+ params.set('_csrf', csrf);
113
+ Object.keys(payload).forEach(function(k){ params.set(k, payload[k] || ''); });
114
+ var r = await fetch('/sambatest', {
115
+ method: 'POST',
116
+ credentials: 'same-origin',
117
+ headers: {
118
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
119
+ 'Accept': 'application/json',
120
+ 'X-CSRF-Token': csrf,
121
+ 'CSRF-Token': csrf,
122
+ 'X-Requested-With': 'XMLHttpRequest'
123
+ },
124
+ body: params.toString()
125
+ });
126
+ // Defensiv: kommt HTML statt JSON zurück (z. B. Login-Redirect), Text anzeigen.
127
+ var raw = await r.text();
128
+ var data;
129
+ try { data = JSON.parse(raw); }
130
+ catch (parseErr) {
131
+ var short = raw.replace(/<[^>]+>/g,'').replace(/\s+/g,' ').trim().slice(0, 300);
132
+ out.innerHTML =
133
+ '<div class="alert alert-danger">' +
134
+ '<b>✗ Antwort war kein JSON</b> (HTTP ' + r.status + ')<br>' +
135
+ 'Mögliche Ursachen: nicht als Admin angemeldet (Login-Redirect), CSRF-Token ungültig, ' +
136
+ 'oder die Route <code>/sambatest</code> ist noch nicht registriert ' +
137
+ '(Plugin neu installieren bzw. Saltcorn neu starten).<br>' +
138
+ '<details style="margin-top:.4rem"><summary>Antwort des Servers</summary>' +
139
+ '<pre style="white-space:pre-wrap;margin-top:.4rem">' +
140
+ short.replace(/[<>&]/g, function(c){return {'<':'&lt;','>':'&gt;','&':'&amp;'}[c];}) +
141
+ '</pre></details>' +
142
+ '</div>';
143
+ return;
144
+ }
145
+ if (data && data.ok) {
146
+ var rows = (data.entries||[]).map(function(e){
147
+ return '<li>'+ (e.isDirectory?'📁 ':'📄 ') + String(e.name).replace(/[<>&]/g,'?') +'</li>';
148
+ }).join('');
149
+ out.innerHTML =
150
+ '<div class="alert alert-success">' +
151
+ '<b>✓ Verbindung erfolgreich</b> (' + data.duration_ms + ' ms)<br>' +
152
+ 'Server: <code>' + data.server + ':' + data.port + '</code>, Share: <code>' + data.share + '</code>, ' +
153
+ 'Basispfad: <code>' + data.base_path + '</code>, Benutzer: <code>' + data.username + '</code><br>' +
154
+ 'Einträge gefunden: <b>' + data.entry_count + '</b>' + (data.truncated ? ' (erste 20 unten)' : '') +
155
+ (rows ? '<ul style="margin:.5rem 0 0 1rem">' + rows + '</ul>' : '') +
156
+ '</div>';
157
+ } else {
158
+ var a = data && data.attempted || {};
159
+ out.innerHTML =
160
+ '<div class="alert alert-danger">' +
161
+ '<b>✗ Verbindung fehlgeschlagen</b><br>' +
162
+ 'Fehler: <code>' + String(data && data.error || 'Unbekannt').replace(/[<>&]/g,'?') + '</code>' +
163
+ (data && data.code ? ' <span class="text-muted">(' + data.code + ')</span>' : '') + '<br>' +
164
+ (data && data.hint ? '<div style="margin-top:.4rem"><b>Hinweis:</b> ' + String(data.hint).replace(/[<>&]/g,'?') + '</div>' : '') +
165
+ '<details style="margin-top:.4rem"><summary>Versuchte Verbindungsdaten</summary>' +
166
+ '<table class="table table-sm" style="margin-top:.4rem">' +
167
+ '<tr><td>Server</td><td><code>' + (a.server||'') + ':' + (a.port||'') + '</code></td></tr>' +
168
+ '<tr><td>Share</td><td><code>' + (a.share||'') + '</code></td></tr>' +
169
+ '<tr><td>Basispfad</td><td><code>' + (a.base_path||'') + '</code></td></tr>' +
170
+ '<tr><td>Domäne</td><td><code>' + (a.domain||'') + '</code></td></tr>' +
171
+ '<tr><td>Benutzer</td><td><code>' + (a.username||'') + '</code></td></tr>' +
172
+ '</table></details>' +
173
+ '</div>';
174
+ }
175
+ } catch (e) {
176
+ out.innerHTML = '<div class="alert alert-danger">Fehler beim Aufruf von /sambatest: ' +
177
+ String(e && e.message || e).replace(/[<>&]/g,'?') + '</div>';
178
+ } finally {
179
+ btn.disabled = false;
180
+ btn.textContent = oldTxt;
181
+ }
182
+ };
183
+ </script>
184
+ `;
185
+
57
186
  const configuration_workflow = () =>
58
187
  new Workflow({
59
188
  steps: [
60
189
  {
61
- name: "Samba server",
190
+ name: "Samba-Server",
62
191
  form: () =>
63
192
  new Form({
64
193
  fields: [
65
194
  new Field({
66
195
  name: "server",
67
- label: "Server",
68
- sublabel: "Hostname or IP of the Samba server (no smb:// prefix)",
196
+ label: "Server (Hostname oder IP)",
197
+ sublabel:
198
+ "Adresse des Samba-Servers <b>ohne</b> smb://-Präfix und ohne Backslashes. " +
199
+ "Beispiele: <code>192.168.1.20</code>, <code>fileserver.local</code>, <code>nas01</code>. " +
200
+ "Läuft Saltcorn in Docker, ist <code>localhost</code> → der Container selbst; nutzen Sie die LAN-IP " +
201
+ "des Host-Systems (nicht 127.0.0.1) oder verbinden Sie den Container ins gleiche Bridge/Host-Netzwerk. " +
202
+ "Der Hostname muss <b>aus Sicht des Saltcorn-Prozesses</b> auflösbar sein.",
69
203
  type: "String",
70
204
  required: true,
71
205
  }),
72
206
  new Field({
73
207
  name: "share",
74
- label: "Share name",
75
- sublabel: "The share to connect to (without slashes)",
208
+ label: "Freigabe / Share-Name",
209
+ sublabel:
210
+ "Name der SMB-Freigabe <b>ohne</b> Schrägstriche. Auf dem Server sichtbar als <code>[NAME]</code>-Abschnitt " +
211
+ "in <code>/etc/samba/smb.conf</code> oder unter Windows als Freigabe-Name. " +
212
+ "Beispiele: <code>daten</code>, <code>public</code>, <code>projekte</code>. " +
213
+ "Nicht die Ordner-Bezeichnung, sondern der Freigabe-Name eingeben.",
76
214
  type: "String",
77
215
  required: true,
78
216
  }),
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" }),
217
+ new Field({
218
+ name: "domain",
219
+ label: "Domäne / Arbeitsgruppe",
220
+ sublabel:
221
+ "Windows-Domäne oder Arbeitsgruppe des Benutzers. " +
222
+ "Für klassische Samba-Server im Heim-/Firmennetz meist <code>WORKGROUP</code> (Standard). " +
223
+ "Für Active Directory: NetBIOS-Name der Domäne, z. B. <code>CONTOSO</code>. Nicht der FQDN.",
224
+ type: "String",
225
+ default: "WORKGROUP",
226
+ }),
227
+ new Field({
228
+ name: "username",
229
+ label: "Benutzername",
230
+ sublabel:
231
+ "Samba-/AD-Benutzer, unter dem gelesen und (je nach Rechten) geschrieben wird. " +
232
+ "<b>Nicht</b> im Format <code>DOMÄNE\\user</code> – die Domäne gehört oben ins eigene Feld. " +
233
+ "Leer lassen für anonymen Zugriff (nur möglich, wenn der Share <code>guest ok = yes</code> erlaubt).",
234
+ type: "String",
235
+ }),
236
+ new Field({
237
+ name: "password",
238
+ label: "Passwort",
239
+ sublabel:
240
+ "Passwort des Samba-Benutzers. Wird verschlüsselt in der Saltcorn-Datenbank abgelegt. " +
241
+ "<b>Wichtig:</b> Samba nutzt ein eigenes Passwort (<code>smbpasswd</code>), nicht zwingend das Linux-Login-Passwort. " +
242
+ "Moderne Samba-Server lehnen leere Passwörter ab.",
243
+ type: "String",
244
+ input_type: "password",
245
+ }),
82
246
  new Field({
83
247
  name: "base_path",
84
- label: "Base path",
85
- sublabel: "Optional. All access is restricted to this sub-directory of the share.",
248
+ label: "Basispfad (optional)",
249
+ sublabel:
250
+ "Beschränkt sämtlichen Zugriff auf ein Unterverzeichnis der Freigabe. " +
251
+ "Relativ, mit Schrägstrichen, <b>ohne</b> führenden Slash. " +
252
+ "Beispiel: <code>projekte/2026</code> → der File-Manager sieht nur diesen Unterordner und alles darin. " +
253
+ "Leer lassen für die komplette Share-Wurzel. <code>..</code> und absolute Pfade werden abgelehnt.",
86
254
  type: "String",
87
255
  }),
88
- new Field({ name: "port", label: "Port", type: "Integer", default: 445 }),
256
+ new Field({
257
+ name: "port",
258
+ label: "TCP-Port",
259
+ sublabel:
260
+ "SMB-Port des Servers. Standard: <code>445</code> (SMB2/3 über TCP). " +
261
+ "Nur ändern, wenn Ihr Server bewusst auf einem anderen Port läuft. " +
262
+ "<b>SMBv1 (Port 139/NetBIOS) wird nicht unterstützt</b> – aktivieren Sie SMB2+ auf dem Server " +
263
+ "(<code>min protocol = SMB2</code> in smb.conf).",
264
+ type: "Integer",
265
+ default: 445,
266
+ }),
267
+ new Field({
268
+ name: "_test_html",
269
+ label: " ",
270
+ input_type: "custom_html",
271
+ attributes: { html: CONNECTION_TEST_HTML },
272
+ }),
89
273
  ],
90
274
  }),
91
275
  },
92
276
  {
93
- name: "Access & permissions",
277
+ name: "Zugriff & Berechtigungen",
94
278
  form: () =>
95
279
  new Form({
96
280
  fields: [
97
281
  new Field({
98
282
  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.",
283
+ label: "Mindestrolle für Lesen",
284
+ sublabel:
285
+ "Saltcorn-Rolle, die mindestens nötig ist, um Dateien und Verzeichnisse anzusehen. " +
286
+ "Werte: <code>1</code>=Admin, <code>40</code>=Staff, <code>80</code>=User, <code>100</code>=Public. " +
287
+ "Kleinere Zahl = höhere Rolle. Standard: <code>80</code> (alle angemeldeten Benutzer).",
101
288
  type: "Integer",
102
289
  default: 80,
103
290
  }),
104
291
  new Field({
105
292
  name: "min_role_write",
106
- label: "Minimum role to upload / delete / rename",
293
+ label: "Mindestrolle für Upload / Löschen / Umbenennen",
107
294
  sublabel:
108
- "Set to 100 to disable all write actions completely. Default: 40 (staff and admins).",
295
+ "Setzen Sie auf <code>100</code>, um <b>alle</b> Schreibaktionen komplett zu deaktivieren auch wenn die " +
296
+ "Checkboxen unten aktiv sind. Standard: <code>40</code> (nur Staff und Admin).",
109
297
  type: "Integer",
110
298
  default: 40,
111
299
  }),
112
300
  new Field({
113
301
  name: "allow_upload",
114
- label: "Allow file upload",
302
+ label: "Datei-Upload erlauben",
303
+ sublabel: "Zeigt den Upload-Button und aktiviert POST /sambaupload.",
115
304
  type: "Bool",
116
305
  default: false,
117
306
  }),
118
307
  new Field({
119
308
  name: "allow_delete",
120
- label: "Allow delete",
309
+ label: "Löschen erlauben",
310
+ sublabel: "Erlaubt das Löschen von Dateien und leeren Verzeichnissen.",
121
311
  type: "Bool",
122
312
  default: false,
123
313
  }),
124
314
  new Field({
125
315
  name: "allow_rename",
126
- label: "Allow rename / move",
316
+ label: "Umbenennen / Verschieben erlauben",
317
+ sublabel: "Erlaubt Umbenennen und Verschieben innerhalb des Basispfads.",
127
318
  type: "Bool",
128
319
  default: false,
129
320
  }),
130
321
  new Field({
131
322
  name: "allow_mkdir",
132
- label: "Allow creating new directories",
323
+ label: "Neue Verzeichnisse anlegen erlauben",
324
+ sublabel: "Zeigt den „Neuer Ordner“-Button in der File-Manager-Ansicht.",
133
325
  type: "Bool",
134
326
  default: false,
135
327
  }),
136
328
  new Field({
137
329
  name: "max_upload_mb",
138
- label: "Max upload size per file (MiB)",
330
+ label: "Max. Upload-Größe pro Datei (MiB)",
331
+ sublabel: "Serverseitige Obergrenze pro Datei. Standard: 50 MiB.",
139
332
  type: "Integer",
140
333
  default: 50,
141
334
  }),
142
335
  new Field({
143
336
  name: "denied_extensions",
144
- label: "Blocked file extensions (comma-separated, no dots)",
337
+ label: "Gesperrte Datei-Endungen (komma-getrennt, ohne Punkte)",
145
338
  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.",
339
+ "Standard: <code>exe,bat,cmd,com,msi,scr,vbs,js,jse,wsf,wsh,ps1,ps1xml,psm1,sh,bash,zsh</code>. " +
340
+ "Leer lassen für die Standard-Liste.",
147
341
  type: "String",
148
342
  }),
149
343
  new Field({
150
344
  name: "public_smb_host",
151
- label: "SMB host visible to clients",
345
+ label: "Für Clients sichtbarer SMB-Hostname (optional)",
152
346
  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.",
347
+ "Wird in erzeugten <code>smb://</code>-Links verwendet. Standard: der Wert aus dem Feld <b>Server</b>. " +
348
+ "Nützlich, wenn Saltcorn in Docker läuft (Server = interne IP), Clients aber den LAN-Namen sehen sollen " +
349
+ "(z. B. <code>fileserver.local</code>).",
154
350
  type: "String",
155
351
  }),
156
352
  ],
@@ -369,6 +565,118 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
369
565
  },
370
566
  },
371
567
 
568
+ // ---- Connection test (admin only) --------------------------------------
569
+ //
570
+ // POST /sambatest
571
+ // Body (JSON): { server, share, domain, username, password, base_path, port }
572
+ // Returns: { ok, server, share, base_path, entries?: [{name,isDirectory}],
573
+ // error?, code?, hint? }
574
+ //
575
+ // The route accepts values straight from the config form so admins can
576
+ // verify the connection BEFORE saving. It never touches the persisted
577
+ // configuration and is restricted to role_id === 1 (admin).
578
+ {
579
+ url: "/sambatest",
580
+ method: "post",
581
+ // The route is protected by an explicit admin check in the handler
582
+ // (roleOf(req) !== 1 returns 403), so the standard CSRF middleware
583
+ // would only add friction without adding security here — the token
584
+ // hidden field is not in scope of an arbitrary attacker who cannot
585
+ // already run scripts inside an admin's browser (which would defeat
586
+ // any web-app anyway). Setting noCsrf keeps the button working even
587
+ // when the config-page CSRF token has already been consumed by the
588
+ // save-workflow or when a stricter CSRF policy is applied.
589
+ noCsrf: true,
590
+ callback: async ({ req, res }) => {
591
+ if (roleOf(req) !== 1) {
592
+ return jsonError(res, 403, "Only admins can test the connection.");
593
+ }
594
+ // Accept both JSON and URL-encoded bodies. express.json and
595
+ // express.urlencoded are both installed globally by Saltcorn.
596
+ const body = (req.body && typeof req.body === "object") ? req.body : {};
597
+ const testCfg = {
598
+ server: String(body.server || "").trim(),
599
+ share: String(body.share || "").trim(),
600
+ domain: String(body.domain || "").trim() || "WORKGROUP",
601
+ username: String(body.username || "").trim(),
602
+ password: String(body.password || ""),
603
+ base_path: String(body.base_path || "").trim(),
604
+ port: Number(body.port) || 445,
605
+ };
606
+
607
+ if (!testCfg.server) return jsonError(res, 400, "Please enter a Server (hostname or IP).");
608
+ if (!testCfg.share) return jsonError(res, 400, "Please enter a Share name.");
609
+
610
+ const started = Date.now();
611
+ try {
612
+ const listing = await withClient(testCfg, async (client) => {
613
+ // If base_path is set, list it — that also verifies traversal.
614
+ const rel = testCfg.base_path ? sanitizeRelativePath(testCfg.base_path) : "";
615
+ return await client.readdir(rel);
616
+ });
617
+ const took = Date.now() - started;
618
+ return res.json({
619
+ ok: true,
620
+ server: testCfg.server,
621
+ share: testCfg.share,
622
+ base_path: testCfg.base_path || "(share root)",
623
+ port: testCfg.port,
624
+ domain: testCfg.domain,
625
+ username: testCfg.username || "(anonymous)",
626
+ duration_ms: took,
627
+ entry_count: Array.isArray(listing) ? listing.length : 0,
628
+ entries: (Array.isArray(listing) ? listing : []).slice(0, 20).map((e) => ({
629
+ name: e && (e.name || e),
630
+ isDirectory: !!(e && (e.isDirectory === true || (e.stats && e.stats.isDirectory && e.stats.isDirectory()))),
631
+ })),
632
+ truncated: Array.isArray(listing) && listing.length > 20,
633
+ });
634
+ } catch (err) {
635
+ // Turn opaque SMB / socket errors into actionable hints.
636
+ const msg = String((err && err.message) || err || "Unknown error");
637
+ const code = (err && (err.code || err.errno)) || null;
638
+ let hint = null;
639
+ const m = msg.toLowerCase();
640
+ if (code === "ECONNREFUSED" || m.includes("econnrefused"))
641
+ hint = "The server refused the connection on port " + testCfg.port +
642
+ ". Check that Samba is running and that a firewall (or Docker) does not block TCP/445.";
643
+ else if (code === "ETIMEDOUT" || m.includes("etimedout") || m.includes("timed out"))
644
+ 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 + ").";
645
+ else if (code === "ENOTFOUND" || code === "EAI_AGAIN" || m.includes("getaddrinfo"))
646
+ hint = "DNS lookup failed for '" + testCfg.server + "'. Use an IP address or make sure the hostname resolves from inside the Saltcorn container.";
647
+ else if (code === "EHOSTUNREACH" || m.includes("ehostunreach"))
648
+ hint = "No route to host. Check network / VPN / bridged Docker network.";
649
+ else if (m.includes("logon_failure") || m.includes("nt_status_logon_failure") || m.includes("access_denied") || m.includes("nt_status_access_denied"))
650
+ hint = "Login rejected. Check Username, Password and Domain / Workgroup. On modern Samba servers, empty passwords are usually disabled.";
651
+ else if (m.includes("bad_network_name") || m.includes("nt_status_bad_network_name"))
652
+ hint = "The share '" + testCfg.share + "' does not exist on " + testCfg.server + ". Check spelling and the [share] section in smb.conf.";
653
+ else if (m.includes("account_disabled") || m.includes("account_locked"))
654
+ hint = "The user account is disabled or locked on the Samba server.";
655
+ else if (m.includes("password_expired") || m.includes("password_must_change"))
656
+ hint = "The password must be changed before this account can be used.";
657
+ else if (m.includes("smb1") || m.includes("protocol"))
658
+ 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).";
659
+ else if (m.includes("traversal") || m.includes("path"))
660
+ 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 '..'.";
661
+
662
+ return res.status(200).json({
663
+ ok: false,
664
+ error: msg,
665
+ code,
666
+ hint,
667
+ attempted: {
668
+ server: testCfg.server,
669
+ share: testCfg.share,
670
+ port: testCfg.port,
671
+ domain: testCfg.domain,
672
+ username: testCfg.username || "(anonymous)",
673
+ base_path: testCfg.base_path || "(share root)",
674
+ },
675
+ });
676
+ }
677
+ },
678
+ },
679
+
372
680
  // ---- Write routes (v0.3.0) --------------------------------------------
373
681
 
374
682
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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": {