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 1817bddc6c
commit ab684301a5
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):
try:
EMAIL_PATH.parent.mkdir(parents=True, exist_ok=True)
# Extract password before writing yaml
password = payload.pop("password", None) or payload.pop("password_set", None)
# Only store if it's a real new value (not the sentinel True/False)
# Extract password before writing yaml; discard the sentinel boolean regardless
password = payload.pop("password", None)
payload.pop("password_set", None) # always discard — boolean sentinel, not a secret
if password and isinstance(password, str):
set_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY, password)
# 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")
def test_email(payload: dict):
try:
# Allow test to pass in a password override, or use stored credential
password = payload.get("password") or get_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY)
# Always use the stored credential — never accept a password in the test request body
password = get_credential(EMAIL_CRED_SERVICE, EMAIL_CRED_KEY)
host = payload.get("host", "")
port = int(payload.get("port", 993))
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_]*)\}$')
CRED_DIR = Path("config/credentials")
KEY_PATH = Path("config/.credential_key")
_PROJECT_ROOT = Path(__file__).parent.parent
CRED_DIR = _PROJECT_ROOT / "config" / "credentials"
KEY_PATH = _PROJECT_ROOT / "config" / ".credential_key"
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 filePathsSaving = 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() {
loadError.value = null
@ -158,11 +162,12 @@ export const useSystemStore = defineStore('settings/system', () => {
`/api/settings/system/integrations/${id}/connect`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }
)
if (error || !data?.ok) {
return { ok: false, error: data?.error ?? 'Connection failed' }
}
await loadIntegrations()
return { ok: true }
const result = error || !data?.ok
? { ok: false, error: data?.error ?? 'Connection failed' }
: { ok: true }
integrationResults.value = { ...integrationResults.value, [id]: result }
if (result.ok) await loadIntegrations()
return result
}
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`,
{ 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) {
@ -206,7 +213,7 @@ export const useSystemStore = defineStore('settings/system', () => {
body: JSON.stringify(filePaths.value),
})
filePathsSaving.value = false
if (error) saveError.value = 'Failed to save file paths.'
filePathsError.value = error ? 'Failed to save file paths.' : null
}
async function loadDeployConfig() {
@ -222,14 +229,14 @@ export const useSystemStore = defineStore('settings/system', () => {
body: JSON.stringify(deployConfig.value),
})
deploySaving.value = false
if (error) saveError.value = 'Failed to save deployment config.'
deployError.value = error ? 'Failed to save deployment config.' : null
}
return {
backends, byokAcknowledged, byokPending, saving, saveError, loadError,
loadLlm, trySave, confirmByok, cancelByok,
services, emailConfig, integrations, serviceErrors, emailSaving, emailError,
filePaths, deployConfig, filePathsSaving, deploySaving,
services, emailConfig, integrations, integrationResults, serviceErrors, emailSaving, emailError,
filePaths, deployConfig, filePathsSaving, deploySaving, filePathsError, deployError,
loadServices, startService, stopService,
loadEmail, saveEmail, testEmail, saveEmailWithPassword,
loadIntegrations, connectIntegration, testIntegration, disconnectIntegration,

View file

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