Step 7.5a: Spotify client credentials in config/UI, write-only secrets
- Core config: cfg_mark_secret(group, key). GET /api/config returns ""
for secret keys; a POST with "" keeps the stored value, null clears it;
cfg_get() returns the real value. Documented in aes67-core-base.md.
- source.spotify_client_id / spotify_client_secret (secret write-only):
each user's own Spotify developer app, needed by the maintained cspot
fork (philippe44/cspot) since Spotify's 2025 API restrictions.
- UI: client ID field, password field for the secret ("leave empty to
keep"), link to developer.spotify.com; enabled for spotify/auto modes.
- status.spotify_state: "no client credentials" while either is missing.
- Verified: secret never appears in GET; UI-style re-save keeps it;
survives reboot; null clears it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#define MAX_GROUPS 12
|
||||
#define MAX_SECRETS 4
|
||||
#define MAX_BODY 8192
|
||||
#define NVS_NAMESPACE "cfg"
|
||||
|
||||
@@ -24,6 +25,8 @@ typedef struct {
|
||||
cJSON *values;
|
||||
cfg_validate_cb_t validate;
|
||||
cfg_apply_cb_t apply;
|
||||
char secrets[MAX_SECRETS][24]; // write-only keys
|
||||
int n_secrets;
|
||||
} cfg_group_t;
|
||||
|
||||
static cfg_group_t s_groups[MAX_GROUPS];
|
||||
@@ -220,6 +223,35 @@ esp_err_t cfg_register(const char *group, const char *defaults_json,
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t cfg_mark_secret(const char *group, const char *key)
|
||||
{
|
||||
if (!s_lock) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
cfg_group_t *g = find(group);
|
||||
esp_err_t err = ESP_OK;
|
||||
if (!g || !cJSON_IsString(cJSON_GetObjectItemCaseSensitive(g->defaults, key))) {
|
||||
err = ESP_ERR_NOT_FOUND;
|
||||
} else if (g->n_secrets >= MAX_SECRETS || strlen(key) >= sizeof(g->secrets[0])) {
|
||||
err = ESP_ERR_NO_MEM;
|
||||
} else {
|
||||
strcpy(g->secrets[g->n_secrets++], key);
|
||||
}
|
||||
xSemaphoreGive(s_lock);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Group values for the API: secrets blanked.
|
||||
static cJSON *public_values(const cfg_group_t *g)
|
||||
{
|
||||
cJSON *v = cJSON_Duplicate(g->values, true);
|
||||
for (int i = 0; i < g->n_secrets; i++) {
|
||||
cJSON_ReplaceItemInObjectCaseSensitive(v, g->secrets[i], cJSON_CreateString(""));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
cJSON *cfg_get(const char *group)
|
||||
{
|
||||
if (!s_lock) {
|
||||
@@ -270,7 +302,7 @@ static esp_err_t config_get(httpd_req_t *req)
|
||||
cJSON *all = cJSON_CreateObject();
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
for (int i = 0; i < s_group_n; i++) {
|
||||
cJSON_AddItemToObject(all, s_groups[i].name, cJSON_Duplicate(s_groups[i].values, true));
|
||||
cJSON_AddItemToObject(all, s_groups[i].name, public_values(&s_groups[i]));
|
||||
}
|
||||
xSemaphoreGive(s_lock);
|
||||
esp_err_t err = web_send_json(req, all);
|
||||
@@ -331,7 +363,18 @@ static esp_err_t config_post(httpd_req_t *req)
|
||||
break;
|
||||
}
|
||||
cand[i] = cJSON_Duplicate(g->values, true);
|
||||
ok = merge_known(cand[i], g->defaults, src, g->name, err, sizeof(err));
|
||||
// Secrets: "" = unchanged (the API never returns the stored value), null = clear.
|
||||
cJSON *in_g = cJSON_Duplicate(src, true);
|
||||
for (int k = 0; k < g->n_secrets; k++) {
|
||||
const cJSON *sv = cJSON_GetObjectItemCaseSensitive(in_g, g->secrets[k]);
|
||||
if (cJSON_IsString(sv) && !sv->valuestring[0]) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(in_g, g->secrets[k]);
|
||||
} else if (cJSON_IsNull(sv)) {
|
||||
cJSON_ReplaceItemInObjectCaseSensitive(in_g, g->secrets[k], cJSON_CreateString(""));
|
||||
}
|
||||
}
|
||||
ok = merge_known(cand[i], g->defaults, in_g, g->name, err, sizeof(err));
|
||||
cJSON_Delete(in_g);
|
||||
if (ok && g->validate) {
|
||||
char verr[96] = "";
|
||||
ok = g->validate(cand[i], verr, sizeof(verr));
|
||||
|
||||
Reference in New Issue
Block a user