diff --git a/apps/api/app/main.py b/apps/api/app/main.py index 9e7a71f..9c53d98 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -1360,7 +1360,15 @@ class ApiHandler(BaseHTTPRequestHandler): def update_source(self,sid,payload,db,user): source=db.execute("SELECT * FROM sources WHERE id=? AND organization_id=?",(sid,user['organization_id'])).fetchone() if not source:return self.send_json(404,{"error":"not_found"}) - if 'enabled' not in payload:return self.send_json(400,{"error":"enabled_required"}) + if 'config' in payload: + config=payload.get('config') + if not isinstance(config,dict) or contains_secret(config): return self.send_json(400,{"error":"invalid_source_config"}) + adapter=adapter_for(source['kind']); validation=adapter.validate_config(config) + if not validation.valid: return self.send_json(400,{"error":"invalid_source_config","details":validation.errors}) + db.execute("UPDATE sources SET config_json=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND organization_id=?",(json.dumps(config,sort_keys=True),sid,user['organization_id'])) + self.audit(db,user,'source.configured',str(sid)); db.commit() + if 'enabled' not in payload: + row=db.execute("SELECT * FROM sources WHERE id=?",(sid,)).fetchone(); return self.send_json(200,row_json(row)) value=int(bool(payload['enabled'])) if value: adapter=adapter_for(source['kind']) diff --git a/apps/api/tests/test_sources_phase5.py b/apps/api/tests/test_sources_phase5.py index 2a43c51..a30964c 100644 --- a/apps/api/tests/test_sources_phase5.py +++ b/apps/api/tests/test_sources_phase5.py @@ -74,6 +74,13 @@ class SourceApiTests(unittest.TestCase): self.assertEqual(status, 201) self.assertEqual(self.req('PATCH', f"/api/v1/sources/{website['id']}", {'enabled': True})[0], 409) + def test_source_configuration_can_be_saved_before_enablement(self): + status, source = self.req('POST', '/api/v1/sources', {'name': 'DNS', 'kind': 'dns', 'config': {}}) + self.assertEqual(status, 201) + status, configured = self.req('PATCH', f"/api/v1/sources/{source['id']}", {'config': {'domains': ['example.co.za']}}) + self.assertEqual(status, 200) + self.assertEqual(self.req('PATCH', f"/api/v1/sources/{source['id']}", {'enabled': True})[0], 200) + def test_fresh_schema_accepts_optional_source_kind_fail_closed(self): status, source = self.req('POST', '/api/v1/sources', {'name': 'RDAP', 'kind': 'rdap', 'config': {}}) self.assertEqual(status, 201) diff --git a/apps/web/app.js b/apps/web/app.js index 3cf5925..316cb5c 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -339,6 +339,24 @@ $('aiProviderForm').addEventListener('submit',saveAiProviderSettings);$('aiProvi if (badge) badge.textContent = 'Public and operator-controlled sources'; if (copy) copy.textContent = 'Register bounded public sources or operator-controlled imports. New sources start disabled and must be tested before use.'; } + async function configureSourceFromUi(source, button) { + const type = sourceType(source); + const label = type === 'public_website' ? 'Enter a public website URL' : type === 'dns' ? 'Enter a domain for DNS lookup' : type === 'rdap' ? 'Enter a domain for RDAP lookup' : type === 'ct_logs' ? 'Enter a domain for certificate-transparency lookup' : 'Enter source configuration'; + const value = window.prompt(label + ':'); + if (!value || !value.trim()) return; + const text = value.trim(); + const config = type === 'public_website' ? {urls:[text]} : type === 'dns' ? {domains:[text]} : {domain:text}; + button.disabled = true; + try { await jsonRequest(`/api/v1/sources/${encodeURIComponent(source.id)}`, {method:'PATCH', headers:{'Content-Type':'application/json'}, body:JSON.stringify({config})}); sourceMessage('Source configuration saved. Test it before enabling.'); await loadSources(); } + catch (error) { if (error.message !== 'unauthorized') sourceMessage(error.message || 'Unable to configure source.', true); } + finally { button.disabled = false; } + } + $('sourcesList')?.addEventListener('click', event => { + const actionButton = event.target.closest?.('[data-source-action="toggle"]'); + if (!actionButton) return; + const source = sources.find(item => String(item.id) === String(actionButton.dataset.sourceId)); + if (source && !source.configured && source.available) { event.preventDefault(); event.stopImmediatePropagation(); configureSourceFromUi(source, actionButton); } + }, true); document.querySelectorAll('.sidebar nav a, [data-scroll]').forEach(link => link.addEventListener('click', event => { const target = (link.getAttribute('href') || link.dataset.scroll || '').replace(/^#/, ''); const view = viewMap[target]; if (view) { event.preventDefault(); activateView(view); document.querySelector('.sidebar')?.classList.remove('open'); } })); bootstrap(); })(); diff --git a/apps/web/asset-manifest.json b/apps/web/asset-manifest.json index 975d9f7..d5984aa 100644 --- a/apps/web/asset-manifest.json +++ b/apps/web/asset-manifest.json @@ -1,6 +1,6 @@ { "schema": 1, - "version": "phase-21", + "version": "phase-22", "entrypoints": [ "config.js", "app.js", @@ -13,10 +13,10 @@ "healthz" ], "integrity": { - "config.js": "sha256-247380b078f895f025229d42c4f680285a237c498c248cd721f32938ec2398d6", - "app.js": "sha256-8927dd32aa1b79eb9cfa5f58e8ab100927dc3a839a1036774540e475c74626dc", + "config.js": "sha256-734a7d93ee125a12ba355206e4e789c6a8e942f0955ad3abcaf604119f1b8b63", + "app.js": "sha256-9073461507ae2e96eb1d62c62d0cc1103bbc427c17f7f42ae7c6f2c52528a87b", "styles.css": "sha256-a9fd194b001c0de98775bcf5fbba5da676ccfdd607a889eae84bd30a54c15e08", - "index.html": "sha256-0a010f373cc15ff43d8bf12a5c6cd2eaeb5e8072bad94bf698f4586c2dabc219", + "index.html": "sha256-9f6838c02890eff9b83e32177af01b56c0423758edf9411bc8f196cb61510b5e", "health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81", "error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf", "healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22" diff --git a/apps/web/config.js b/apps/web/config.js index c17c4fe..33685bb 100644 --- a/apps/web/config.js +++ b/apps/web/config.js @@ -1,5 +1,5 @@ /* Public, non-secret runtime configuration. Replace this file at deploy time if needed. */ window.__PROSPECT_CONFIG__ = Object.freeze({ apiBase: '', - assetVersion: 'phase-21' + assetVersion: 'phase-22' }); diff --git a/apps/web/index.html b/apps/web/index.html index 47a024b..96ad828 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,7 +5,7 @@ ProspectOS · Pipeline intelligence - +
@@ -130,7 +130,7 @@ - - + +