fix: accept credentials in source config upsert (#117)

* fix: unified source config + credentials

Accept optional credentials in PUT /api/sources/:sourceId so the
dashboard can send config and credentials in a single request,
eliminating the race condition between parallel config/credential
updates that left sources uninitialized until server restart.

The existing /credentials endpoint is preserved for independent
credential updates.

Co-authored-by: Ona <no-reply@ona.com>

* refactor: rename upsertSourceConfig to saveSourceConfig

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-04-12 15:17:29 +01:00
committed by GitHub
parent b5236e0e52
commit e54c5d5462
6 changed files with 205 additions and 28 deletions

View File

@@ -20,13 +20,7 @@ import {
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import {
fetchSourceConfig,
pushLocation,
replaceSource,
updateProviderConfig,
updateSourceCredentials,
} from "@/lib/api"
import { fetchSourceConfig, pushLocation, replaceSource, updateProviderConfig } from "@/lib/api"
interface SourceConfigPanelProps {
source: SourceDefinition
@@ -80,23 +74,24 @@ export function SourceConfigPanel({ source, onUpdate }: SourceConfigPanelProps)
const saveMutation = useMutation({
mutationFn: async () => {
const promises: Promise<void>[] = [
replaceSource(source.id, { enabled, config: getUserConfig() }),
]
const credentialFields = getCredentialFields()
const hasCredentials = Object.values(credentialFields).some(
(v) => typeof v === "string" && v.length > 0,
)
if (hasCredentials) {
if (source.perUserCredentials) {
promises.push(updateSourceCredentials(source.id, credentialFields))
} else {
promises.push(updateProviderConfig(source.id, { credentials: credentialFields }))
}
}
await Promise.all(promises)
const body: Parameters<typeof replaceSource>[1] = {
enabled,
config: getUserConfig(),
}
if (hasCredentials && source.perUserCredentials) {
body.credentials = credentialFields
}
await replaceSource(source.id, body)
// For non-per-user credentials (provider-level), still use the admin endpoint.
if (hasCredentials && !source.perUserCredentials) {
await updateProviderConfig(source.id, { credentials: credentialFields })
}
},
onSuccess() {
setDirty({})

View File

@@ -114,7 +114,7 @@ const sourceDefinitions: SourceDefinition[] = [
timeZone: {
type: "string",
label: "Timezone",
description: "IANA timezone for determining \"today\" (e.g. Europe/London). Defaults to UTC.",
description: 'IANA timezone for determining "today" (e.g. Europe/London). Defaults to UTC.',
},
},
},
@@ -174,7 +174,7 @@ export async function fetchConfigs(): Promise<SourceConfig[]> {
export async function replaceSource(
sourceId: string,
body: { enabled: boolean; config: unknown },
body: { enabled: boolean; config: unknown; credentials?: Record<string, unknown> },
): Promise<void> {
const res = await fetch(`${serverBase()}/sources/${sourceId}`, {
method: "PUT",