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):
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue