improve csv import handling
This commit is contained in:
@@ -365,6 +365,93 @@ curl http://minio.kindred.internal:9000/minio/health/live
|
||||
|
||||
---
|
||||
|
||||
## SSL/TLS with FreeIPA and Nginx
|
||||
|
||||
For production deployments, Silo should be served over HTTPS using nginx as a reverse proxy with certificates from FreeIPA.
|
||||
|
||||
### Setup IPA and Nginx
|
||||
|
||||
Run the IPA/nginx setup script after the basic host setup:
|
||||
|
||||
```bash
|
||||
sudo /opt/silo/src/scripts/setup-ipa-nginx.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Installs FreeIPA client and nginx
|
||||
2. Enrolls the host in FreeIPA domain
|
||||
3. Requests SSL certificate from IPA CA (auto-renewed by certmonger)
|
||||
4. Configures nginx as reverse proxy (HTTP → HTTPS redirect)
|
||||
5. Opens firewall ports 80 and 443
|
||||
|
||||
### Manual Steps After Script
|
||||
|
||||
1. Verify certificate was issued:
|
||||
```bash
|
||||
getcert list
|
||||
```
|
||||
|
||||
2. The silo config is already updated to use `https://silo.kindred.internal` as base URL. Restart silo:
|
||||
```bash
|
||||
sudo systemctl restart silod
|
||||
```
|
||||
|
||||
3. Test the setup:
|
||||
```bash
|
||||
curl https://silo.kindred.internal/health
|
||||
```
|
||||
|
||||
### Certificate Management
|
||||
|
||||
Certificates are automatically renewed by certmonger. Check status:
|
||||
|
||||
```bash
|
||||
# List all tracked certificates
|
||||
getcert list
|
||||
|
||||
# Check specific certificate
|
||||
getcert list -f /etc/ssl/silo/silo.crt
|
||||
|
||||
# Manual renewal if needed
|
||||
getcert resubmit -f /etc/ssl/silo/silo.crt
|
||||
```
|
||||
|
||||
### Trusting IPA CA on Clients
|
||||
|
||||
For clients to trust the Silo HTTPS certificate, they need the IPA CA:
|
||||
|
||||
```bash
|
||||
# Download CA cert
|
||||
curl -o /tmp/ipa-ca.crt https://ipa.kindred.internal/ipa/config/ca.crt
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo cp /tmp/ipa-ca.crt /usr/local/share/ca-certificates/ipa-ca.crt
|
||||
sudo update-ca-certificates
|
||||
|
||||
# RHEL/Fedora
|
||||
sudo cp /tmp/ipa-ca.crt /etc/pki/ca-trust/source/anchors/
|
||||
sudo update-ca-trust
|
||||
```
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
The nginx config is installed at `/etc/nginx/sites-available/silo`. Key settings:
|
||||
|
||||
- HTTP redirects to HTTPS
|
||||
- TLS 1.2/1.3 only with strong ciphers
|
||||
- Proxies to `127.0.0.1:8080` (silod)
|
||||
- 100MB max upload size for CAD files
|
||||
- Security headers (X-Frame-Options, etc.)
|
||||
|
||||
To modify:
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/silo
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] `/etc/silo/silod.env` has mode 600 (`chmod 600`)
|
||||
@@ -372,5 +459,8 @@ curl http://minio.kindred.internal:9000/minio/health/live
|
||||
- [ ] MinIO credentials are specific to silo (not admin)
|
||||
- [ ] SSL/TLS enabled for PostgreSQL (`sslmode: require`)
|
||||
- [ ] SSL/TLS enabled for MinIO (`use_ssl: true`) if available
|
||||
- [ ] Firewall restricts access to port 8080
|
||||
- [ ] HTTPS enabled via nginx reverse proxy
|
||||
- [ ] Silod listens on localhost only (`host: 127.0.0.1`)
|
||||
- [ ] Firewall allows only ports 80, 443 (not 8080)
|
||||
- [ ] Service runs as non-root `silo` user
|
||||
- [ ] Host enrolled in FreeIPA for centralized auth (future)
|
||||
|
||||
@@ -195,6 +195,7 @@ func (s *Server) HandleImportCSV(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get options
|
||||
dryRun := r.FormValue("dry_run") == "true"
|
||||
skipExisting := r.FormValue("skip_existing") == "true"
|
||||
schemaName := r.FormValue("schema")
|
||||
if schemaName == "" {
|
||||
schemaName = "kindred-rd"
|
||||
@@ -297,6 +298,10 @@ func (s *Server) HandleImportCSV(w http.ResponseWriter, r *http.Request) {
|
||||
if partNumber != "" {
|
||||
existing, _ := s.items.GetByPartNumber(ctx, partNumber)
|
||||
if existing != nil {
|
||||
if skipExisting {
|
||||
// Silently skip existing items
|
||||
continue
|
||||
}
|
||||
result.Errors = append(result.Errors, CSVImportErr{
|
||||
Row: rowNum,
|
||||
Field: "part_number",
|
||||
@@ -532,6 +537,8 @@ func isStandardColumn(col string) bool {
|
||||
"updated_at": true,
|
||||
"category": true,
|
||||
"projects": true,
|
||||
"objects": true, // FreeCAD objects data - skip on import
|
||||
"archived_at": true,
|
||||
}
|
||||
return standardCols[col]
|
||||
}
|
||||
|
||||
@@ -444,6 +444,15 @@
|
||||
<span>Dry run (validate without creating items)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="import-skip-existing" checked />
|
||||
<span
|
||||
>Skip existing part numbers (don't report as
|
||||
errors)</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
id="import-results"
|
||||
class="import-results"
|
||||
@@ -2138,6 +2147,9 @@
|
||||
|
||||
const fileInput = document.getElementById("import-file");
|
||||
const dryRun = document.getElementById("import-dry-run").checked;
|
||||
const skipExisting = document.getElementById(
|
||||
"import-skip-existing",
|
||||
).checked;
|
||||
const resultsDiv = document.getElementById("import-results");
|
||||
const importBtn = document.getElementById("import-btn");
|
||||
|
||||
@@ -2149,6 +2161,7 @@
|
||||
const formData = new FormData();
|
||||
formData.append("file", fileInput.files[0]);
|
||||
formData.append("dry_run", dryRun.toString());
|
||||
formData.append("skip_existing", skipExisting.toString());
|
||||
formData.append("schema", "kindred-rd");
|
||||
|
||||
importBtn.textContent = dryRun ? "Validating..." : "Importing...";
|
||||
@@ -2160,6 +2173,17 @@
|
||||
body: formData,
|
||||
});
|
||||
|
||||
// Check content type to handle non-JSON errors (e.g., nginx error pages)
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
const text = await response.text();
|
||||
console.error("Non-JSON response:", text);
|
||||
resultsDiv.innerHTML = `<h4>Error</h4><p>Server returned non-JSON response (status ${response.status}). Check server logs.</p><pre style="max-height:200px;overflow:auto;font-size:12px;">${text.substring(0, 500)}</pre>`;
|
||||
resultsDiv.className = "import-results error";
|
||||
resultsDiv.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user