fix(settings): task 6 review fixes — credential paths, email security, integrationResults in store

- Anchor CRED_DIR/KEY_PATH to __file__ (not CWD) in credential_store.py
- Fix email PUT: separate password pop from sentinel discard (was fragile or-chain)
- Fix email test: always use stored credential, remove password override path
- Move integrationResults into system store (was view-local — spec violation)
- saveFilePaths/saveDeployConfig write to dedicated error refs, not saveError
This commit is contained in:
pyr0ball 2026-03-22 15:46:47 -07:00
parent f6ddaca14f
commit a380ec33ec
4 changed files with 31 additions and 25 deletions

View file

@ -1293,9 +1293,9 @@ def get_email_config():
def save_email_config(payload: dict): def save_email_config(payload: dict):
try: try:
EMAIL_PATH.parent.mkdir(parents=True, exist_ok=True) EMAIL_PATH.parent.mkdir(parents=True, exist_ok=True)
# Extract password before writing yaml # Extract password before writing yaml; discard the sentinel boolean regardless
password = payload.pop("password", None) or payload.pop("password_set", None) password = payload.pop("password", None)
# Only store if it's a real new value (not the sentinel True/False) payload.pop("password_set", None) # always discard — boolean sentinel, not a secret
if password and isinstance(password, str): if password and isinstance(password, str):
set_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY, password) set_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY, password)
# Write non-secret fields to yaml (chmod 600 still, contains username) # Write non-secret fields to yaml (chmod 600 still, contains username)
@ -1311,8 +1311,8 @@ def save_email_config(payload: dict):
@app.post("/api/settings/system/email/test") @app.post("/api/settings/system/email/test")
def test_email(payload: dict): def test_email(payload: dict):
try: try:
# Allow test to pass in a password override, or use stored credential # Always use the stored credential — never accept a password in the test request body
password = payload.get("password") or get_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY) password = get_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY)
host = payload.get("host", "") host = payload.get("host", "")
port = int(payload.get("port", 993)) port = int(payload.get("port", 993))
use_ssl = payload.get("ssl", True) use_ssl = payload.get("ssl", True)

View file

@ -23,8 +23,9 @@ logger = logging.getLogger(__name__)
_ENV_REF = re.compile(r'^\$\{([A-Z_][A-Z0-9_]*)\}$') _ENV_REF = re.compile(r'^\$\{([A-Z_][A-Z0-9_]*)\}$')
CRED_DIR = Path("config/credentials") _PROJECT_ROOT = Path(__file__).parent.parent
KEY_PATH = Path("config/.credential_key") CRED_DIR = _PROJECT_ROOT / "config" / "credentials"
KEY_PATH = _PROJECT_ROOT / "config" / ".credential_key"
def _resolve_env_ref(value: str) -> Optional[str]: def _resolve_env_ref(value: str) -> Optional[str]:

View file

@ -30,6 +30,10 @@ export const useSystemStore = defineStore('settings/system', () => {
const deployConfig = ref<Record<string, unknown>>({}) const deployConfig = ref<Record<string, unknown>>({})
const filePathsSaving = ref(false) const filePathsSaving = ref(false)
const deploySaving = ref(false) const deploySaving = ref(false)
const filePathsError = ref<string | null>(null)
const deployError = ref<string | null>(null)
// Integration test/connect results — keyed by integration id
const integrationResults = ref<Record<string, {ok: boolean; error?: string}>>({})
async function loadLlm() { async function loadLlm() {
loadError.value = null loadError.value = null
@ -158,11 +162,12 @@ export const useSystemStore = defineStore('settings/system', () => {
`/api/settings/system/integrations/${id}/connect`, `/api/settings/system/integrations/${id}/connect`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) } { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }
) )
if (error || !data?.ok) { const result = error || !data?.ok
return { ok: false, error: data?.error ?? 'Connection failed' } ? { ok: false, error: data?.error ?? 'Connection failed' }
} : { ok: true }
await loadIntegrations() integrationResults.value = { ...integrationResults.value, [id]: result }
return { ok: true } if (result.ok) await loadIntegrations()
return result
} }
async function testIntegration(id: string, credentials: Record<string, string>) { async function testIntegration(id: string, credentials: Record<string, string>) {
@ -170,7 +175,9 @@ export const useSystemStore = defineStore('settings/system', () => {
`/api/settings/system/integrations/${id}/test`, `/api/settings/system/integrations/${id}/test`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) } { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }
) )
return { ok: data?.ok ?? false, error: data?.error ?? (error ? 'Test failed' : undefined) } const result = { ok: data?.ok ?? false, error: data?.error ?? (error ? 'Test failed' : undefined) }
integrationResults.value = { ...integrationResults.value, [id]: result }
return result
} }
async function disconnectIntegration(id: string) { async function disconnectIntegration(id: string) {
@ -206,7 +213,7 @@ export const useSystemStore = defineStore('settings/system', () => {
body: JSON.stringify(filePaths.value), body: JSON.stringify(filePaths.value),
}) })
filePathsSaving.value = false filePathsSaving.value = false
if (error) saveError.value = 'Failed to save file paths.' filePathsError.value = error ? 'Failed to save file paths.' : null
} }
async function loadDeployConfig() { async function loadDeployConfig() {
@ -222,14 +229,14 @@ export const useSystemStore = defineStore('settings/system', () => {
body: JSON.stringify(deployConfig.value), body: JSON.stringify(deployConfig.value),
}) })
deploySaving.value = false deploySaving.value = false
if (error) saveError.value = 'Failed to save deployment config.' deployError.value = error ? 'Failed to save deployment config.' : null
} }
return { return {
backends, byokAcknowledged, byokPending, saving, saveError, loadError, backends, byokAcknowledged, byokPending, saving, saveError, loadError,
loadLlm, trySave, confirmByok, cancelByok, loadLlm, trySave, confirmByok, cancelByok,
services, emailConfig, integrations, serviceErrors, emailSaving, emailError, services, emailConfig, integrations, integrationResults, serviceErrors, emailSaving, emailError,
filePaths, deployConfig, filePathsSaving, deploySaving, filePaths, deployConfig, filePathsSaving, deploySaving, filePathsError, deployError,
loadServices, startService, stopService, loadServices, startService, stopService,
loadEmail, saveEmail, testEmail, saveEmailWithPassword, loadEmail, saveEmail, testEmail, saveEmailWithPassword,
loadIntegrations, connectIntegration, testIntegration, disconnectIntegration, loadIntegrations, connectIntegration, testIntegration, disconnectIntegration,

View file

@ -144,8 +144,8 @@
<div class="form-actions"> <div class="form-actions">
<button @click="handleConnect(integration.id)" class="btn-primary">Connect</button> <button @click="handleConnect(integration.id)" class="btn-primary">Connect</button>
<button @click="handleTest(integration.id)" class="btn-secondary">Test</button> <button @click="handleTest(integration.id)" class="btn-secondary">Test</button>
<span v-if="integrationResults[integration.id]" :class="integrationResults[integration.id].ok ? 'test-ok' : 'test-fail'"> <span v-if="store.integrationResults[integration.id]" :class="store.integrationResults[integration.id].ok ? 'test-ok' : 'test-fail'">
{{ integrationResults[integration.id].ok ? '✓ OK' : '✗ ' + integrationResults[integration.id].error }} {{ store.integrationResults[integration.id].ok ? '✓ OK' : '✗ ' + store.integrationResults[integration.id].error }}
</span> </span>
</div> </div>
</div> </div>
@ -176,6 +176,7 @@
{{ store.filePathsSaving ? 'Saving…' : 'Save Paths' }} {{ store.filePathsSaving ? 'Saving…' : 'Save Paths' }}
</button> </button>
</div> </div>
<p v-if="store.filePathsError" class="error-msg">{{ store.filePathsError }}</p>
</section> </section>
<!-- Deployment / Server --> <!-- Deployment / Server -->
@ -199,6 +200,7 @@
{{ store.deploySaving ? 'Saving…' : 'Save (requires restart)' }} {{ store.deploySaving ? 'Saving…' : 'Save (requires restart)' }}
</button> </button>
</div> </div>
<p v-if="store.deployError" class="error-msg">{{ store.deployError }}</p>
</section> </section>
<!-- BYOK Modal --> <!-- BYOK Modal -->
@ -288,8 +290,6 @@ async function handleConfirmByok() {
const emailTestResult = ref<boolean | null>(null) const emailTestResult = ref<boolean | null>(null)
const emailPasswordInput = ref('') const emailPasswordInput = ref('')
const integrationInputs = ref<Record<string, string>>({}) const integrationInputs = ref<Record<string, string>>({})
const integrationResults = ref<Record<string, {ok: boolean; error?: string}>>({})
async function handleTestEmail() { async function handleTestEmail() {
const result = await store.testEmail() const result = await store.testEmail()
emailTestResult.value = result?.ok ?? false emailTestResult.value = result?.ok ?? false
@ -307,8 +307,7 @@ async function handleConnect(id: string) {
for (const field of integration.fields) { for (const field of integration.fields) {
credentials[field.key] = integrationInputs.value[`${id}:${field.key}`] ?? '' credentials[field.key] = integrationInputs.value[`${id}:${field.key}`] ?? ''
} }
const result = await store.connectIntegration(id, credentials) await store.connectIntegration(id, credentials)
integrationResults.value = { ...integrationResults.value, [id]: result }
} }
async function handleTest(id: string) { async function handleTest(id: string) {
@ -318,8 +317,7 @@ async function handleTest(id: string) {
for (const field of integration.fields) { for (const field of integration.fields) {
credentials[field.key] = integrationInputs.value[`${id}:${field.key}`] ?? '' credentials[field.key] = integrationInputs.value[`${id}:${field.key}`] ?? ''
} }
const result = await store.testIntegration(id, credentials) await store.testIntegration(id, credentials)
integrationResults.value = { ...integrationResults.value, [id]: result }
} }
onMounted(async () => { onMounted(async () => {