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:
parent
f6ddaca14f
commit
a380ec33ec
4 changed files with 31 additions and 25 deletions
10
dev-api.py
10
dev-api.py
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue