#include "commands.h" #include "cmd_util.h" #include #include #include #include "audit.h" #include "config.h" #include "db.h" #include "secret.h" #include "util.h" /* ------------------------------------------------------------------ */ /* settings */ /* ------------------------------------------------------------------ */ static yyjson_mut_val *h_settings_get(struct req *r) { yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); int have_default = 0, have_bank = 0, have_receivable = 0, have_revenue = 0; int have_password = 0, have_security = 0; int have_voucher = 0, have_invoice = 0, have_payroll = 0; int have_bokslut = 0, have_ib = 0; char legacy_series[16] = "A"; sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( r->db, "SELECT key,value FROM settings WHERE org_id=?1", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, r->org_id); while (sqlite3_step(st) == SQLITE_ROW) { const char *k = (const char *)sqlite3_column_text(st, 0); const char *v = (const char *)sqlite3_column_text(st, 1); if (k) { if (secret_key_is_secret(k)) { have_password = 1; continue; } yyjson_mut_obj_add(o, yyjson_mut_strcpy(r->rdoc, k), yyjson_mut_strcpy(r->rdoc, v ? v : "")); if (strcmp(k, "default_series") == 0) { have_default = 1; if (v && *v) snprintf(legacy_series, sizeof legacy_series, "%s", v); } else if (strcmp(k, "series_voucher") == 0) { have_voucher = 1; } else if (strcmp(k, "series_invoice") == 0) { have_invoice = 1; } else if (strcmp(k, "series_payroll") == 0) { have_payroll = 1; } else if (strcmp(k, "series_bokslut") == 0) { have_bokslut = 1; } else if (strcmp(k, "series_ib") == 0) { have_ib = 1; } else if (strcmp(k, "bank_account") == 0) { have_bank = 1; } else if (strcmp(k, "invoice_receivable_account") == 0) { have_receivable = 1; } else if (strcmp(k, "invoice_revenue_account") == 0) { have_revenue = 1; } else if (strcmp(k, "smtp_security") == 0) { have_security = 1; } } } sqlite3_finalize(st); } if (!have_default) yyjson_mut_obj_add_strcpy(r->rdoc, o, "default_series", "A"); if (!have_voucher) yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_voucher", legacy_series); if (!have_invoice) yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_invoice", "F"); if (!have_payroll) yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_payroll", "L"); if (!have_bokslut) yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_bokslut", "Å"); if (!have_ib) yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_ib", "IB"); if (!have_bank) yyjson_mut_obj_add_strcpy(r->rdoc, o, "bank_account", "1930"); if (!have_receivable) yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_receivable_account", "1510"); if (!have_revenue) yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_revenue_account", "3001"); if (!have_security) yyjson_mut_obj_add_strcpy(r->rdoc, o, "smtp_security", "starttls"); yyjson_mut_obj_add_bool(r->rdoc, o, "smtp_password_set", have_password); return o; } static yyjson_mut_val *settings_result(struct req *r, const char *key, const char *value, int dry) { yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key); yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value); if (dry) yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); return o; } static char *settings_secret_audit_json(const char *key) { yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, o); yyjson_mut_obj_add_strcpy(doc, o, "key", key); yyjson_mut_obj_add_strcpy(doc, o, "value", "[redacted]"); char *json = yyjson_mut_write(doc, 0, NULL); yyjson_mut_doc_free(doc); return json ? json : xstrdup("{}"); } static yyjson_mut_val *h_settings_set(struct req *r) { const char *key = arg_str(r->args, "key"); const char *value = arg_str(r->args, "value"); if (!key || !value) return fail(r, "INVALID_ARGS", "key and value are required"); if (secret_key_is_secret(key)) { if (!secret_available()) return fail(r, "INTERNAL", "BOKFD_SECRET_KEY is missing or invalid"); char *stored = NULL; if (*value && secret_encrypt(value, &stored) != 0) return fail(r, "INTERNAL", "BOKFD_SECRET_KEY is missing or invalid"); if (r->dry_run) { free(stored); return settings_result(r, key, "[redacted]", 1); } sqlite3_stmt *st = NULL; const char *sql = *value ? "INSERT INTO settings(org_id,key,value)" " VALUES(?1,?2,?3)" " ON CONFLICT(org_id,key) DO UPDATE SET" " value=excluded.value" : "DELETE FROM settings WHERE org_id=?1" " AND key=?2"; if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) { free(stored); return db_error(r); } sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT); if (*value) sqlite3_bind_text(st, 3, stored, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); free(stored); if (rc != SQLITE_DONE) return db_sqlite_error(r); char *reqjson = settings_secret_audit_json(key); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "settings.set", reqjson, "OK", NULL); free(reqjson); return settings_result(r, key, "[redacted]", 0); } size_t maxlen; int digits_only = 0, bankgiro = 0, port = 0, security = 0; if (strcmp(key, "default_series") == 0 || strcmp(key, "series_voucher") == 0 || strcmp(key, "series_invoice") == 0 || strcmp(key, "series_payroll") == 0 || strcmp(key, "series_bokslut") == 0 || strcmp(key, "series_ib") == 0) maxlen = 8; else if (strcmp(key, "attachment_dir") == 0 || strcmp(key, "smtp_host") == 0 || strcmp(key, "smtp_user") == 0) maxlen = 255; else if (strcmp(key, "smtp_from") == 0 || strcmp(key, "smtp_reply_to") == 0) maxlen = 254; else if (strcmp(key, "smtp_port") == 0) { maxlen = 5; port = 1; } else if (strcmp(key, "smtp_security") == 0) { maxlen = 8; security = 1; } else if (strcmp(key, "bank_account") == 0 || strcmp(key, "invoice_receivable_account") == 0 || strcmp(key, "invoice_revenue_account") == 0) { maxlen = 10; digits_only = 1; } else if (strcmp(key, "invoice_bankgiro") == 0) { maxlen = 16; bankgiro = 1; } else if (strcmp(key, "invoice_our_ref") == 0) { maxlen = 64; } else return fail(r, "UNSUPPORTED", "unknown setting"); size_t len = strlen(value); if (len == 0 || len > maxlen) return failf(r, "INVALID_ARGS", "%s must be 1-%zu characters", key, maxlen); for (const char *p = value; *p; p++) { if ((digits_only || port) && (*p < '0' || *p > '9')) return failf(r, "INVALID_ARGS", "%s must be digits only", key); if (bankgiro && !((*p >= '0' && *p <= '9') || *p == '-')) return failf(r, "INVALID_ARGS", "%s must be digits and hyphens", key); if ((unsigned char)*p < 32) return failf(r, "INVALID_ARGS", "%s must not contain control characters", key); } if (port) { long v = strtol(value, NULL, 10); if (v < 1 || v > 65535) return failf(r, "INVALID_ARGS", "%s must be 1-65535", key); } if (security && strcmp(value, "starttls") != 0 && strcmp(value, "tls") != 0 && strcmp(value, "plain") != 0) return failf(r, "INVALID_ARGS", "%s must be starttls, tls or plain", key); if (r->dry_run) return settings_result(r, key, value, 1); sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( r->db, "INSERT INTO settings(org_id,key,value) VALUES(?1,?2,?3)" " ON CONFLICT(org_id,key) DO UPDATE SET value=excluded.value", -1, &st, NULL) != SQLITE_OK) return db_error(r); sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, value, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) return db_sqlite_error(r); char *reqjson = audit_args_json(r->args); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "settings.set", reqjson, "OK", NULL); free(reqjson); yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key); yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value); return o; } static const struct cmd_arg args_settings_set[] = { { "key", ARG_STR, 1, NULL, NULL, "default_series, attachment_dir, bank_account," " invoice_receivable_account, invoice_revenue_account, smtp_host," " smtp_port, smtp_user, smtp_from, smtp_reply_to, smtp_security or" " smtp_password" }, { "value", ARG_STR, 0, NULL, NULL, "Setting value; an empty value clears smtp_password" }, }; const struct command g_cmd_settings[] = { { "settings.get", "Read org settings", PERM_READ, 1, 0, 0, h_settings_get, NULL, 0 }, { "settings.set", "Change an org setting", PERM_WRITE, 1, 1, 1, h_settings_set, CMD_ARGS(args_settings_set) }, }; const struct cmd_table g_cmd_table_settings = { g_cmd_settings, sizeof g_cmd_settings / sizeof g_cmd_settings[0] };