#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "tui.h" #include "formula.h" #include "util.h" #include "version.h" #include "yyjson.h" #include "ui.h" /* Screen names for ^R restoration and --screen. Only the top-level views; sub-screens unwind to their parent. */ struct scene { const char *name; void (*fn)(struct app *); }; static const struct scene SCENES[] = { { "dashboard", dashboard }, { "vouchers", vouchers_screen }, { "inbox", inbox_screen }, { "bank", bank_screen }, { "invoices", invoices_screen }, { "customers", customers_screen }, { "payroll", payroll_screen }, { "employees", employees_screen }, { "tax_tables", payroll_tax_tables_screen }, { "ib", ib_screen }, { "templates", templates_screen }, { "rules", rules_screen }, { "reports", reports_screen }, { "audit", audit_screen }, { "company", company_screen }, { "settings", company_screen }, /* ^R/--screen compatibility */ { "system", system_screen }, { "bokslut", bokslut_screen }, { "yearinfo", bokslut_screen }, /* ^R compatibility */ }; void open_scene(struct app *a, const char *name) { snprintf(g_scene, sizeof g_scene, "%s", name); for (size_t i = 0; i < sizeof SCENES / sizeof SCENES[0]; i++) { if (strcmp(name, SCENES[i].name) == 0) { SCENES[i].fn(a); return; } } } static void do_reload(struct app *a, const char *self) { char orgbuf[32], fybuf[32]; char *argv[12]; int n = 0; snprintf(orgbuf, sizeof orgbuf, "%lld", (long long)a->org); snprintf(fybuf, sizeof fybuf, "%lld", (long long)a->fy); argv[n++] = (char *)(self && *self ? self : "bokftui"); argv[n++] = "--socket"; argv[n++] = a->socket; argv[n++] = "--org"; argv[n++] = orgbuf; if (a->fy > 0) { argv[n++] = "--fy"; argv[n++] = fybuf; } argv[n++] = "--screen"; argv[n++] = g_reload_scene[0] ? g_reload_scene : g_scene; argv[n] = NULL; setenv("BOKFD_SESSION", a->session, 1); if (a->password[0]) setenv("BOKFD_PASSWORD", a->password, 1); if (a->username[0]) setenv("BOKFD_USER", a->username, 1); tui_log("reload: screen=%s org=%lld fy=%lld", g_reload_scene, (long long)a->org, (long long)a->fy); endwin(); execvp(argv[0], argv); fprintf(stderr, "bokftui: kunde inte ladda om: %s\n", strerror(errno)); } /* ------------------------------------------------------------------ */ /* login */ /* ------------------------------------------------------------------ */ /* The login screen remembers server and user (never the password) in $XDG_CONFIG_HOME/bokf/tui.conf, defaulting to ~/.config/bokf/tui.conf. */ static void config_path(char *buf, size_t n) { const char *xdg = getenv("XDG_CONFIG_HOME"); if (xdg && *xdg) { snprintf(buf, n, "%s/bokf/tui.conf", xdg); return; } const char *home = getenv("HOME"); snprintf(buf, n, "%s/.config/bokf/tui.conf", home && *home ? home : "."); } void config_mkdirs(const char *path) { char tmp[640]; snprintf(tmp, sizeof tmp, "%s", path); if (strlen(tmp) >= sizeof tmp) return; for (char *p = tmp + 1; *p; p++) { if (*p != '/') continue; *p = '\0'; mkdir(tmp, 0700); *p = '/'; } } static void config_load(struct app *a) { char path[512]; config_path(path, sizeof path); FILE *f = fopen(path, "r"); if (!f) return; char line[512]; while (fgets(line, sizeof line, f)) { char *nl = strchr(line, '\n'); if (nl) *nl = '\0'; char *eq = strchr(line, '='); if (!eq) continue; *eq = '\0'; const char *val = util_str_trim(eq + 1); if (strcmp(line, "server") == 0 && !a->socket[0]) snprintf(a->socket, sizeof a->socket, "%s", val); else if (strcmp(line, "user") == 0 && !a->username[0]) snprintf(a->username, sizeof a->username, "%s", val); else if (strcmp(line, "attachment_dir") == 0 && !a->attachment_dir[0]) snprintf(a->attachment_dir, sizeof a->attachment_dir, "%s", val); } fclose(f); } void config_save(struct app *a) { char path[512]; config_path(path, sizeof path); config_mkdirs(path); FILE *f = fopen(path, "w"); if (!f) return; fprintf(f, "server=%s\n", a->socket); if (a->username[0]) fprintf(f, "user=%s\n", a->username); if (a->attachment_dir[0]) fprintf(f, "attachment_dir=%s\n", a->attachment_dir); fclose(f); chmod(path, 0600); } /* Connects and opens a session. Returns 0 on success, -2 when the connection failed and -1 when the server rejected the login. */ static int try_login(struct app *a, const char *user, const char *pass, const char *token, char **err_out) { *err_out = NULL; if (client_connect(a->socket, &a->conn) != 0) { *err_out = xstrdup(client_last_error()); return -2; } char *session = NULL; int rc; if (token && *token) rc = client_token_login(&a->conn, token, &session, err_out); else rc = client_login(&a->conn, user, pass, &session, err_out); if (rc != 0) { client_close(&a->conn); return -1; } snprintf(a->session, sizeof a->session, "%s", session); if (user && *user) { /* callers may pass a->username itself; snprintf with overlapping source and destination is undefined and yields "" on glibc */ char ubuf[64]; snprintf(ubuf, sizeof ubuf, "%s", user); snprintf(a->username, sizeof a->username, "%s", ubuf); } free(session); if (token && *token) { char *resp = client_rpc(&a->conn, "session.whoami", a->session, 0, NULL); char *u = jstr_dup(resp, "result.user.username"); if (u && *u) snprintf(a->username, sizeof a->username, "%s", u); free(u); free(resp); } return 0; } static int login_screen(struct app *a) { static int sel = 0; char socket_path[256], user[64], pass[128]; snprintf(socket_path, sizeof socket_path, "%s", a->socket); const char *env_pass = getenv("BOKFD_PASSWORD"); snprintf(user, sizeof user, "%s", a->username); snprintf(pass, sizeof pass, "%s", env_pass ? env_pass : ""); struct tui_form_field ff[4]; memset(ff, 0, sizeof ff); ff[0].label = "Server"; ff[0].value = socket_path; ff[0].cap = sizeof socket_path; ff[0].kind = TUI_F_TEXT; ff[1].label = "Användare"; ff[1].value = user; ff[1].cap = sizeof user; ff[1].kind = TUI_F_TEXT; ff[2].label = "Lösenord"; ff[2].value = pass; ff[2].cap = sizeof pass; ff[2].kind = TUI_F_TEXT; ff[2].mask = 1; ff[3].label = ""; ff[3].value = "Logga in"; ff[3].kind = TUI_F_ACTION; for (;;) { int r = tui_form_run("bokf — inloggning", ff, 4, 1, &sel); if (r == TUI_FORM_BACK) { if (g_quit) { endwin(); return -1; } continue; /* Esc never exits here */ } if (r != TUI_FORM_SUBMIT && r != 3) continue; /* attempt login */ snprintf(a->socket, sizeof a->socket, "%s", socket_path); char *err = NULL; int rc = try_login(a, user, pass, NULL, &err); if (rc == -2) { tui_message("Fel", "Kunde inte ansluta till %s: %s", a->socket, err ? err : "okänt fel"); free(err); continue; } if (rc != 0) { if (err && err[0] == '{') { char *code = jstr_dup(err, "error.code"); char *msg = jstr_dup(err, "error.message"); tui_message("Inloggning misslyckades", "%s: %s", code ? code : "fel", msg ? msg : "okänt fel"); free(code); free(msg); } else { tui_message("Inloggning misslyckades", "%s", err ? err : "transportfel"); } free(err); continue; } snprintf(a->password, sizeof a->password, "%s", pass); config_save(a); return 0; } } static int select_org(struct app *a) { char *resp = client_rpc(&a->conn, "session.list_orgs", a->session, 0, "{}"); if (!resp || !client_ok(resp)) { show_error("Organisationer", resp); free(resp); return -1; } size_t n = jarr_size(resp, "result.items"); if (n == 0) { tui_message("Organisationer", "Inga organisationer. Skapa en med bokfctl org.create."); free(resp); return -1; } char **items = xcalloc(n, sizeof(char *)); int64_t *ids = xcalloc(n, sizeof(int64_t)); for (size_t i = 0; i < n; i++) { char path[64]; snprintf(path, sizeof path, "result.items.%zu.name", i); char *name = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.role", i); char *role = jstr_dup(resp, path); char nbuf[128], line[256]; snprintf(nbuf, sizeof nbuf, "%s", name ? name : ""); tui_pad_field(nbuf, sizeof nbuf, 30); snprintf(line, sizeof line, "%s (%s)", nbuf, role ? role : ""); items[i] = xstrdup(line); snprintf(path, sizeof path, "result.items.%zu.id", i); ids[i] = jint_val(resp, path, 0); free(name); free(role); } int sel = tui_select_list("Välj organisation att representera", items, (int)n, 0, 0, NULL, 0, NULL, 0); for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(resp); if (sel < 0) return -1; a->org = ids[sel]; free(ids); return 0; } static void usage(FILE *f) { fprintf(f, "usage: bokftui [options]\n" " --socket TARGET unix path, tcp:host:port or tls:host:port\n" " --user NAME prefill username\n" " --org ID select org directly\n" " --fy ID select fiscal year directly\n" " --session ID reuse an open session (or BOKFD_SESSION)\n" " --screen NAME start in a view: dashboard, vouchers, inbox,\n" " bank, ib, templates, rules, reports, bokslut\n" " (yearinfo is an alias), audit, company,\n" " invoices, customers, payroll, employees,\n" " tax_tables, system (settings is an alias)\n" " --version\n"); } static void reset_modify_keys(void) { putp("\033[>4m"); fflush(stdout); } int main(int argc, char **argv) { struct app app; memset(&app, 0, sizeof app); app.conn.fd = -1; const char *self = argc > 0 ? argv[0] : "bokftui"; const char *cli_socket = NULL, *cli_user = NULL, *cli_session = NULL; const char *screen_arg = NULL; int64_t want_org = 0, want_fy = 0; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--socket") == 0 && i + 1 < argc) cli_socket = argv[++i]; else if (strcmp(argv[i], "--user") == 0 && i + 1 < argc) cli_user = argv[++i]; else if (strcmp(argv[i], "--session") == 0 && i + 1 < argc) cli_session = argv[++i]; else if (strcmp(argv[i], "--screen") == 0 && i + 1 < argc) screen_arg = argv[++i]; else if (strcmp(argv[i], "--fy") == 0 && i + 1 < argc) want_fy = strtoll(argv[++i], NULL, 10); else if (strcmp(argv[i], "--org") == 0 && i + 1 < argc) { want_org = strtoll(argv[++i], NULL, 10); } else if (strcmp(argv[i], "--version") == 0) { printf("bokftui %s\n", BOKF_VERSION); return 0; } else if (strcmp(argv[i], "--help") == 0) { usage(stdout); return 0; } else { usage(stderr); return 2; } } const char *env_socket = getenv("BOKFD_SOCKET"); const char *env_user = getenv("BOKFD_USER"); if (cli_socket) snprintf(app.socket, sizeof app.socket, "%s", cli_socket); else if (env_socket) snprintf(app.socket, sizeof app.socket, "%s", env_socket); if (cli_user) snprintf(app.username, sizeof app.username, "%s", cli_user); else if (env_user) snprintf(app.username, sizeof app.username, "%s", env_user); config_load(&app); if (!app.socket[0]) snprintf(app.socket, sizeof app.socket, "%s", "/run/bokfd/bokfd.sock"); setlocale(LC_ALL, ""); initscr(); set_escdelay(100); cbreak(); noecho(); keypad(stdscr, TRUE); tui_keys_setup(); tui_set_input(ui_getch); tui_set_screen(tui_frame, tui_hints); tui_set_quit(request_quit); atexit(reset_modify_keys); curs_set(1); const char *env_session = getenv("BOKFD_SESSION"); const char *session_arg = cli_session ? cli_session : env_session; const char *token = getenv("BOKFD_TOKEN"); const char *env_pass = getenv("BOKFD_PASSWORD"); int have_auto = (session_arg && *session_arg) || (token && *token) || (app.username[0] && env_pass && *env_pass); int stale_session = 0; char *auto_err = NULL; if (have_auto) { tui_log("auto-login start (session=%d token=%d password=%d)", session_arg && *session_arg ? 1 : 0, token && *token ? 1 : 0, app.username[0] && env_pass && *env_pass ? 1 : 0); for (int i = 0; i < 60 && !app.session[0]; i++) { if (i > 0) { char msg[128]; snprintf(msg, sizeof msg, "Återansluter till servern... (%ds)", i); tui_set_status(msg, NULL); tui_frame("bokf — ansluter"); tui_redraw(); sleep(1); } if (session_arg && *session_arg) { struct client_conn c; if (client_connect(app.socket, &c) != 0) continue; char *resp = client_rpc(&c, "session.whoami", session_arg, 0, NULL); if (resp && client_ok(resp)) { app.conn = c; snprintf(app.session, sizeof app.session, "%s", session_arg); free(resp); tui_log("auto-login: reused session"); break; } if (!resp) { free(resp); client_close(&c); continue; } free(resp); client_close(&c); stale_session = 1; session_arg = NULL; tui_log("auto-login: session expired"); } if (token && *token) { char *err = NULL; int rc = try_login(&app, NULL, NULL, token, &err); if (rc == 0) { tui_log("auto-login: token ok"); break; } token = NULL; if (rc == -2) { free(err); continue; } tui_log("auto-login: token failed: %s", err ? err : "?"); free(auto_err); auto_err = err; } if (app.username[0] && env_pass && *env_pass) { char *err = NULL; int rc = try_login(&app, app.username, env_pass, NULL, &err); if (rc == 0) { snprintf(app.password, sizeof app.password, "%s", env_pass); tui_log("auto-login: password ok"); break; } env_pass = NULL; if (rc == -2) { free(err); continue; } tui_log("auto-login: password failed: %s", err ? err : "?"); free(auto_err); auto_err = err; } break; } if (!app.session[0]) tui_log("auto-login: giving up, login screen"); } if (!app.session[0]) { if (auto_err) { char *code = jstr_dup(auto_err, "error.code"); char *msg = jstr_dup(auto_err, "error.message"); tui_message("Återanslutning misslyckades", "%s: %s", code ? code : "fel", msg ? msg : "okänt fel"); free(code); free(msg); } else if (stale_session) { tui_message("Session", "Sessionen har gått ut, logga in igen."); } } free(auto_err); if (!app.session[0] && login_screen(&app) != 0) { if (g_reload) do_reload(&app, self); endwin(); return 0; } curs_set(0); if (want_org > 0) { app.org = want_org; } else if (select_org(&app) != 0) { if (g_reload) do_reload(&app, self); endwin(); return 0; } app_refresh_context(&app); if (want_fy > 0) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)want_fy); char *resp = client_rpc(&app.conn, "fiscal_year.get", app.session, app.org, args); if (resp && client_ok(resp)) { app.fy = want_fy; char *label = jstr_dup(resp, "result.label"); char *start = jstr_dup(resp, "result.start_date"); char *end = jstr_dup(resp, "result.end_date"); if (label) snprintf(app.fy_label, sizeof app.fy_label, "%s", label); if (start) snprintf(app.fy_start, sizeof app.fy_start, "%s", start); if (end) snprintf(app.fy_end, sizeof app.fy_end, "%s", end); free(label); free(start); free(end); } free(resp); } update_status(&app); if (screen_arg && *screen_arg && strcmp(screen_arg, "dashboard") != 0) open_scene(&app, screen_arg); dashboard(&app); if (g_reload) do_reload(&app, self); client_rpc(&app.conn, "session.close", app.session, 0, "{}"); acct_cache_free(); client_close(&app.conn); endwin(); return 0; }