Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_dynamic_runtime.c
Go to the documentation of this file.
1/**
2 * @file
3 * @brief In-process dynamic-app request handler for the Pharos native runtime.
4 *
5 * Replaces the former @c spawn_app_request() path for apps whose
6 * @c host_profile is @c "dynamic-app". Instead of forking a child
7 * process, the request is handled entirely within the Pharos binary.
8 *
9 * Flow:
10 * 1. Resolve the route fixture for the app.
11 * 2. Attempt static-asset serving (static/, media/, well-known JSON).
12 * 3. Match the request path against the declared route fixture.
13 * 4. Dispatch page and action routes through the in-process handler.
14 * 5. Fall back to the dispatch table for API-style routes.
15 * 6. Return a 404 for unmatched paths.
16 */
17
26
27#include "native_fs.h"
29#include "native_support.h"
30
31#include <stdlib.h>
32#include <string.h>
33#include <strings.h>
34#include <stdio.h>
35#include <stdint.h>
36#include <time.h>
37#include <stdarg.h>
38#include <fcntl.h>
39#ifndef _WIN32
40#include <unistd.h>
41#endif
42
43/* ------------------------------------------------------------------ */
44/* Constants */
45/* ------------------------------------------------------------------ */
46
47/** @brief Host profile string that triggers in-process dynamic handling. */
48#define DYNAMIC_HOST_PROFILE "dynamic-app"
49
50/** @brief Maximum query string length we process. */
51#define MAX_QUERY_LEN 4096
52
53/** @brief CSRF cookie name used by the Pharos framework. */
54#define CSRF_COOKIE_NAME "pharos_csrf"
55
56/** @brief Session cookie name used by the Pharos framework. */
57#define SESSION_COOKIE_NAME "pharos_session"
58
59/** @brief Maximum CSRF token length (24 bytes = 48 hex chars). */
60#define CSRF_TOKEN_HEX_LEN 48
61
62/** @brief CSRF cookie max-age in seconds. */
63#define CSRF_COOKIE_MAX_AGE "7200"
64
65/* ------------------------------------------------------------------ */
66/* Internal helpers */
67/* ------------------------------------------------------------------ */
68
69static yyjson_val *state_array_runtime_local(yyjson_val *root, const char *field_name);
70static yyjson_val *find_business_by_slug_runtime_local(yyjson_val *root, const char *business_slug);
71static yyjson_val *find_business_by_id_runtime_local(yyjson_val *root, long business_id);
72static yyjson_val *find_user_by_id_runtime_local(yyjson_val *root, long user_id);
73static int long_from_value_runtime_local(yyjson_val *value, long *out_value);
74static yyjson_val *find_service_for_business_runtime_local(yyjson_val *root, long service_id, long business_id);
75static yyjson_val *find_appointment_by_id_runtime_local(yyjson_val *root, long appointment_id);
76static char *page_title_dup_runtime(yyjson_val *page_spec_root, yyjson_val *body_params_root);
78static char *request_path_dup_runtime(const HttpRequest *request);
79static char *generated_admin_manage_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *surface_kind, const char *error_text);
80static char *business_availability_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long availability_id, const char *error_text);
81static char *profile_page_params_json_dup_runtime_local(const char *state_path, long user_id, const char *payment_preference_options_html);
82static yyjson_val *proof_state_root_load_runtime(const char *state_path, yyjson_doc **doc_out);
83static char *proof_home_page_params_json_dup_runtime(const char *state_path);
84static int write_proof_state_api_response(NativeConnection *connection, const char *state_path);
85static int text_append_format_runtime_local(char **buffer, size_t *length, size_t *capacity, const char *format, ...);
86static char *register_page_params_json_dup_runtime(const char *query_string);
87
88typedef struct {
89 const char *surface_kind;
92 const char *surface_title;
93 const char *surface_intro;
94 const char *filter_title;
95 const char *sort_options_html;
96 const char *records_title;
97 const char *header_html;
98 const char *create_title;
103
106 const GeneratedAdminSurfaceDescriptor *descriptor,
107 yyjson_val *root,
108 long business_id,
109 const char *business_slug,
110 char **row_html,
111 size_t *row_html_length,
112 size_t *row_html_capacity
113);
115 const GeneratedAdminSurfaceDescriptor *descriptor,
116 char **form_fields_html,
117 size_t *form_fields_length,
118 size_t *form_fields_capacity,
119 const char *day_options_html
120);
122 const GeneratedAdminSurfaceDescriptor *descriptor
123);
125 yyjson_mut_doc *doc,
126 yyjson_mut_val *page,
127 const GeneratedAdminSurfaceDescriptor *descriptor,
128 const char *filter_controls_html
129);
131static char *password_reset_sent_page_params_json_dup_runtime(const char *preview_path);
132static char *password_reset_complete_page_params_json_dup_runtime(const char *request_path);
133static char *invite_created_page_params_json_dup_runtime(const char *invite_email, const char *invite_path, const char *return_path);
134static char *invite_accept_page_params_json_dup_runtime(const char *invite_kind, const char *target_business_name, const char *error_message, const char *invite_email, const char *form_action);
135static char *invite_accept_done_page_params_json_dup_runtime(const char *continue_path);
136static char *service_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long service_id);
137char *business_landing_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long user_id, const char *app_root);
138char *business_calendar_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *app_root, const char *selected_year_text, const char *selected_month_text, const char *selected_date_text);
139char *diagnostics_render_page_params_json_dup_runtime(const char *state_path);
140static unsigned char *request_body_bytes_dup_runtime(const HttpRequest *request, size_t *out_size);
141
142typedef struct {
146
148 yyjson_val *root,
149 const char *resource_collection_name,
150 long resource_id,
151 ResourceLinkDisplay *display
152);
155 yyjson_val *root,
156 long user_id,
157 char **subscriptions_block,
158 size_t *subscriptions_length,
159 size_t *subscriptions_capacity
160);
161static char *profile_subscriptions_block_dup_runtime(yyjson_val *root, long user_id);
163 yyjson_mut_doc *doc,
164 yyjson_mut_val *root,
165 const char *key,
166 const char *value
167);
169 yyjson_mut_doc *doc,
170 yyjson_mut_val *root,
171 const char *key,
172 const char *value
173);
175static char *password_reset_preview_html_dup_runtime(const char *preview_path);
176static char *invite_accept_target_html_dup_runtime(const char *target_business_name);
177static char *request_form_value_dup_runtime(const HttpRequest *request, const char *field_name);
178static char *password_hash_dup_runtime(const char *password_value);
180 const AppRuntime *app,
181 const HttpRequest *request,
182 const NativeDynamicRouteMatch *match,
183 const char *request_path,
184 const char *query_string,
185 long active_user_id
186);
188 const AppRuntime *app,
189 const NativeDynamicRouteMatch *match,
190 const char *request_path
191);
193 NativeConnection *connection,
194 const AppRuntime *app,
195 const HttpRequest *request
196);
198 NativeConnection *connection,
199 const AppRuntime *app,
200 const HttpRequest *request,
201 const NativeDynamicRouteMatch *match
202);
204 NativeConnection *connection,
205 const AppRuntime *app,
206 const HttpRequest *request,
207 const NativeDynamicRouteMatch *match
208);
210 NativeConnection *connection,
211 const AppRuntime *app,
212 const HttpRequest *request,
213 const NativeDynamicRouteMatch *match
214);
215static long state_tenant_id_for_business_runtime_local(yyjson_val *root, long business_id);
216static int user_has_active_tenant_membership_runtime_local(yyjson_val *root, long user_id, long tenant_id);
217static const char *membership_role_id_for_invite_runtime_local(const char *role_hint, const char *account_type);
218static int persist_app_state_text_runtime_local(const AppRuntime *app, const char *state_json_text);
219static int append_rate_limit_event_runtime_local(const AppRuntime *app, const char *policy_key, const char *bucket_key, const char *outcome);
221 NativeConnection *connection,
222 const AppRuntime *app,
223 const HttpRequest *request,
224 const char *page_id,
225 const char *request_path,
226 const char *body_params_json
227);
228
229/**
230 * @brief Writes a JSON error response to a connection.
231 *
232 * @param connection Connection to write to.
233 * @param status HTTP status code.
234 * @param status_text HTTP reason phrase.
235 * @param error_kind Machine-readable error kind.
236 * @param message Human-readable error message.
237 */
239 NativeConnection *connection,
240 int status,
241 const char *status_text,
242 const char *error_kind,
243 const char *message
244) {
245 char body[1024];
246 int written;
247
248 written = snprintf(body, sizeof(body),
249 "{\"error\":\"%s\",\"message\":\"%s\"}",
250 error_kind != NULL ? error_kind : "error",
251 message != NULL ? message : "An error occurred"
252 );
253 if (written < 0 || (size_t)written >= sizeof(body)) {
254 native_send_text_response(connection, 500, "Internal Server Error", "response formatting error\n");
255 return;
256 }
258 connection,
259 "HTTP/1.1 %d %s\r\n"
260 "Content-Type: application/json\r\n"
261 "Content-Length: %d\r\n"
262 "Connection: close\r\n"
263 "\r\n%s",
264 status,
265 status_text,
266 written,
267 body
268 );
269}
270
271/**
272 * @brief Generates a random hex token for CSRF protection.
273 *
274 * Produces @p byte_count random bytes encoded as hex (so the
275 * returned string is @p byte_count * 2 characters long).
276 *
277 * @param buf Destination buffer (must be at least byte_count*2+1).
278 * @param byte_count Number of random bytes to generate.
279 */
280static void csrf_generate_random_hex(char *buf, size_t byte_count) {
281 static const char hex[] = "0123456789abcdef";
282 size_t i;
283 unsigned char raw[48];
284 if (byte_count > sizeof(raw)) {
285 byte_count = sizeof(raw);
286 }
287 {
288 int fd = open("/dev/urandom", O_RDONLY);
289 if (fd >= 0) {
290 size_t got = 0;
291 while (got < byte_count) {
292 ssize_t n = read(fd, raw + got, byte_count - got);
293 if (n <= 0) break;
294 got += (size_t)n;
295 }
296 close(fd);
297 }
298 }
299 for (i = 0; i < byte_count; i++) {
300 buf[i * 2] = hex[(raw[i] >> 4) & 0x0f];
301 buf[i * 2 + 1] = hex[raw[i] & 0x0f];
302 }
303 buf[byte_count * 2] = '\0';
304}
305
306/**
307 * @brief Extracts the CSRF token from the request cookie header.
308 *
309 * @param cookie_header Raw Cookie header value, or NULL.
310 * @return Newly allocated token string, or NULL if not found.
311 * Caller must free.
312 */
313static char *csrf_token_from_cookie(const char *cookie_header) {
314 return native_dynamic_cookie_value(cookie_header, CSRF_COOKIE_NAME);
315}
316
317/**
318 * @brief Returns the CSRF token to use for a response.
319 *
320 * Reads the token from the request cookie. If absent, generates
321 * a new random token.
322 *
323 * @param cookie_header Raw Cookie header value, or NULL.
324 * @return Newly allocated token string. Caller must free.
325 */
326static char *csrf_token_for_response(const char *cookie_header) {
327 char *existing = csrf_token_from_cookie(cookie_header);
328 if (existing != NULL && existing[0] != '\0') {
329 return existing;
330 }
331 free(existing);
332 {
333 char *token = (char *)malloc(CSRF_TOKEN_HEX_LEN + 1);
334 if (token != NULL) {
335 csrf_generate_random_hex(token, 24);
336 }
337 return token;
338 }
339}
340
341static int request_has_session_cookie(const char *cookie_header) {
342 char *session_token = native_dynamic_cookie_value(cookie_header, SESSION_COOKIE_NAME);
343 int has_session = session_token != NULL && session_token[0] != '\0';
344 free(session_token);
345 return has_session;
346}
347
348static char *request_header_value_dup_runtime(const char *headers, const char *header_name) {
349 size_t name_len = 0;
350 const char *cursor = headers;
351 if (headers == NULL || header_name == NULL || header_name[0] == '\0') {
352 return NULL;
353 }
354 name_len = strlen(header_name);
355 while (*cursor != '\0') {
356 const char *line_end = strchr(cursor, '\n');
357 size_t line_len = line_end != NULL ? (size_t)(line_end - cursor) : strlen(cursor);
358 if (line_len > name_len + 1 && strncasecmp(cursor, header_name, name_len) == 0 && cursor[name_len] == ':') {
359 const char *value_start = cursor + name_len + 1;
360 const char *value_end = cursor + line_len;
361 while (value_start < value_end && (*value_start == ' ' || *value_start == '\t')) {
362 value_start++;
363 }
364 while (value_end > value_start && (value_end[-1] == ' ' || value_end[-1] == '\t' || value_end[-1] == '\r')) {
365 value_end--;
366 }
367 return dup_range(value_start, (size_t)(value_end - value_start));
368 }
369 cursor = line_end != NULL ? line_end + 1 : cursor + line_len;
370 }
371 return NULL;
372}
373
374static char *csrf_request_token_dup_runtime(const HttpRequest *request, const char *form_body) {
375 char *token = native_dynamic_form_value(form_body != NULL ? form_body : "", "csrf_token");
376 if (token != NULL && token[0] != '\0') {
377 return token;
378 }
379 free(token);
380 return request_header_value_dup_runtime(request != NULL ? request->headers : NULL, "X-CSRF-Token");
381}
382
383static int csrf_request_valid_runtime(const HttpRequest *request, const char *form_body) {
384 char *cookie_token = csrf_token_from_cookie(request != NULL ? request->cookie : NULL);
385 char *request_token = csrf_request_token_dup_runtime(request, form_body);
386 int valid = cookie_token != NULL && request_token != NULL && cookie_token[0] != '\0' && request_token[0] != '\0' && streq(cookie_token, request_token);
387 free(cookie_token);
388 free(request_token);
389 return valid;
390}
391
392static char *payment_preference_options_html_dup_runtime(const char *selected_value) {
393 static const struct {
394 const char *value;
395 const char *label;
396 } options[] = {
397 {"pay_on_site", "Pay on site"},
398 {"credit_card", "Credit card"}
399 };
400 char *html = NULL;
401 size_t capacity = 256;
402 size_t length = 0;
403 size_t index = 0;
404
405 html = (char *)malloc(capacity);
406 if (html == NULL) {
407 return NULL;
408 }
409 html[0] = '\0';
410
411 for (index = 0; index < (sizeof(options) / sizeof(options[0])); index++) {
412 const char *selected_attr = (selected_value != NULL && streq(selected_value, options[index].value)) ? " selected" : "";
413 int written = snprintf(
414 html + length,
415 capacity - length,
416 "<option value=\"%s\"%s>%s</option>",
417 options[index].value,
418 selected_attr,
419 options[index].label
420 );
421 if (written < 0) {
422 free(html);
423 return NULL;
424 }
425 if ((size_t)written >= capacity - length) {
426 size_t new_capacity = capacity * 2 + (size_t)written + 1;
427 char *grown = (char *)realloc(html, new_capacity);
428 if (grown == NULL) {
429 free(html);
430 return NULL;
431 }
432 html = grown;
433 capacity = new_capacity;
434 written = snprintf(
435 html + length,
436 capacity - length,
437 "<option value=\"%s\"%s>%s</option>",
438 options[index].value,
439 selected_attr,
440 options[index].label
441 );
442 if (written < 0 || (size_t)written >= capacity - length) {
443 free(html);
444 return NULL;
445 }
446 }
447 length += (size_t)written;
448 }
449
450 return html;
451}
452
453static char *request_body_text_dup_runtime(const HttpRequest *request) {
454 if (request == NULL) {
455 return strdup("");
456 }
457 if (request->body != NULL) {
458 return strdup(request->body);
459 }
460 if (request->body_file_path != NULL && request->body_file_path[0] != '\0') {
461 char *body_text = read_file_text(request->body_file_path);
462 if (body_text != NULL) {
463 return body_text;
464 }
465 }
466 return strdup("");
467}
468
469static unsigned char *request_body_bytes_dup_runtime(const HttpRequest *request, size_t *out_size) {
470 if (out_size != NULL) {
471 *out_size = 0;
472 }
473 if (request == NULL) {
474 return NULL;
475 }
476 if (request->body_file_path != NULL && request->body_file_path[0] != '\0') {
477 return read_file_bytes_dup_runtime(request->body_file_path, out_size);
478 }
479 if (request->body != NULL) {
480 unsigned char *copy = (unsigned char *)malloc(request->body_len + 1);
481 if (copy == NULL) {
482 return NULL;
483 }
484 memcpy(copy, request->body, request->body_len);
485 copy[request->body_len] = '\0';
486 if (out_size != NULL) {
487 *out_size = request->body_len;
488 }
489 return copy;
490 }
491 return NULL;
492}
493
494static char *request_form_value_dup_runtime(const HttpRequest *request, const char *field_name) {
495 if (field_name == NULL || field_name[0] == '\0') {
496 return NULL;
497 }
498 if (request != NULL && request->is_multipart) {
499 size_t body_len = 0;
500 unsigned char *body_bytes = request_body_bytes_dup_runtime(request, &body_len);
501 char *value = NULL;
502 if (body_bytes != NULL) {
503 value = native_dynamic_multipart_extract_text(body_bytes, body_len, request->content_type, field_name);
504 free(body_bytes);
505 if (value != NULL) {
506 return value;
507 }
508 }
509 }
510 {
511 char *form_body = request_body_text_dup_runtime(request);
512 char *value = native_dynamic_form_value(form_body != NULL ? form_body : "", field_name);
513 free(form_body);
514 return value;
515 }
516}
517
518static char *request_path_dup_runtime(const HttpRequest *request) {
519 const char *target = request != NULL && request->target != NULL ? request->target : "/";
520 const char *qmark = strchr(target, '?');
521 size_t path_len = qmark != NULL ? (size_t)(qmark - target) : strlen(target);
522 return dup_range(target, path_len);
523}
524
525static char *route_request_path_dup_runtime(const AppRuntime *app, const HttpRequest *request) {
526 char *full_path = request_path_dup_runtime(request);
527 const char *base_path = app != NULL ? app->base_path : NULL;
528 if (full_path == NULL) {
529 return NULL;
530 }
531 if (base_path == NULL || base_path[0] == '\0' || streq(base_path, "/")) {
532 return full_path;
533 }
534 {
535 size_t base_len = strlen(base_path);
536 if (strncmp(full_path, base_path, base_len) == 0) {
537 const char *suffix = full_path + base_len;
538 char *trimmed = NULL;
539 if (*suffix == '\0') {
540 free(full_path);
541 return strdup("/");
542 }
543 if (*suffix == '/') {
544 trimmed = strdup(suffix);
545 free(full_path);
546 return trimmed;
547 }
548 }
549 }
550 return full_path;
551}
552
554 return strdup(
555 "{\"schema\":\"pharos.dynamic_app_state.v1\",\"next_id\":3,\"last_action\":\"seeded\",\"items\":[{\"id\":1,\"done\":true,\"title\":\"Confirm dynamic host build path\"},{\"id\":2,\"done\":false,\"title\":\"Create a task through the compiled host\"}]}"
556 );
557}
558
559static yyjson_val *proof_state_root_load_runtime(const char *state_path, yyjson_doc **doc_out) {
560 yyjson_doc *doc = NULL;
561 yyjson_val *root = NULL;
562 yyjson_val *items = NULL;
563 char *seed_json = NULL;
564 if (doc_out != NULL) {
565 *doc_out = NULL;
566 }
567 if (state_path == NULL || state_path[0] == '\0') {
568 return NULL;
569 }
570 doc = native_json_doc_load_file(state_path);
571 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
572 items = root != NULL ? native_json_obj_get(root, "items") : NULL;
573 if (root != NULL && yyjson_is_obj(root) && items != NULL && yyjson_is_arr(items)) {
574 if (doc_out != NULL) {
575 *doc_out = doc;
576 }
577 return root;
578 }
581 if (seed_json == NULL || write_text_file(state_path, seed_json) != 0) {
582 free(seed_json);
583 return NULL;
584 }
585 free(seed_json);
586 doc = native_json_doc_load_file(state_path);
587 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
588 items = root != NULL ? native_json_obj_get(root, "items") : NULL;
589 if (root == NULL || !yyjson_is_obj(root) || items == NULL || !yyjson_is_arr(items)) {
591 return NULL;
592 }
593 if (doc_out != NULL) {
594 *doc_out = doc;
595 }
596 return root;
597}
598
600 yyjson_arr_iter iter;
601 yyjson_val *item = NULL;
602 char *html = NULL;
603 size_t length = 0;
604 size_t capacity = 0;
605 int has_items = 0;
606 if (items != NULL && yyjson_is_arr(items) && yyjson_arr_iter_init(items, &iter)) {
607 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
608 long task_id = 0;
609 int done = 0;
610 char *title = NULL;
611 char *title_html = NULL;
612 task_id = native_json_obj_get_long_default(item, "id", 0, NULL);
613 done = yyjson_get_bool(native_json_obj_get(item, "done"));
614 title = native_json_obj_get_string_dup(item, "title");
615 title_html = html_escaped_dup_runtime(title != NULL ? title : "");
616 if (task_id > 0 && title_html != NULL && text_append_format_runtime_local(
617 &html,
618 &length,
619 &capacity,
620 "<li><form method=\"post\" action=\"/toggle\" style=\"display:inline\"><input type=\"hidden\" name=\"id\" value=\"%ld\"><button type=\"submit\">%s</button></form> <span>%s%s</span></li>",
621 task_id,
622 done ? "Reopen" : "Done",
623 done ? "✓ " : "",
624 title_html)) {
625 has_items = 1;
626 }
627 free(title);
628 free(title_html);
629 }
630 }
631 if (!has_items) {
632 free(html);
633 return strdup("<li>No tasks yet.</li>");
634 }
635 return html;
636}
637
638static char *proof_home_page_params_json_dup_runtime(const char *state_path) {
639 yyjson_doc *state_doc = NULL;
640 yyjson_val *state_root = proof_state_root_load_runtime(state_path, &state_doc);
641 yyjson_val *items = state_root != NULL ? native_json_obj_get(state_root, "items") : NULL;
642 char *task_items_html = proof_task_items_html_dup_runtime(items);
643 char *last_action = state_root != NULL ? native_json_obj_get_string_dup(state_root, "last_action") : NULL;
645 yyjson_mut_val *page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
646 char *result = NULL;
647 if (doc == NULL || page == NULL || task_items_html == NULL || !yyjson_mut_obj_add_strcpy(doc, page, "title", "Dynamic App Template") || !yyjson_mut_obj_add_strcpy(doc, page, "intro", "Served by the Pharos native dynamic runtime contract with app-owned declarative templates.") || !yyjson_mut_obj_add_strcpy(doc, page, "last_action", last_action != NULL ? last_action : "seeded") || !yyjson_mut_obj_add_strcpy(doc, page, "task_items_html", task_items_html)) {
649 free(task_items_html);
650 free(last_action);
651 native_json_doc_free(state_doc);
652 return NULL;
653 }
654 yyjson_mut_doc_set_root(doc, page);
655 result = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
657 free(task_items_html);
658 free(last_action);
659 native_json_doc_free(state_doc);
660 return result;
661}
662
663static int write_proof_state_api_response(NativeConnection *connection, const char *state_path) {
664 yyjson_doc *state_doc = NULL;
665 char *body = NULL;
666 int body_len = 0;
667 if (connection == NULL) {
668 return 500;
669 }
670 if (proof_state_root_load_runtime(state_path, &state_doc) == NULL || state_doc == NULL) {
671 native_send_text_response(connection, 500, "Internal Server Error", "proof state unavailable\n");
672 return 500;
673 }
674 body = yyjson_write(state_doc, YYJSON_WRITE_NOFLAG, NULL);
675 native_json_doc_free(state_doc);
676 if (body == NULL) {
677 native_send_text_response(connection, 500, "Internal Server Error", "proof state serialize failed\n");
678 return 500;
679 }
680 body_len = (int)strlen(body);
682 connection,
683 "HTTP/1.1 200 OK\r\n"
684 "Content-Type: application/json\r\n"
685 "Content-Length: %d\r\n"
686 "Cache-Control: no-store\r\n"
687 "Connection: close\r\n"
688 "\r\n%s",
689 body_len,
690 body
691 );
692 free(body);
693 return 200;
694}
695
696static int text_append_runtime_local(char **buffer, size_t *length, size_t *capacity, const char *text) {
697 size_t text_len;
698 char *grown;
699 if (buffer == NULL || length == NULL || capacity == NULL || text == NULL) {
700 return 0;
701 }
702 text_len = strlen(text);
703 if (*buffer == NULL) {
704 *capacity = text_len + 64;
705 *buffer = (char *)malloc(*capacity);
706 if (*buffer == NULL) {
707 *capacity = 0;
708 return 0;
709 }
710 (*buffer)[0] = '\0';
711 *length = 0;
712 }
713 if (*length + text_len + 1 > *capacity) {
714 *capacity = (*length + text_len + 1) * 2;
715 grown = (char *)realloc(*buffer, *capacity);
716 if (grown == NULL) {
717 return 0;
718 }
719 *buffer = grown;
720 }
721 memcpy(*buffer + *length, text, text_len);
722 *length += text_len;
723 (*buffer)[*length] = '\0';
724 return 1;
725}
726
727static int text_append_format_runtime_local(char **buffer, size_t *length, size_t *capacity, const char *format, ...) {
728 va_list args;
729 va_list args_copy;
730 int needed;
731 char *chunk;
732 int ok;
733 if (buffer == NULL || length == NULL || capacity == NULL || format == NULL) {
734 return 0;
735 }
736 va_start(args, format);
737 va_copy(args_copy, args);
738 needed = vsnprintf(NULL, 0, format, args_copy);
739 va_end(args_copy);
740 if (needed < 0) {
741 va_end(args);
742 return 0;
743 }
744 chunk = (char *)malloc((size_t)needed + 1);
745 if (chunk == NULL) {
746 va_end(args);
747 return 0;
748 }
749 vsnprintf(chunk, (size_t)needed + 1, format, args);
750 va_end(args);
751 ok = text_append_runtime_local(buffer, length, capacity, chunk);
752 free(chunk);
753 return ok;
754}
755
756static char *day_of_week_options_html_dup_runtime(const char *selected_day) {
757 static const char *days[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
758 char *html = NULL;
759 size_t length = 0;
760 size_t capacity = 0;
761 size_t index;
762 for (index = 0; index < sizeof(days) / sizeof(days[0]); index++) {
763 char option[96];
764 snprintf(option, sizeof(option), "<option value=\"%s\"%s>%s</option>",
765 days[index],
766 (selected_day != NULL && streq(selected_day, days[index])) ? " selected" : "",
767 days[index]);
768 if (!text_append_runtime_local(&html, &length, &capacity, option)) {
769 free(html);
770 return NULL;
771 }
772 }
773 return html != NULL ? html : strdup("");
774}
775
776/**
777 * @brief Returns the generated-admin descriptor for one declared manage surface.
778 *
779 * @param surface_kind Surface identifier requested by the page route.
780 * @return Matching descriptor, or NULL when the surface is unsupported.
781 */
783 static const GeneratedAdminSurfaceDescriptor descriptors[] = {
784 {
785 "services",
786 "services",
787 "services",
788 "Generated Service Admin",
789 "Framework-owned generated admin surface for business services.",
790 "Filter",
791 "<option value=\"sort_order_asc\" selected>Display order</option><option value=\"name_asc\">Name</option><option value=\"duration_desc\">Longest duration</option>",
792 "Records",
793 "<th>Display order</th><th>Name</th><th>Cost</th><th>Duration</th>",
794 "Create service",
795 "Create",
796 "<tr><td colspan=\"5\">No services found.</td></tr>",
797 ""
798 },
799 {
800 "availability",
801 "business_availability",
802 "availability",
803 "Generated Availability Admin",
804 "Framework-owned generated admin surface for business availability.",
805 "Filter availability",
806 "<option value=\"day_time_asc\" selected>Day / start time</option><option value=\"start_time_asc\">Start time</option>",
807 "Availability windows",
808 "<th>Day</th><th>Start</th><th>End</th>",
809 "Add availability",
810 "Add window",
811 "<tr><td colspan=\"4\">No availability windows found.</td></tr>",
812 "<label>Day<select name=\"day_of_week\" class=\"form-control\"><option value=\"\">All days</option><option value=\"Mon\">Mon</option><option value=\"Tue\">Tue</option><option value=\"Wed\">Wed</option><option value=\"Thu\">Thu</option><option value=\"Fri\">Fri</option><option value=\"Sat\">Sat</option><option value=\"Sun\">Sun</option></select></label>"
813 },
814 {
815 "technicians",
816 "technicians",
817 "technicians",
818 "Generated Technician Admin",
819 "Framework-owned generated admin surface for business technicians.",
820 "Filter technicians",
821 "<option value=\"name_asc\" selected>Name</option><option value=\"email_asc\">Email</option>",
822 "Technicians",
823 "<th>Name</th><th>Email</th><th>Status</th>",
824 "Create technician",
825 "Create technician",
826 "<tr><td colspan=\"4\">No technicians found.</td></tr>",
827 ""
828 }
829 };
830 size_t index;
831 if (surface_kind == NULL || surface_kind[0] == '\0') {
832 return NULL;
833 }
834 for (index = 0; index < sizeof(descriptors) / sizeof(descriptors[0]); index++) {
835 if (streq(surface_kind, descriptors[index].surface_kind)) {
836 return &descriptors[index];
837 }
838 }
839 return NULL;
840}
841
842/**
843 * @brief Appends row HTML for one generated-admin manage surface.
844 *
845 * @param descriptor Surface descriptor that owns the row contract.
846 * @param root Loaded runtime state root.
847 * @param business_id Current business identifier.
848 * @param business_slug Current business slug used in edit links.
849 * @param row_html Accumulated row HTML output buffer.
850 * @param row_html_length Current row HTML length.
851 * @param row_html_capacity Current row HTML capacity.
852 * @return 1 on success, or 0 on allocation or formatting failure.
853 */
855 const GeneratedAdminSurfaceDescriptor *descriptor,
856 yyjson_val *root,
857 long business_id,
858 const char *business_slug,
859 char **row_html,
860 size_t *row_html_length,
861 size_t *row_html_capacity
862) {
863 yyjson_val *rows = NULL;
864 yyjson_arr_iter iter;
865 yyjson_val *row = NULL;
866 if (descriptor == NULL || root == NULL || business_slug == NULL || row_html == NULL || row_html_length == NULL || row_html_capacity == NULL) {
867 return 0;
868 }
869 rows = state_array_runtime_local(root, descriptor->resource_collection_name);
870 if (rows != NULL && yyjson_arr_iter_init(rows, &iter)) {
871 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
872 long row_business_id = 0;
873 if (!yyjson_is_obj(row)) {
874 continue;
875 }
876 row_business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
877 if (row_business_id != business_id) {
878 continue;
879 }
880 if (streq(descriptor->surface_kind, "services")) {
881 char *name = native_json_obj_get_string_dup(row, "name");
882 char *cost = native_json_obj_get_string_dup(row, "cost");
883 long resource_id = native_json_obj_get_long_default(row, "id", 0, NULL);
884 long sort_order = native_json_obj_get_long_default(row, "sort_order", 0, NULL);
885 long duration_minutes = native_json_obj_get_long_default(row, "duration_minutes", 0, NULL);
887 row_html,
888 row_html_length,
889 row_html_capacity,
890 "<tr><td>%ld</td><td>%s</td><td>%s</td><td>%ld</td><td><a class=\"btn btn-secondary\" href=\"/businesses/%s/%s/%ld/edit/\">Edit</a></td></tr>",
891 sort_order,
892 name != NULL ? name : "",
893 cost != NULL ? cost : "",
894 duration_minutes,
895 business_slug,
896 descriptor->surface_path_segment,
897 resource_id)) {
898 free(name);
899 free(cost);
900 return 0;
901 }
902 free(name);
903 free(cost);
904 } else if (streq(descriptor->surface_kind, "availability")) {
905 char *day = native_json_obj_get_string_dup(row, "day_of_week");
906 char *start_time = native_json_obj_get_string_dup(row, "start_time");
907 char *end_time = native_json_obj_get_string_dup(row, "end_time");
908 long resource_id = native_json_obj_get_long_default(row, "id", 0, NULL);
910 row_html,
911 row_html_length,
912 row_html_capacity,
913 "<tr><td>%s</td><td>%s</td><td>%s</td><td><a class=\"btn btn-secondary\" href=\"/businesses/%s/%s/%ld/edit/\">Edit</a></td></tr>",
914 day != NULL ? day : "",
915 start_time != NULL ? start_time : "",
916 end_time != NULL ? end_time : "",
917 business_slug,
918 descriptor->surface_path_segment,
919 resource_id)) {
920 free(day);
921 free(start_time);
922 free(end_time);
923 return 0;
924 }
925 free(day);
926 free(start_time);
927 free(end_time);
928 } else if (streq(descriptor->surface_kind, "technicians")) {
929 char *name = native_json_obj_get_string_dup(row, "name");
930 char *email = native_json_obj_get_string_dup(row, "email");
931 long resource_id = native_json_obj_get_long_default(row, "id", 0, NULL);
932 int is_active = yyjson_get_bool(native_json_obj_get(row, "is_active"));
934 row_html,
935 row_html_length,
936 row_html_capacity,
937 "<tr><td>%s</td><td>%s</td><td>%s</td><td><a class=\"btn btn-secondary\" href=\"/businesses/%s/%s/%ld/edit/\">Edit</a></td></tr>",
938 name != NULL ? name : "",
939 email != NULL ? email : "",
940 is_active ? "Active" : "Inactive",
941 business_slug,
942 descriptor->surface_path_segment,
943 resource_id)) {
944 free(name);
945 free(email);
946 return 0;
947 }
948 free(name);
949 free(email);
950 }
951 }
952 }
953 if (*row_html == NULL) {
954 return text_append_runtime_local(row_html, row_html_length, row_html_capacity, descriptor->empty_state_row_html);
955 }
956 return 1;
957}
958
959/**
960 * @brief Appends create-form field HTML for one generated-admin surface.
961 *
962 * @param descriptor Surface descriptor that owns the create-form contract.
963 * @param form_fields_html Accumulated form field output buffer.
964 * @param form_fields_length Current form field length.
965 * @param form_fields_capacity Current form field capacity.
966 * @param day_options_html Optional prebuilt day-of-week options for availability surfaces.
967 * @return 1 on success, or 0 on allocation or formatting failure.
968 */
970 const GeneratedAdminSurfaceDescriptor *descriptor,
971 char **form_fields_html,
972 size_t *form_fields_length,
973 size_t *form_fields_capacity,
974 const char *day_options_html
975) {
976 if (descriptor == NULL || form_fields_html == NULL || form_fields_length == NULL || form_fields_capacity == NULL) {
977 return 0;
978 }
979 if (streq(descriptor->surface_kind, "services")) {
981 form_fields_html,
982 form_fields_length,
983 form_fields_capacity,
984 "<label>Name <input type=\"text\" name=\"name\" class=\"form-control\" required></label>"
985 "<label>Description <textarea name=\"description\" class=\"form-control\"></textarea></label>"
986 "<label>Cost <input type=\"text\" name=\"cost\" class=\"form-control\" required></label>"
987 "<label>Duration <input type=\"number\" name=\"duration_minutes\" class=\"form-control\" value=\"45\" required></label>"
988 "<label>Display Order <input type=\"number\" name=\"sort_order\" class=\"form-control\" value=\"1\" required></label>"
989 );
990 }
991 if (streq(descriptor->surface_kind, "availability")) {
993 form_fields_html,
994 form_fields_length,
995 form_fields_capacity,
996 "<label>Day<select name=\"day_of_week\" class=\"form-control\">%s</select></label>"
997 "<label>Start <input type=\"time\" name=\"start_time\" class=\"form-control\" value=\"09:00\"></label>"
998 "<label>End <input type=\"time\" name=\"end_time\" class=\"form-control\" value=\"17:00\"></label>",
999 day_options_html != NULL ? day_options_html : ""
1000 );
1001 }
1002 if (streq(descriptor->surface_kind, "technicians")) {
1004 form_fields_html,
1005 form_fields_length,
1006 form_fields_capacity,
1007 "<label>Name <input type=\"text\" name=\"name\" class=\"form-control\" required></label>"
1008 "<label>Email <input type=\"email\" name=\"email\" class=\"form-control\" required></label>"
1009 );
1010 }
1011 return 0;
1012}
1013
1014/**
1015 * @brief Duplicates the filter-control HTML for one generated-admin surface.
1016 *
1017 * @param descriptor Surface descriptor that owns the filter-control contract.
1018 * @return Newly allocated HTML string, or NULL on allocation failure.
1019 */
1021 const GeneratedAdminSurfaceDescriptor *descriptor
1022) {
1023 if (descriptor == NULL || descriptor->default_filter_controls_html == NULL) {
1024 return NULL;
1025 }
1026 return strdup(descriptor->default_filter_controls_html);
1027}
1028
1029/**
1030 * @brief Writes descriptor-owned page copy into one generated-admin params document.
1031 *
1032 * @param doc Mutable document that owns the page object.
1033 * @param page Mutable page params object.
1034 * @param descriptor Surface descriptor that owns the page copy.
1035 * @param filter_controls_html Effective filter-control HTML for the page.
1036 */
1038 yyjson_mut_doc *doc,
1039 yyjson_mut_val *page,
1040 const GeneratedAdminSurfaceDescriptor *descriptor,
1041 const char *filter_controls_html
1042) {
1043 if (doc == NULL || page == NULL || descriptor == NULL) {
1044 return;
1045 }
1046 yyjson_mut_obj_add_strcpy(doc, page, "surface_title", descriptor->surface_title);
1047 yyjson_mut_obj_add_strcpy(doc, page, "title", descriptor->surface_title);
1048 yyjson_mut_obj_add_strcpy(doc, page, "intro", descriptor->surface_intro);
1049 yyjson_mut_obj_add_strcpy(doc, page, "filter_title", descriptor->filter_title);
1050 yyjson_mut_obj_add_strcpy(doc, page, "sort_options_html", descriptor->sort_options_html);
1051 yyjson_mut_obj_add_strcpy(doc, page, "records_title", descriptor->records_title);
1052 yyjson_mut_obj_add_strcpy(doc, page, "header_html", descriptor->header_html);
1053 yyjson_mut_obj_add_strcpy(doc, page, "create_title", descriptor->create_title);
1054 yyjson_mut_obj_add_strcpy(doc, page, "create_button_label", descriptor->create_button_label);
1055 yyjson_mut_obj_add_strcpy(doc, page, "filter_controls_html", filter_controls_html != NULL ? filter_controls_html : "");
1056}
1057
1058/**
1059 * @brief Loads slug and display-name fields for one linked resource id.
1060 *
1061 * @param root Loaded runtime state root.
1062 * @param resource_collection_name State collection that owns the linked resource.
1063 * @param resource_id Resource identifier to resolve.
1064 * @param display Output record that receives duplicated slug and name values.
1065 * @return 1 on success, or 0 when the resource cannot be resolved.
1066 */
1068 yyjson_val *root,
1069 const char *resource_collection_name,
1070 long resource_id,
1071 ResourceLinkDisplay *display
1072) {
1073 yyjson_val *rows = NULL;
1074 yyjson_arr_iter iter;
1075 yyjson_val *row = NULL;
1076 if (root == NULL || resource_collection_name == NULL || resource_collection_name[0] == '\0' || resource_id <= 0 || display == NULL) {
1077 return 0;
1078 }
1079 memset(display, 0, sizeof(*display));
1080 rows = state_array_runtime_local(root, resource_collection_name);
1081 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
1082 return 0;
1083 }
1084 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
1085 long row_id = 0;
1086 if (!yyjson_is_obj(row)) {
1087 continue;
1088 }
1089 row_id = native_json_obj_get_long_default(row, "id", 0, NULL);
1090 if (row_id != resource_id) {
1091 continue;
1092 }
1093 display->resource_slug = native_json_obj_get_string_dup(row, "slug");
1094 display->resource_name = native_json_obj_get_string_dup(row, "name");
1095 return 1;
1096 }
1097 return 0;
1098}
1099
1100/**
1101 * @brief Frees a linked-resource display record.
1102 *
1103 * @param display Display record to release.
1104 */
1106 if (display == NULL) {
1107 return;
1108 }
1109 free(display->resource_slug);
1110 free(display->resource_name);
1111 display->resource_slug = NULL;
1112 display->resource_name = NULL;
1113}
1114
1115/**
1116 * @brief Appends profile subscription rows for one user into an HTML list buffer.
1117 *
1118 * @param root Loaded runtime state root.
1119 * @param user_id Active signed-in user id.
1120 * @param subscriptions_block Accumulated subscription row HTML.
1121 * @param subscriptions_length Current subscription row length.
1122 * @param subscriptions_capacity Current subscription row capacity.
1123 * @return 1 on success, or 0 on allocation or formatting failure.
1124 */
1126 yyjson_val *root,
1127 long user_id,
1128 char **subscriptions_block,
1129 size_t *subscriptions_length,
1130 size_t *subscriptions_capacity
1131) {
1132 yyjson_val *subscriptions = NULL;
1133 yyjson_arr_iter iter;
1134 yyjson_val *row = NULL;
1135 if (root == NULL || user_id <= 0 || subscriptions_block == NULL || subscriptions_length == NULL || subscriptions_capacity == NULL) {
1136 return 0;
1137 }
1138 subscriptions = state_array_runtime_local(root, "subscriptions");
1139 if (subscriptions == NULL || !yyjson_arr_iter_init(subscriptions, &iter)) {
1140 return 1;
1141 }
1142 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
1143 long row_user_id = 0;
1144 long business_id = 0;
1145 ResourceLinkDisplay business_display;
1146 if (!yyjson_is_obj(row)) {
1147 continue;
1148 }
1149 row_user_id = native_json_obj_get_long_default(row, "user_id", 0, NULL);
1150 if (row_user_id != user_id) {
1151 continue;
1152 }
1153 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
1154 if (!resource_link_display_load_runtime(root, "businesses", business_id, &business_display)) {
1155 continue;
1156 }
1158 subscriptions_block,
1159 subscriptions_length,
1160 subscriptions_capacity,
1161 "<li><a href=\"/b/%s/\">%s</a> <form method=\"post\" action=\"/unsubscribe/%s/\" style=\"display:inline\"><button type=\"submit\" class=\"btn btn-danger\" style=\"padding:0.25rem 0.75rem;font-size:0.875rem\">Unsubscribe</button></form></li>",
1162 business_display.resource_slug != NULL ? business_display.resource_slug : "",
1163 business_display.resource_name != NULL ? business_display.resource_name : "",
1164 business_display.resource_slug != NULL ? business_display.resource_slug : "")) {
1165 resource_link_display_free_runtime(&business_display);
1166 return 0;
1167 }
1168 resource_link_display_free_runtime(&business_display);
1169 }
1170 return 1;
1171}
1172
1173/**
1174 * @brief Builds the profile subscription block for one user.
1175 *
1176 * @param root Loaded runtime state root.
1177 * @param user_id Active signed-in user id.
1178 * @return Newly allocated HTML block, or NULL on failure.
1179 */
1180static char *profile_subscriptions_block_dup_runtime(yyjson_val *root, long user_id) {
1181 char *subscriptions_block = NULL;
1182 size_t subscriptions_length = 0;
1183 size_t subscriptions_capacity = 0;
1184 char *wrapped = NULL;
1185 size_t wrapped_length = 0;
1186 size_t wrapped_capacity = 0;
1188 root,
1189 user_id,
1190 &subscriptions_block,
1191 &subscriptions_length,
1192 &subscriptions_capacity)) {
1193 free(subscriptions_block);
1194 return NULL;
1195 }
1196 if (subscriptions_block == NULL) {
1197 return strdup("<p class=\"muted\">You are not subscribed to any businesses yet.</p>");
1198 }
1199 if (!text_append_runtime_local(&wrapped, &wrapped_length, &wrapped_capacity, "<ul class=\"list-clean stack\">")
1200 || !text_append_runtime_local(&wrapped, &wrapped_length, &wrapped_capacity, subscriptions_block)
1201 || !text_append_runtime_local(&wrapped, &wrapped_length, &wrapped_capacity, "</ul>")) {
1202 free(wrapped);
1203 free(subscriptions_block);
1204 return NULL;
1205 }
1206 free(subscriptions_block);
1207 return wrapped;
1208}
1209
1210static char *generated_admin_manage_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *surface_kind, const char *error_text) {
1211 const GeneratedAdminSurfaceDescriptor *descriptor = NULL;
1212 yyjson_doc *state_doc = NULL;
1213 yyjson_val *root = NULL;
1214 yyjson_val *business = NULL;
1215 yyjson_mut_doc *doc = NULL;
1216 yyjson_mut_val *page = NULL;
1217 char *error_html = NULL;
1218 char *row_html = NULL;
1219 char *form_fields_html = NULL;
1220 char *filter_controls_html = NULL;
1221 char *day_options_html = NULL;
1222 size_t row_html_length = 0;
1223 size_t row_html_capacity = 0;
1224 size_t form_fields_length = 0;
1225 size_t form_fields_capacity = 0;
1226 long business_id = 0;
1227 char *result = NULL;
1228 if (state_path == NULL || business_slug == NULL || surface_kind == NULL) {
1229 return NULL;
1230 }
1231 descriptor = generated_admin_surface_descriptor_runtime(surface_kind);
1232 if (descriptor == NULL) {
1233 return NULL;
1234 }
1235 state_doc = native_json_doc_load_file(state_path);
1236 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
1237 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
1238 if (business == NULL) {
1239 native_json_doc_free(state_doc);
1240 return NULL;
1241 }
1242 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
1243 error_html = error_alert_html_dup_runtime(error_text != NULL ? error_text : "");
1244 doc = yyjson_mut_doc_new(NULL);
1245 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
1246 if (doc == NULL || page == NULL || error_html == NULL) {
1247 free(error_html);
1248 native_json_doc_free(state_doc);
1250 return NULL;
1251 }
1252 yyjson_mut_doc_set_root(doc, page);
1253 if (streq(descriptor->surface_kind, "availability")) {
1254 day_options_html = day_of_week_options_html_dup_runtime("Mon");
1255 if (day_options_html == NULL) {
1256 goto generated_admin_manage_cleanup;
1257 }
1258 }
1260 descriptor,
1261 root,
1262 business_id,
1263 business_slug,
1264 &row_html,
1265 &row_html_length,
1266 &row_html_capacity)) {
1267 goto generated_admin_manage_cleanup;
1268 }
1270 descriptor,
1271 &form_fields_html,
1272 &form_fields_length,
1273 &form_fields_capacity,
1274 day_options_html)) {
1275 goto generated_admin_manage_cleanup;
1276 }
1277 filter_controls_html = generated_admin_surface_filter_controls_dup_runtime(descriptor);
1278 if (descriptor->default_filter_controls_html != NULL && filter_controls_html == NULL) {
1279 goto generated_admin_manage_cleanup;
1280 }
1281 generated_admin_surface_page_copy_runtime(doc, page, descriptor, filter_controls_html);
1282
1283 {
1284 char manage_path[256];
1285 char create_path[256];
1286 char back_path[256];
1287 snprintf(back_path, sizeof(back_path), "/businesses/%s/", business_slug);
1288 snprintf(manage_path, sizeof(manage_path), "/businesses/%s/%s/", business_slug, descriptor->surface_path_segment);
1289 snprintf(create_path, sizeof(create_path), "/businesses/%s/%s/new/", business_slug, descriptor->surface_path_segment);
1290 yyjson_mut_obj_add_strcpy(doc, page, "back_path", back_path);
1291 yyjson_mut_obj_add_strcpy(doc, page, "manage_path", manage_path);
1292 yyjson_mut_obj_add_strcpy(doc, page, "create_path", create_path);
1293 }
1294 yyjson_mut_obj_add_strcpy(doc, page, "error_html", error_html);
1295 yyjson_mut_obj_add_strcpy(doc, page, "form_fields_html", form_fields_html != NULL ? form_fields_html : "");
1296 yyjson_mut_obj_add_strcpy(doc, page, "row_html", row_html != NULL ? row_html : "");
1297 yyjson_mut_obj_add_strcpy(doc, page, "current_page_size", "25");
1298 yyjson_mut_obj_add_strcpy(doc, page, "pagination_summary", "Page 1 of 1 · local state");
1299 result = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
1300
1301generated_admin_manage_cleanup:
1302 free(error_html);
1303 free(row_html);
1304 free(form_fields_html);
1305 free(filter_controls_html);
1306 free(day_options_html);
1307 native_json_doc_free(state_doc);
1309 return result;
1310}
1311
1312/**
1313 * @brief Builds profile-page template params directly from runtime state.
1314 *
1315 * This local builder keeps the profile page on the native dynamic render path
1316 * without depending on the older shared profile params helper.
1317 *
1318 * @param state_path Runtime state JSON path.
1319 * @param user_id Active signed-in user id.
1320 * @param payment_preference_options_html Pre-rendered select option HTML.
1321 * @return Newly allocated compact JSON string, or NULL on failure.
1322 */
1323static char *profile_page_params_json_dup_runtime_local(const char *state_path, long user_id, const char *payment_preference_options_html) {
1324 yyjson_doc *state_doc = NULL;
1325 yyjson_val *root = NULL;
1326 yyjson_val *user = NULL;
1327 char *subscriptions_block = NULL;
1328 char *full_name = NULL;
1329 char *email = NULL;
1330 char *phone_number = NULL;
1331 char *address = NULL;
1332 yyjson_mut_doc *doc = NULL;
1333 yyjson_mut_val *page = NULL;
1334 char *result = NULL;
1335
1336 if (state_path == NULL || user_id <= 0) {
1337 return NULL;
1338 }
1339 state_doc = native_json_doc_load_file(state_path);
1340 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
1341 user = root != NULL ? find_user_by_id_runtime_local(root, user_id) : NULL;
1342 if (root == NULL || user == NULL) {
1343 native_json_doc_free(state_doc);
1344 return NULL;
1345 }
1346
1347 subscriptions_block = profile_subscriptions_block_dup_runtime(root, user_id);
1348 if (subscriptions_block == NULL) {
1349 free(full_name);
1350 free(email);
1351 free(phone_number);
1352 free(address);
1353 native_json_doc_free(state_doc);
1354 return NULL;
1355 }
1356
1357 full_name = native_json_obj_get_string_dup(user, "full_name");
1358 email = native_json_obj_get_string_dup(user, "email");
1359 phone_number = native_json_obj_get_string_dup(user, "phone_number");
1360 address = native_json_obj_get_string_dup(user, "address");
1361 doc = yyjson_mut_doc_new(NULL);
1362 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
1363 if (doc == NULL || page == NULL
1364 || !yyjson_mut_obj_add_strcpy(doc, page, "full_name", full_name != NULL ? full_name : "")
1365 || !yyjson_mut_obj_add_strcpy(doc, page, "email", email != NULL ? email : "")
1366 || !yyjson_mut_obj_add_strcpy(doc, page, "phone_number", phone_number != NULL ? phone_number : "")
1367 || !yyjson_mut_obj_add_strcpy(doc, page, "address", address != NULL ? address : "")
1368 || !yyjson_mut_obj_add_strcpy(doc, page, "payment_preference_options_html", payment_preference_options_html != NULL ? payment_preference_options_html : "")
1369 || !yyjson_mut_obj_add_strcpy(doc, page, "google_status", "Disconnected")
1370 || !yyjson_mut_obj_add_strcpy(doc, page, "subscriptions_block", subscriptions_block != NULL ? subscriptions_block : "")) {
1371 free(full_name);
1372 free(email);
1373 free(phone_number);
1374 free(address);
1375 free(subscriptions_block);
1376 native_json_doc_free(state_doc);
1378 return NULL;
1379 }
1380 yyjson_mut_doc_set_root(doc, page);
1381 result = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
1382 free(full_name);
1383 free(email);
1384 free(phone_number);
1385 free(address);
1386 free(subscriptions_block);
1387 native_json_doc_free(state_doc);
1389 return result;
1390}
1391
1392static char *business_availability_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long availability_id, const char *error_text) {
1393 yyjson_doc *state_doc = NULL;
1394 yyjson_val *root = NULL;
1395 yyjson_val *business = NULL;
1396 yyjson_val *rows = NULL;
1397 yyjson_arr_iter iter;
1398 yyjson_val *row = NULL;
1399 yyjson_mut_doc *doc = NULL;
1400 yyjson_mut_val *page = NULL;
1401 char *day = NULL;
1402 char *start = NULL;
1403 char *end = NULL;
1404 char *day_options_html = NULL;
1405 char *error_html = NULL;
1406 long business_id = 0;
1407 char *result = NULL;
1408 if (state_path == NULL || business_slug == NULL || availability_id <= 0) {
1409 return NULL;
1410 }
1411 state_doc = native_json_doc_load_file(state_path);
1412 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
1413 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
1414 if (business == NULL) {
1415 native_json_doc_free(state_doc);
1416 return NULL;
1417 }
1418 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
1419 rows = state_array_runtime_local(root, "business_availability");
1420 if (rows != NULL && yyjson_arr_iter_init(rows, &iter)) {
1421 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
1422 if (!yyjson_is_obj(row)) continue;
1423 if (native_json_obj_get_long_default(row, "id", 0, NULL) == availability_id
1424 && native_json_obj_get_long_default(row, "business_id", 0, NULL) == business_id) {
1425 day = native_json_obj_get_string_dup(row, "day_of_week");
1426 start = native_json_obj_get_string_dup(row, "start_time");
1427 end = native_json_obj_get_string_dup(row, "end_time");
1428 break;
1429 }
1430 }
1431 }
1432 if (day == NULL && start == NULL && end == NULL) {
1433 native_json_doc_free(state_doc);
1434 return NULL;
1435 }
1436 day_options_html = day_of_week_options_html_dup_runtime(day != NULL ? day : "");
1437 error_html = error_alert_html_dup_runtime(error_text != NULL ? error_text : "");
1438 doc = yyjson_mut_doc_new(NULL);
1439 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
1440 if (doc == NULL || page == NULL || day_options_html == NULL || error_html == NULL) {
1441 free(day); free(start); free(end); free(day_options_html); free(error_html);
1443 return NULL;
1444 }
1445 yyjson_mut_doc_set_root(doc, page);
1446 {
1447 char save_path[256];
1448 char business_path[256];
1449 snprintf(save_path, sizeof(save_path), "/businesses/%s/availability/%ld/edit/", business_slug, availability_id);
1450 snprintf(business_path, sizeof(business_path), "/businesses/%s/", business_slug);
1451 yyjson_mut_obj_add_strcpy(doc, page, "save_path", save_path);
1452 yyjson_mut_obj_add_strcpy(doc, page, "business_path", business_path);
1453 }
1454 yyjson_mut_obj_add_strcpy(doc, page, "error_html", error_html);
1455 yyjson_mut_obj_add_strcpy(doc, page, "day_options_html", day_options_html);
1456 yyjson_mut_obj_add_strcpy(doc, page, "start_time", start != NULL ? start : "");
1457 yyjson_mut_obj_add_strcpy(doc, page, "end_time", end != NULL ? end : "");
1458 result = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
1459 free(day); free(start); free(end); free(day_options_html); free(error_html);
1461 return result;
1462}
1463
1464/**
1465 * @brief Tests whether a request path is the exact business services list API path.
1466 *
1467 * Matches only @c /api/businesses/{business_slug}/services/ and avoids
1468 * hijacking other four-segment paths that happen to contain a slug-like
1469 * segment in the same position.
1470 *
1471 * @param request_path Normalized request path without query string.
1472 * @return 1 when the path is the exact services list API path, else 0.
1473 */
1474static int is_business_services_api_list_path_runtime(const char *request_path) {
1475 static const char *prefix = "/api/businesses/";
1476 static const char *suffix = "/services/";
1477 const char *slug_start;
1478 const char *suffix_start;
1479 size_t prefix_len;
1480 size_t suffix_len;
1481 size_t slug_len;
1482 size_t path_len;
1483
1484 if (request_path == NULL) {
1485 return 0;
1486 }
1487 prefix_len = strlen(prefix);
1488 suffix_len = strlen(suffix);
1489 path_len = strlen(request_path);
1490 if (path_len <= prefix_len + suffix_len) {
1491 return 0;
1492 }
1493 if (strncmp(request_path, prefix, prefix_len) != 0) {
1494 return 0;
1495 }
1496 suffix_start = request_path + (path_len - suffix_len);
1497 if (strcmp(suffix_start, suffix) != 0) {
1498 return 0;
1499 }
1500 slug_start = request_path + prefix_len;
1501 slug_len = (size_t)(suffix_start - slug_start);
1502 if (slug_len == 0) {
1503 return 0;
1504 }
1505 return memchr(slug_start, '/', slug_len) == NULL ? 1 : 0;
1506}
1507
1508/**
1509 * @brief Tests whether a request path is the exact business availability list API path.
1510 *
1511 * Matches only @c /api/businesses/{business_slug}/availability/.
1512 *
1513 * @param request_path Normalized request path without query string.
1514 * @return 1 when the path is the exact availability list API path, else 0.
1515 */
1516static int is_business_availability_api_list_path_runtime(const char *request_path) {
1517 static const char *prefix = "/api/businesses/";
1518 static const char *suffix = "/availability/";
1519 const char *slug_start;
1520 const char *suffix_start;
1521 size_t prefix_len;
1522 size_t suffix_len;
1523 size_t slug_len;
1524 size_t path_len;
1525
1526 if (request_path == NULL) {
1527 return 0;
1528 }
1529 prefix_len = strlen(prefix);
1530 suffix_len = strlen(suffix);
1531 path_len = strlen(request_path);
1532 if (path_len <= prefix_len + suffix_len) {
1533 return 0;
1534 }
1535 if (strncmp(request_path, prefix, prefix_len) != 0) {
1536 return 0;
1537 }
1538 suffix_start = request_path + (path_len - suffix_len);
1539 if (strcmp(suffix_start, suffix) != 0) {
1540 return 0;
1541 }
1542 slug_start = request_path + prefix_len;
1543 slug_len = (size_t)(suffix_start - slug_start);
1544 if (slug_len == 0) {
1545 return 0;
1546 }
1547 return memchr(slug_start, '/', slug_len) == NULL ? 1 : 0;
1548}
1549
1550/**
1551 * @brief Tests whether a request path is the exact business services schema API path.
1552 * @param request_path Normalized request path without query string.
1553 * @return 1 when the path is the exact services schema API path, else 0.
1554 */
1555static int is_business_services_api_schema_path_runtime(const char *request_path) {
1556 static const char *prefix = "/api/businesses/";
1557 static const char *suffix = "/services/schema/";
1558 const char *slug_start;
1559 const char *suffix_start;
1560 size_t prefix_len;
1561 size_t suffix_len;
1562 size_t slug_len;
1563 size_t path_len;
1564
1565 if (request_path == NULL) {
1566 return 0;
1567 }
1568 prefix_len = strlen(prefix);
1569 suffix_len = strlen(suffix);
1570 path_len = strlen(request_path);
1571 if (path_len <= prefix_len + suffix_len) {
1572 return 0;
1573 }
1574 if (strncmp(request_path, prefix, prefix_len) != 0) {
1575 return 0;
1576 }
1577 suffix_start = request_path + (path_len - suffix_len);
1578 if (strcmp(suffix_start, suffix) != 0) {
1579 return 0;
1580 }
1581 slug_start = request_path + prefix_len;
1582 slug_len = (size_t)(suffix_start - slug_start);
1583 if (slug_len == 0) {
1584 return 0;
1585 }
1586 return memchr(slug_start, '/', slug_len) == NULL ? 1 : 0;
1587}
1588
1589/**
1590 * @brief Tests whether a request path is the exact business availability schema API path.
1591 * @param request_path Normalized request path without query string.
1592 * @return 1 when the path is the exact availability schema API path, else 0.
1593 */
1594static int is_business_availability_api_schema_path_runtime(const char *request_path) {
1595 static const char *prefix = "/api/businesses/";
1596 static const char *suffix = "/availability/schema/";
1597 const char *slug_start;
1598 const char *suffix_start;
1599 size_t prefix_len;
1600 size_t suffix_len;
1601 size_t slug_len;
1602 size_t path_len;
1603
1604 if (request_path == NULL) {
1605 return 0;
1606 }
1607 prefix_len = strlen(prefix);
1608 suffix_len = strlen(suffix);
1609 path_len = strlen(request_path);
1610 if (path_len <= prefix_len + suffix_len) {
1611 return 0;
1612 }
1613 if (strncmp(request_path, prefix, prefix_len) != 0) {
1614 return 0;
1615 }
1616 suffix_start = request_path + (path_len - suffix_len);
1617 if (strcmp(suffix_start, suffix) != 0) {
1618 return 0;
1619 }
1620 slug_start = request_path + prefix_len;
1621 slug_len = (size_t)(suffix_start - slug_start);
1622 if (slug_len == 0) {
1623 return 0;
1624 }
1625 return memchr(slug_start, '/', slug_len) == NULL ? 1 : 0;
1626}
1627
1628/**
1629 * @brief Resolves the final page title from a declarative page spec and params.
1630 *
1631 * Prefers an explicit @c title value from the page spec. When the spec uses
1632 * @c title_mode instead, this treats the title-mode value as the params field
1633 * name whose string value should supply the page title.
1634 *
1635 * @param page_spec_root Loaded page-spec JSON root.
1636 * @param body_params_root Loaded page-params JSON root.
1637 * @return Newly allocated page title string, or an empty string on fallback.
1638 * Caller must free.
1639 */
1640static int valid_day_of_week_runtime(const char *day_text) {
1641 return day_text != NULL && (
1642 streq(day_text, "Mon") || streq(day_text, "Tue") || streq(day_text, "Wed") ||
1643 streq(day_text, "Thu") || streq(day_text, "Fri") || streq(day_text, "Sat") || streq(day_text, "Sun")
1644 );
1645}
1646
1647static int time_text_to_minutes_runtime(const char *time_text, int *minutes_out) {
1648 int hour = 0;
1649 int minute = 0;
1650 if (minutes_out != NULL) {
1651 *minutes_out = 0;
1652 }
1653 if (time_text == NULL || strlen(time_text) != 5 || time_text[2] != ':') {
1654 return 0;
1655 }
1656 if (time_text[0] < '0' || time_text[0] > '9' || time_text[1] < '0' || time_text[1] > '9'
1657 || time_text[3] < '0' || time_text[3] > '9' || time_text[4] < '0' || time_text[4] > '9') {
1658 return 0;
1659 }
1660 hour = (time_text[0] - '0') * 10 + (time_text[1] - '0');
1661 minute = (time_text[3] - '0') * 10 + (time_text[4] - '0');
1662 if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
1663 return 0;
1664 }
1665 if (minutes_out != NULL) {
1666 *minutes_out = hour * 60 + minute;
1667 }
1668 return 1;
1669}
1670
1671static char *page_title_dup_runtime(yyjson_val *page_spec_root, yyjson_val *body_params_root) {
1672 char *title = NULL;
1673 char *title_mode = NULL;
1674 if (page_spec_root != NULL) {
1675 title = native_json_obj_get_string_dup(page_spec_root, "title");
1676 title_mode = native_json_obj_get_string_dup(page_spec_root, "title_mode");
1677 }
1678 if ((title == NULL || title[0] == '\0') && title_mode != NULL && title_mode[0] != '\0' && body_params_root != NULL) {
1679 free(title);
1680 title = native_json_obj_get_string_dup(body_params_root, title_mode);
1681 }
1682 free(title_mode);
1683 if (title == NULL) {
1684 title = strdup("");
1685 }
1686 return title;
1687}
1688
1690 NativeConnection *connection,
1691 const char *state_path,
1692 const char *business_slug
1693) {
1694 yyjson_doc *state_doc = NULL;
1695 yyjson_val *root = NULL;
1696 yyjson_val *business = NULL;
1697 yyjson_val *services = NULL;
1698 yyjson_arr_iter iter;
1699 yyjson_val *service = NULL;
1700 yyjson_mut_doc *out_doc = NULL;
1701 yyjson_mut_val *out_root = NULL;
1702 yyjson_mut_val *items = NULL;
1703 char *body = NULL;
1704 long business_id = 0;
1705 int status = 500;
1706
1707 if (connection == NULL || state_path == NULL || business_slug == NULL) {
1708 return 500;
1709 }
1710 state_doc = native_json_doc_load_file(state_path);
1711 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
1712 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
1713 if (business == NULL) {
1714 write_json_error(connection, 404, "Not Found", "not_found", "Business not found.");
1715 native_json_doc_free(state_doc);
1716 return 404;
1717 }
1718 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
1719 services = root != NULL ? state_array_runtime_local(root, "services") : NULL;
1720 out_doc = yyjson_mut_doc_new(NULL);
1721 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
1722 items = out_doc != NULL ? yyjson_mut_arr(out_doc) : NULL;
1723 if (out_doc == NULL || out_root == NULL || items == NULL) {
1724 native_json_doc_free(state_doc);
1725 yyjson_mut_doc_free(out_doc);
1726 native_send_text_response(connection, 500, "Internal Server Error", "service api render failed\n");
1727 return 500;
1728 }
1729 yyjson_mut_doc_set_root(out_doc, out_root);
1730 if (services != NULL && yyjson_arr_iter_init(services, &iter)) {
1731 while ((service = yyjson_arr_iter_next(&iter)) != NULL) {
1732 yyjson_mut_val *entry = NULL;
1733 if (!yyjson_is_obj(service) || native_json_obj_get_long_default(service, "business_id", 0, NULL) != business_id) {
1734 continue;
1735 }
1736 entry = yyjson_val_mut_copy(out_doc, service);
1737 if (entry != NULL) {
1738 yyjson_mut_arr_append(items, entry);
1739 }
1740 }
1741 }
1742 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "items"), items);
1743 body = yyjson_mut_write(out_doc, YYJSON_WRITE_NOFLAG, NULL);
1744 if (body == NULL) {
1745 native_json_doc_free(state_doc);
1746 yyjson_mut_doc_free(out_doc);
1747 native_send_text_response(connection, 500, "Internal Server Error", "service api serialize failed\n");
1748 return 500;
1749 }
1751 connection,
1752 "HTTP/1.1 200 OK\r\n"
1753 "Content-Type: application/json\r\n"
1754 "Content-Length: %d\r\n"
1755 "Cache-Control: no-store\r\n"
1756 "Connection: close\r\n"
1757 "\r\n%s",
1758 (int)strlen(body),
1759 body
1760 );
1761 status = 200;
1762 free(body);
1763 native_json_doc_free(state_doc);
1764 yyjson_mut_doc_free(out_doc);
1765 return status;
1766}
1767
1769 NativeConnection *connection,
1770 const char *state_path,
1771 const char *business_slug
1772) {
1773 yyjson_doc *state_doc = NULL;
1774 yyjson_val *root = NULL;
1775 yyjson_val *business = NULL;
1776 yyjson_val *availability_rows = NULL;
1777 yyjson_arr_iter iter;
1778 yyjson_val *row = NULL;
1779 yyjson_mut_doc *out_doc = NULL;
1780 yyjson_mut_val *out_root = NULL;
1781 yyjson_mut_val *items = NULL;
1782 char *body = NULL;
1783 long business_id = 0;
1784 int status = 500;
1785
1786 if (connection == NULL || state_path == NULL || business_slug == NULL) {
1787 return 500;
1788 }
1789 state_doc = native_json_doc_load_file(state_path);
1790 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
1791 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
1792 if (business == NULL) {
1793 write_json_error(connection, 404, "Not Found", "not_found", "Business not found.");
1794 native_json_doc_free(state_doc);
1795 return 404;
1796 }
1797 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
1798 availability_rows = root != NULL ? state_array_runtime_local(root, "business_availability") : NULL;
1799 out_doc = yyjson_mut_doc_new(NULL);
1800 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
1801 items = out_doc != NULL ? yyjson_mut_arr(out_doc) : NULL;
1802 if (out_doc == NULL || out_root == NULL || items == NULL) {
1803 native_json_doc_free(state_doc);
1804 yyjson_mut_doc_free(out_doc);
1805 native_send_text_response(connection, 500, "Internal Server Error", "availability api render failed\n");
1806 return 500;
1807 }
1808 yyjson_mut_doc_set_root(out_doc, out_root);
1809 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
1810 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
1811 yyjson_mut_val *entry = NULL;
1812 if (!yyjson_is_obj(row) || native_json_obj_get_long_default(row, "business_id", 0, NULL) != business_id) {
1813 continue;
1814 }
1815 entry = yyjson_val_mut_copy(out_doc, row);
1816 if (entry != NULL) {
1817 yyjson_mut_arr_append(items, entry);
1818 }
1819 }
1820 }
1821 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "items"), items);
1822 body = yyjson_mut_write(out_doc, YYJSON_WRITE_NOFLAG, NULL);
1823 if (body == NULL) {
1824 native_json_doc_free(state_doc);
1825 yyjson_mut_doc_free(out_doc);
1826 native_send_text_response(connection, 500, "Internal Server Error", "availability api serialize failed\n");
1827 return 500;
1828 }
1830 connection,
1831 "HTTP/1.1 200 OK\r\n"
1832 "Content-Type: application/json\r\n"
1833 "Content-Length: %d\r\n"
1834 "Cache-Control: no-store\r\n"
1835 "Connection: close\r\n"
1836 "\r\n%s",
1837 (int)strlen(body),
1838 body
1839 );
1840 status = 200;
1841 free(body);
1842 native_json_doc_free(state_doc);
1843 yyjson_mut_doc_free(out_doc);
1844 return status;
1845}
1846
1847/**
1848 * @brief Resolves the generated-surface fixture path for the active app.
1849 *
1850 * Prefers the built-artifact copy and falls back to the app-root copy.
1851 *
1852 * @param app Active app runtime.
1853 * @return Newly allocated path string, or NULL when unavailable.
1854 * Caller must free.
1855 */
1857 char *path = NULL;
1858 if (app == NULL) {
1859 return NULL;
1860 }
1861 if (app->artifact_dir != NULL) {
1862 path = join_base_relative_path_dup_runtime(app->artifact_dir, "data/framework/generated-surface-fixture-v1.json");
1863 if (path != NULL && path_exists(path)) {
1864 return path;
1865 }
1866 free(path);
1867 }
1868 if (app->app_root != NULL) {
1869 path = join_base_relative_path_dup_runtime(app->app_root, "data/framework/generated-surface-fixture-v1.json");
1870 if (path != NULL && path_exists(path)) {
1871 return path;
1872 }
1873 free(path);
1874 }
1875 return NULL;
1876}
1877
1878/**
1879 * @brief Writes one generated-surface schema object from the declarative fixture.
1880 *
1881 * @param connection Connection to write to.
1882 * @param app Active app runtime for fixture resolution.
1883 * @param state_path Runtime state path used to validate the business slug.
1884 * @param business_slug Business slug from the request path.
1885 * @param surface_id Schema surface id to emit.
1886 * @return HTTP status code written to the client.
1887 */
1889 NativeConnection *connection,
1890 const AppRuntime *app,
1891 const char *state_path,
1892 const char *business_slug,
1893 const char *surface_id
1894) {
1895 yyjson_doc *state_doc = NULL;
1896 yyjson_val *state_root = NULL;
1897 yyjson_val *business = NULL;
1898 char *fixture_path = NULL;
1899 yyjson_doc *fixture_doc = NULL;
1900 yyjson_val *fixture_root = NULL;
1901 yyjson_val *surfaces = NULL;
1902 yyjson_arr_iter iter;
1903 yyjson_val *surface = NULL;
1904 yyjson_mut_doc *out_doc = NULL;
1905 yyjson_mut_val *out_surface = NULL;
1906 char *body = NULL;
1907
1908 if (connection == NULL || app == NULL || state_path == NULL || business_slug == NULL || surface_id == NULL) {
1909 return 500;
1910 }
1911 state_doc = native_json_doc_load_file(state_path);
1912 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
1913 business = state_root != NULL ? find_business_by_slug_runtime_local(state_root, business_slug) : NULL;
1914 if (business == NULL) {
1915 write_json_error(connection, 404, "Not Found", "not_found", "Business not found.");
1916 native_json_doc_free(state_doc);
1917 return 404;
1918 }
1920 fixture_doc = fixture_path != NULL ? native_json_doc_load_file(fixture_path) : NULL;
1921 fixture_root = fixture_doc != NULL ? yyjson_doc_get_root(fixture_doc) : NULL;
1922 surfaces = fixture_root != NULL ? native_json_obj_get(fixture_root, "surfaces") : NULL;
1923 if (surfaces != NULL && yyjson_is_arr(surfaces) && yyjson_arr_iter_init(surfaces, &iter)) {
1924 while ((surface = yyjson_arr_iter_next(&iter)) != NULL) {
1925 yyjson_val *surface_id_value = native_json_obj_get(surface, "surface_id");
1926 if (yyjson_is_str(surface_id_value) && yyjson_equals_str(surface_id_value, surface_id)) {
1927 break;
1928 }
1929 surface = NULL;
1930 }
1931 }
1932 if (surface == NULL) {
1933 free(fixture_path);
1934 native_json_doc_free(state_doc);
1935 native_json_doc_free(fixture_doc);
1936 write_json_error(connection, 404, "Not Found", "not_found", "Generated surface schema not found.");
1937 return 404;
1938 }
1939 out_doc = yyjson_mut_doc_new(NULL);
1940 out_surface = out_doc != NULL ? yyjson_val_mut_copy(out_doc, surface) : NULL;
1941 if (out_doc == NULL || out_surface == NULL) {
1942 free(fixture_path);
1943 native_json_doc_free(state_doc);
1944 native_json_doc_free(fixture_doc);
1945 yyjson_mut_doc_free(out_doc);
1946 native_send_text_response(connection, 500, "Internal Server Error", "schema render failed\n");
1947 return 500;
1948 }
1949 yyjson_mut_doc_set_root(out_doc, out_surface);
1950 body = yyjson_mut_write(out_doc, YYJSON_WRITE_NOFLAG, NULL);
1951 if (body == NULL) {
1952 free(fixture_path);
1953 native_json_doc_free(state_doc);
1954 native_json_doc_free(fixture_doc);
1955 yyjson_mut_doc_free(out_doc);
1956 native_send_text_response(connection, 500, "Internal Server Error", "schema serialize failed\n");
1957 return 500;
1958 }
1960 connection,
1961 "HTTP/1.1 200 OK\r\n"
1962 "Content-Type: application/json\r\n"
1963 "Content-Length: %d\r\n"
1964 "Cache-Control: no-store\r\n"
1965 "Connection: close\r\n"
1966 "\r\n%s",
1967 (int)strlen(body),
1968 body
1969 );
1970 free(body);
1971 free(fixture_path);
1972 native_json_doc_free(state_doc);
1973 native_json_doc_free(fixture_doc);
1974 yyjson_mut_doc_free(out_doc);
1975 return 200;
1976}
1977
1978static char *login_page_params_json_dup_runtime(const char *query_string, const char *error_message) {
1979 yyjson_mut_doc *doc = NULL;
1980 yyjson_mut_val *root = NULL;
1981 char *params_json = NULL;
1982 char *next_path = native_dynamic_query_value(query_string, "next");
1983 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
1984 char *next_input = hidden_next_input_html_dup_runtime(next_path != NULL ? next_path : "");
1985
1986 doc = yyjson_mut_doc_new(NULL);
1987 root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
1988 if (doc == NULL || root == NULL
1989 || error_html == NULL || next_input == NULL
1990 || !yyjson_mut_obj_add_strcpy(doc, root, "error_html", error_html)
1991 || !yyjson_mut_obj_add_strcpy(doc, root, "next_input", next_input)) {
1993 free(next_path);
1994 free(error_html);
1995 free(next_input);
1996 return NULL;
1997 }
1998 yyjson_mut_doc_set_root(doc, root);
1999 params_json = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
2001 free(next_path);
2002 free(error_html);
2003 free(next_input);
2004 return params_json;
2005}
2006
2007static char *register_page_params_json_dup_runtime(const char *query_string) {
2008 yyjson_mut_doc *doc = NULL;
2009 yyjson_mut_val *root = NULL;
2010 char *params_json = NULL;
2011 char *next_path = native_dynamic_query_value(query_string, "next");
2012 char *error_html = error_alert_html_dup_runtime("");
2013 char *next_input = hidden_next_input_html_dup_runtime(next_path != NULL ? next_path : "");
2014 const char *account_type_options_html =
2015 "<option value=\"business_customer\" selected>Customer</option>"
2016 "<option value=\"business_owner\">Business owner</option>";
2017
2018 doc = yyjson_mut_doc_new(NULL);
2019 root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2020 if (doc == NULL || root == NULL
2021 || error_html == NULL || next_input == NULL
2022 || !yyjson_mut_obj_add_strcpy(doc, root, "error_html", error_html)
2023 || !yyjson_mut_obj_add_strcpy(doc, root, "next_input", next_input)
2024 || !yyjson_mut_obj_add_strcpy(doc, root, "account_type_options_html", account_type_options_html)) {
2026 free(next_path);
2027 free(error_html);
2028 free(next_input);
2029 return NULL;
2030 }
2031 yyjson_mut_doc_set_root(doc, root);
2032 params_json = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
2034 free(next_path);
2035 free(error_html);
2036 free(next_input);
2037 return params_json;
2038}
2039
2040/**
2041 * @brief Builds native template params for the password-reset request page.
2042 *
2043 * @return Newly allocated compact JSON string, or NULL on failure.
2044 */
2045/**
2046 * @brief Adds one literal string field to a page-params JSON object.
2047 *
2048 * @param doc Mutable params document.
2049 * @param root Mutable params object.
2050 * @param key Field name to populate.
2051 * @param value Raw string value to store.
2052 * @return 1 on success, or 0 on allocation failure.
2053 */
2055 yyjson_mut_doc *doc,
2056 yyjson_mut_val *root,
2057 const char *key,
2058 const char *value
2059) {
2060 if (doc == NULL || root == NULL || key == NULL || key[0] == '\0') {
2061 return 0;
2062 }
2063 return yyjson_mut_obj_add_strcpy(doc, root, key, value != NULL ? value : "") ? 1 : 0;
2064}
2065
2066/**
2067 * @brief Adds one HTML-escaped string field to a page-params JSON object.
2068 *
2069 * @param doc Mutable params document.
2070 * @param root Mutable params object.
2071 * @param key Field name to populate.
2072 * @param value Raw string value to escape before storing.
2073 * @return 1 on success, or 0 on allocation or escaping failure.
2074 */
2076 yyjson_mut_doc *doc,
2077 yyjson_mut_val *root,
2078 const char *key,
2079 const char *value
2080) {
2081 char *escaped_value = NULL;
2082 int added = 0;
2083 if (doc == NULL || root == NULL || key == NULL || key[0] == '\0') {
2084 return 0;
2085 }
2086 escaped_value = html_escaped_dup_runtime(value != NULL ? value : "");
2087 if (escaped_value == NULL) {
2088 return 0;
2089 }
2090 added = yyjson_mut_obj_add_strcpy(doc, root, key, escaped_value) ? 1 : 0;
2091 free(escaped_value);
2092 return added;
2093}
2094
2095/**
2096 * @brief Finalizes and writes one page-params document as compact JSON.
2097 *
2098 * @param doc Mutable params document.
2099 * @param root Mutable params object.
2100 * @return Newly allocated compact JSON string, or NULL on failure.
2101 */
2103 if (doc == NULL || root == NULL) {
2104 return NULL;
2105 }
2106 yyjson_mut_doc_set_root(doc, root);
2107 return yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
2108}
2109
2110/**
2111 * @brief Builds the optional password-reset preview HTML block.
2112 *
2113 * @param preview_path Local preview path to expose.
2114 * @return Newly allocated HTML fragment, or an empty string when no preview path exists.
2115 */
2116static char *password_reset_preview_html_dup_runtime(const char *preview_path) {
2117 size_t needed = 0;
2118 char *preview_html = NULL;
2119 if (preview_path == NULL || preview_path[0] == '\0') {
2120 return strdup("");
2121 }
2122 needed = strlen(preview_path) * 2 + 128;
2123 preview_html = (char *)malloc(needed);
2124 if (preview_html == NULL) {
2125 return NULL;
2126 }
2127 snprintf(
2128 preview_html,
2129 needed,
2130 "<p>Local runtime preview link:</p><p><a href=\"%s\"><code>%s</code></a></p>",
2131 preview_path,
2132 preview_path
2133 );
2134 return preview_html;
2135}
2136
2137/**
2138 * @brief Builds the optional invite target HTML fragment.
2139 *
2140 * @param target_business_name Optional invite-associated business display name.
2141 * @return Newly allocated HTML fragment, or an empty string when no target exists.
2142 */
2143static char *invite_accept_target_html_dup_runtime(const char *target_business_name) {
2144 char *escaped_target_business_name = NULL;
2145 char *target_html = NULL;
2146 size_t target_length = 0;
2147 size_t target_capacity = 0;
2148 if (target_business_name == NULL || target_business_name[0] == '\0') {
2149 return strdup("");
2150 }
2151 escaped_target_business_name = html_escaped_dup_runtime(target_business_name);
2152 if (escaped_target_business_name == NULL
2153 || !text_append_runtime_local(&target_html, &target_length, &target_capacity, "<p>This invite is associated with <strong>")
2154 || !text_append_runtime_local(&target_html, &target_length, &target_capacity, escaped_target_business_name)
2155 || !text_append_runtime_local(&target_html, &target_length, &target_capacity, "</strong>.</p>")) {
2156 free(escaped_target_business_name);
2157 free(target_html);
2158 return NULL;
2159 }
2160 free(escaped_target_business_name);
2161 return target_html;
2162}
2163
2166 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2167 char *error_html = error_alert_html_dup_runtime("");
2168 char *params_json = NULL;
2169 if (doc == NULL || root == NULL || error_html == NULL
2170 || !runtime_page_params_add_string_runtime(doc, root, "error_html", error_html)) {
2172 free(error_html);
2173 return NULL;
2174 }
2175 params_json = runtime_page_params_write_runtime(doc, root);
2177 free(error_html);
2178 return params_json;
2179}
2180
2181static char *password_reset_sent_page_params_json_dup_runtime(const char *preview_path) {
2183 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2184 char *params_json = NULL;
2185 char *preview_html = password_reset_preview_html_dup_runtime(preview_path);
2186 if (doc == NULL || root == NULL || preview_html == NULL
2187 || !runtime_page_params_add_string_runtime(doc, root, "preview_html", preview_html)) {
2189 free(preview_html);
2190 return NULL;
2191 }
2192 params_json = runtime_page_params_write_runtime(doc, root);
2194 free(preview_html);
2195 return params_json;
2196}
2197
2198/**
2199 * @brief Builds native template params for the password-reset completion page.
2200 *
2201 * @param request_path Matched request path that contains the reset token.
2202 * @return Newly allocated compact JSON string, or NULL on failure.
2203 */
2204static char *password_reset_complete_page_params_json_dup_runtime(const char *request_path) {
2206 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2207 char *error_html = error_alert_html_dup_runtime("");
2208 char *params_json = NULL;
2209 if (doc == NULL || root == NULL || error_html == NULL
2210 || !runtime_page_params_add_string_runtime(doc, root, "error_html", error_html)
2211 || !runtime_page_params_add_escaped_string_runtime(doc, root, "reset_post_path", request_path != NULL ? request_path : "")) {
2213 free(error_html);
2214 return NULL;
2215 }
2216 params_json = runtime_page_params_write_runtime(doc, root);
2218 free(error_html);
2219 return params_json;
2220}
2221
2222/**
2223 * @brief Builds template params for the invite-created confirmation page.
2224 *
2225 * @param invite_email Invite target email.
2226 * @param invite_path Native preview path for the invite token.
2227 * @param return_path Business return path.
2228 * @return Newly allocated compact JSON string, or NULL on failure.
2229 */
2230static char *invite_created_page_params_json_dup_runtime(const char *invite_email, const char *invite_path, const char *return_path) {
2232 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2233 char *params_json = NULL;
2234 if (doc == NULL || root == NULL
2235 || !runtime_page_params_add_escaped_string_runtime(doc, root, "invite_email", invite_email != NULL ? invite_email : "")
2236 || !runtime_page_params_add_escaped_string_runtime(doc, root, "invite_path", invite_path != NULL ? invite_path : "")
2237 || !runtime_page_params_add_escaped_string_runtime(doc, root, "return_path", return_path != NULL ? return_path : "")) {
2239 return NULL;
2240 }
2241 params_json = runtime_page_params_write_runtime(doc, root);
2243 return params_json;
2244}
2245
2246/**
2247 * @brief Builds template params for the invite-accept page.
2248 *
2249 * @param invite_kind Invite kind label.
2250 * @param target_business_name Optional business name attached to the invite.
2251 * @param error_message Optional error text.
2252 * @param invite_email Invite email.
2253 * @param form_action Form POST target.
2254 * @return Newly allocated compact JSON string, or NULL on failure.
2255 */
2256static char *invite_accept_page_params_json_dup_runtime(const char *invite_kind, const char *target_business_name, const char *error_message, const char *invite_email, const char *form_action) {
2258 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2259 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
2260 char *target_html = invite_accept_target_html_dup_runtime(target_business_name);
2261 char *params_json = NULL;
2262 if (doc == NULL || root == NULL || error_html == NULL || target_html == NULL
2263 || !runtime_page_params_add_escaped_string_runtime(doc, root, "invite_kind", invite_kind != NULL ? invite_kind : "business_invite")
2264 || !runtime_page_params_add_string_runtime(doc, root, "target_html", target_html)
2265 || !runtime_page_params_add_string_runtime(doc, root, "error_html", error_html)
2266 || !runtime_page_params_add_escaped_string_runtime(doc, root, "invite_email", invite_email != NULL ? invite_email : "")
2267 || !runtime_page_params_add_escaped_string_runtime(doc, root, "form_action", form_action != NULL ? form_action : "")) {
2269 free(error_html);
2270 free(target_html);
2271 return NULL;
2272 }
2273 params_json = runtime_page_params_write_runtime(doc, root);
2275 free(error_html);
2276 free(target_html);
2277 return params_json;
2278}
2279
2280/**
2281 * @brief Builds template params for the invite-accepted confirmation page.
2282 *
2283 * @param continue_path Post-accept landing path.
2284 * @return Newly allocated compact JSON string, or NULL on failure.
2285 */
2286static char *invite_accept_done_page_params_json_dup_runtime(const char *continue_path) {
2288 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2289 char *params_json = NULL;
2290 if (doc == NULL || root == NULL
2291 || !runtime_page_params_add_escaped_string_runtime(doc, root, "continue_path", continue_path != NULL ? continue_path : "/explore/")) {
2293 return NULL;
2294 }
2295 params_json = runtime_page_params_write_runtime(doc, root);
2297 return params_json;
2298}
2299
2300static char *service_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long service_id) {
2301 yyjson_doc *state_doc = NULL;
2302 yyjson_val *root = NULL;
2303 yyjson_val *business = NULL;
2304 yyjson_val *service = NULL;
2305 yyjson_val *services = NULL;
2306 yyjson_val *technicians = NULL;
2307 yyjson_mut_doc *doc = NULL;
2308 yyjson_mut_val *page = NULL;
2309 yyjson_arr_iter iter;
2310 yyjson_val *row = NULL;
2311 char *sort_options_html = NULL;
2312 size_t sort_options_len = 0;
2313 size_t sort_options_cap = 0;
2314 char *option_rows_html = NULL;
2315 size_t option_rows_len = 0;
2316 size_t option_rows_cap = 0;
2317 char *technician_boxes_html = NULL;
2318 size_t technician_boxes_len = 0;
2319 size_t technician_boxes_cap = 0;
2320 char *service_name = NULL;
2321 char *service_description = NULL;
2322 char *service_cost = NULL;
2323 char *service_image = NULL;
2324 char *params_json = NULL;
2325 long business_id = 0;
2326 long service_duration = 45;
2327 long service_sort_order = 1;
2328 long service_count = 0;
2329 int technician_emitted = 0;
2330
2331 if (state_path == NULL || business_slug == NULL || business_slug[0] == '\0' || service_id <= 0) {
2332 return NULL;
2333 }
2334 state_doc = native_json_doc_load_file(state_path);
2335 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
2336 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
2337 business_id = business != NULL ? native_json_obj_get_long_default(business, "id", 0, NULL) : 0;
2338 service = root != NULL ? find_service_for_business_runtime_local(root, service_id, business_id) : NULL;
2339 if (business == NULL || service == NULL) {
2340 native_json_doc_free(state_doc);
2341 return NULL;
2342 }
2343
2344 service_name = native_json_obj_get_string_dup(service, "name");
2345 service_description = native_json_obj_get_string_dup(service, "description");
2346 service_cost = native_json_obj_get_string_dup(service, "cost");
2347 service_image = native_json_obj_get_string_dup(service, "image_url");
2348 service_duration = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
2349 service_sort_order = native_json_obj_get_long_default(service, "sort_order", service_id, NULL);
2350 if (service_duration <= 0) service_duration = 45;
2351 if (service_sort_order <= 0) service_sort_order = service_id;
2352
2353 services = state_array_runtime_local(root, "services");
2354 if (services != NULL && yyjson_arr_iter_init(services, &iter)) {
2355 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
2356 if (yyjson_is_obj(row) && native_json_obj_get_long_default(row, "business_id", 0, NULL) == business_id) {
2357 service_count++;
2358 }
2359 }
2360 }
2361 if (service_count <= 0) service_count = 1;
2362 for (long index = 1; index <= service_count; index++) {
2363 if (!text_append_format_runtime_local(&sort_options_html, &sort_options_len, &sort_options_cap,
2364 "<option value=\"%ld\"%s>%ld</option>",
2365 index,
2366 index == service_sort_order ? " selected" : "",
2367 index)) {
2368 goto service_edit_cleanup;
2369 }
2370 }
2371
2372 {
2373 yyjson_val *booking_options = native_json_obj_get(service, "booking_options");
2374 long option_index = 0;
2375 if (booking_options != NULL && yyjson_is_arr(booking_options) && yyjson_arr_iter_init(booking_options, &iter)) {
2376 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
2377 char *label = native_json_obj_get_string_dup(row, "label");
2378 char *cost = native_json_obj_get_string_dup(row, "cost");
2379 long duration = native_json_obj_get_long_default(row, "duration_minutes", 0, NULL);
2380 option_index++;
2381 if (!text_append_format_runtime_local(&option_rows_html, &option_rows_len, &option_rows_cap,
2382 "<div class=\"stack\"><label>Option Label <input type=\"text\" name=\"booking_option_label_%ld\" value=\"%s\" class=\"form-control\"></label><label>Option Duration <input type=\"number\" name=\"booking_option_duration_%ld\" value=\"%ld\" class=\"form-control\"></label><label>Option Cost <input type=\"text\" name=\"booking_option_cost_%ld\" value=\"%s\" class=\"form-control\"></label></div>",
2383 option_index, label != NULL ? label : "", option_index, duration, option_index, cost != NULL ? cost : "")) {
2384 free(label);
2385 free(cost);
2386 goto service_edit_cleanup;
2387 }
2388 free(label);
2389 free(cost);
2390 }
2391 }
2392 if (option_index == 0) {
2393 if (!text_append_format_runtime_local(&option_rows_html, &option_rows_len, &option_rows_cap,
2394 "<input type=\"hidden\" name=\"booking_option_count\" value=\"0\">")) {
2395 goto service_edit_cleanup;
2396 }
2397 } else {
2398 char *wrapped_html = NULL;
2399 size_t wrapped_len = 0;
2400 size_t wrapped_cap = 0;
2401 if (!text_append_format_runtime_local(&wrapped_html, &wrapped_len, &wrapped_cap,
2402 "<input type=\"hidden\" name=\"booking_option_count\" value=\"%ld\"><div class=\"stack\">%s</div>",
2403 option_index,
2404 option_rows_html != NULL ? option_rows_html : "")) {
2405 free(wrapped_html);
2406 goto service_edit_cleanup;
2407 }
2408 free(option_rows_html);
2409 option_rows_html = wrapped_html;
2410 option_rows_len = wrapped_len;
2411 option_rows_cap = wrapped_cap;
2412 }
2413 }
2414
2415 technicians = state_array_runtime_local(root, "technicians");
2416 if (technicians != NULL && yyjson_arr_iter_init(technicians, &iter)) {
2417 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
2418 char *name = NULL;
2419 long technician_id = 0;
2420 int checked = 0;
2421 yyjson_val *assigned = native_json_obj_get(service, "technician_ids");
2422 yyjson_arr_iter assigned_iter;
2423 yyjson_val *assigned_value = NULL;
2424 if (!yyjson_is_obj(row) || native_json_obj_get_long_default(row, "business_id", 0, NULL) != business_id) {
2425 continue;
2426 }
2427 technician_id = native_json_obj_get_long_default(row, "id", 0, NULL);
2428 name = native_json_obj_get_string_dup(row, "name");
2429 if (assigned != NULL && yyjson_is_arr(assigned) && yyjson_arr_iter_init(assigned, &assigned_iter)) {
2430 while ((assigned_value = yyjson_arr_iter_next(&assigned_iter)) != NULL) {
2431 long assigned_id = 0;
2432 if (long_from_value_runtime_local(assigned_value, &assigned_id) && assigned_id == technician_id) {
2433 checked = 1;
2434 break;
2435 }
2436 }
2437 }
2438 if (!text_append_format_runtime_local(&technician_boxes_html, &technician_boxes_len, &technician_boxes_cap,
2439 "<label style=\"display:flex;gap:0.5rem;align-items:center;\"><input type=\"checkbox\" name=\"technician_%ld\" value=\"%ld\"%s> %s</label>",
2440 technician_id,
2441 technician_id,
2442 checked ? " checked" : "",
2443 name != NULL ? name : "")) {
2444 free(name);
2445 goto service_edit_cleanup;
2446 }
2447 technician_emitted = 1;
2448 free(name);
2449 }
2450 }
2451 if (!technician_emitted) {
2452 if (!text_append_format_runtime_local(&technician_boxes_html, &technician_boxes_len, &technician_boxes_cap,
2453 "<p>No technicians available for this business yet.</p>")) {
2454 goto service_edit_cleanup;
2455 }
2456 }
2457
2458 doc = yyjson_mut_doc_new(NULL);
2459 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
2460 if (doc == NULL || page == NULL) {
2461 goto service_edit_cleanup;
2462 }
2463 yyjson_mut_doc_set_root(doc, page);
2464 {
2465 char save_path[256];
2466 char cancel_path[256];
2467 char duration_buffer[32];
2468 char visual_html[1024];
2469 snprintf(save_path, sizeof(save_path), "/businesses/%s/services/%ld/edit/", business_slug, service_id);
2470 snprintf(cancel_path, sizeof(cancel_path), "/businesses/%s/", business_slug);
2471 snprintf(duration_buffer, sizeof(duration_buffer), "%ld", service_duration);
2472 if (service_image != NULL && service_image[0] != '\0') {
2473 snprintf(visual_html, sizeof(visual_html), "<img src=\"%s\" alt=\"%s\" style=\"max-width:180px;border-radius:0.5rem;\">", service_image, service_name != NULL ? service_name : "Service image");
2474 } else {
2475 snprintf(visual_html, sizeof(visual_html), "<div class=\"card\">No image uploaded.</div>");
2476 }
2477 if (!yyjson_mut_obj_add_strcpy(doc, page, "save_path", save_path)
2478 || !yyjson_mut_obj_add_strcpy(doc, page, "cancel_path", cancel_path)
2479 || !yyjson_mut_obj_add_strcpy(doc, page, "service_visual_html", visual_html)
2480 || !yyjson_mut_obj_add_strcpy(doc, page, "service_name", service_name != NULL ? service_name : "")
2481 || !yyjson_mut_obj_add_strcpy(doc, page, "service_description", service_description != NULL ? service_description : "")
2482 || !yyjson_mut_obj_add_strcpy(doc, page, "service_cost", service_cost != NULL ? service_cost : "")
2483 || !yyjson_mut_obj_add_strcpy(doc, page, "service_duration", duration_buffer)
2484 || !yyjson_mut_obj_add_strcpy(doc, page, "service_sort_order_options_html", sort_options_html != NULL ? sort_options_html : "")
2485 || !yyjson_mut_obj_add_strcpy(doc, page, "booking_options_editor_html", option_rows_html != NULL ? option_rows_html : "")
2486 || !yyjson_mut_obj_add_strcpy(doc, page, "technician_checkboxes_html", technician_boxes_html != NULL ? technician_boxes_html : "")) {
2487 goto service_edit_cleanup;
2488 }
2489 }
2490 params_json = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
2491
2492service_edit_cleanup:
2493 free(service_name);
2494 free(service_description);
2495 free(service_cost);
2496 free(service_image);
2497 free(sort_options_html);
2498 free(option_rows_html);
2499 free(technician_boxes_html);
2501 native_json_doc_free(state_doc);
2502 return params_json;
2503}
2504
2505static int secure_text_equals(const char *left, const char *right) {
2506 size_t left_len;
2507 size_t right_len;
2508 size_t index;
2509 unsigned char diff = 0;
2510 if (left == NULL || right == NULL) {
2511 return 0;
2512 }
2513 left_len = strlen(left);
2514 right_len = strlen(right);
2515 if (left_len != right_len) {
2516 return 0;
2517 }
2518 for (index = 0; index < left_len; index++) {
2519 diff |= (unsigned char)(left[index] ^ right[index]);
2520 }
2521 return diff == 0;
2522}
2523
2524#define SHA256_BLOCK_SIZE 64
2525#define SHA256_DIGEST_SIZE 32
2526
2527typedef struct {
2528 uint8_t data[64];
2529 uint32_t datalen;
2530 uint64_t bitlen;
2531 uint32_t state[8];
2533
2534static uint32_t sha256_rotr(uint32_t value, uint32_t bits) {
2535 return (value >> bits) | (value << (32U - bits));
2536}
2537
2538static void sha256_transform_runtime(Sha256Runtime *ctx, const uint8_t data[]) {
2539 static const uint32_t k[64] = {
2540 0x428a2f98U,0x71374491U,0xb5c0fbcfU,0xe9b5dba5U,0x3956c25bU,0x59f111f1U,0x923f82a4U,0xab1c5ed5U,
2541 0xd807aa98U,0x12835b01U,0x243185beU,0x550c7dc3U,0x72be5d74U,0x80deb1feU,0x9bdc06a7U,0xc19bf174U,
2542 0xe49b69c1U,0xefbe4786U,0x0fc19dc6U,0x240ca1ccU,0x2de92c6fU,0x4a7484aaU,0x5cb0a9dcU,0x76f988daU,
2543 0x983e5152U,0xa831c66dU,0xb00327c8U,0xbf597fc7U,0xc6e00bf3U,0xd5a79147U,0x06ca6351U,0x14292967U,
2544 0x27b70a85U,0x2e1b2138U,0x4d2c6dfcU,0x53380d13U,0x650a7354U,0x766a0abbU,0x81c2c92eU,0x92722c85U,
2545 0xa2bfe8a1U,0xa81a664bU,0xc24b8b70U,0xc76c51a3U,0xd192e819U,0xd6990624U,0xf40e3585U,0x106aa070U,
2546 0x19a4c116U,0x1e376c08U,0x2748774cU,0x34b0bcb5U,0x391c0cb3U,0x4ed8aa4aU,0x5b9cca4fU,0x682e6ff3U,
2547 0x748f82eeU,0x78a5636fU,0x84c87814U,0x8cc70208U,0x90befffaU,0xa4506cebU,0xbef9a3f7U,0xc67178f2U
2548 };
2549 uint32_t a, b, c, d, e, f, g, h;
2550 uint32_t m[64];
2551 uint32_t t1, t2;
2552 size_t index;
2553 for (index = 0; index < 16; index++) {
2554 m[index] = ((uint32_t)data[index * 4] << 24)
2555 | ((uint32_t)data[index * 4 + 1] << 16)
2556 | ((uint32_t)data[index * 4 + 2] << 8)
2557 | ((uint32_t)data[index * 4 + 3]);
2558 }
2559 for (index = 16; index < 64; index++) {
2560 uint32_t s0 = sha256_rotr(m[index - 15], 7) ^ sha256_rotr(m[index - 15], 18) ^ (m[index - 15] >> 3);
2561 uint32_t s1 = sha256_rotr(m[index - 2], 17) ^ sha256_rotr(m[index - 2], 19) ^ (m[index - 2] >> 10);
2562 m[index] = m[index - 16] + s0 + m[index - 7] + s1;
2563 }
2564 a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3];
2565 e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7];
2566 for (index = 0; index < 64; index++) {
2567 uint32_t s1 = sha256_rotr(e, 6) ^ sha256_rotr(e, 11) ^ sha256_rotr(e, 25);
2568 uint32_t ch = (e & f) ^ ((~e) & g);
2569 t1 = h + s1 + ch + k[index] + m[index];
2570 {
2571 uint32_t s0 = sha256_rotr(a, 2) ^ sha256_rotr(a, 13) ^ sha256_rotr(a, 22);
2572 uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
2573 t2 = s0 + maj;
2574 }
2575 h = g;
2576 g = f;
2577 f = e;
2578 e = d + t1;
2579 d = c;
2580 c = b;
2581 b = a;
2582 a = t1 + t2;
2583 }
2584 ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d;
2585 ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h;
2586}
2587
2589 ctx->datalen = 0;
2590 ctx->bitlen = 0;
2591 ctx->state[0] = 0x6a09e667U;
2592 ctx->state[1] = 0xbb67ae85U;
2593 ctx->state[2] = 0x3c6ef372U;
2594 ctx->state[3] = 0xa54ff53aU;
2595 ctx->state[4] = 0x510e527fU;
2596 ctx->state[5] = 0x9b05688cU;
2597 ctx->state[6] = 0x1f83d9abU;
2598 ctx->state[7] = 0x5be0cd19U;
2599}
2600
2601static void sha256_update_runtime(Sha256Runtime *ctx, const uint8_t *data, size_t len) {
2602 size_t index;
2603 for (index = 0; index < len; index++) {
2604 ctx->data[ctx->datalen++] = data[index];
2605 if (ctx->datalen == 64) {
2606 sha256_transform_runtime(ctx, ctx->data);
2607 ctx->bitlen += 512;
2608 ctx->datalen = 0;
2609 }
2610 }
2611}
2612
2614 uint32_t index = ctx->datalen;
2615 if (ctx->datalen < 56) {
2616 ctx->data[index++] = 0x80;
2617 while (index < 56) ctx->data[index++] = 0x00;
2618 } else {
2619 ctx->data[index++] = 0x80;
2620 while (index < 64) ctx->data[index++] = 0x00;
2621 sha256_transform_runtime(ctx, ctx->data);
2622 memset(ctx->data, 0, 56);
2623 }
2624 ctx->bitlen += (uint64_t)ctx->datalen * 8U;
2625 ctx->data[63] = (uint8_t)(ctx->bitlen);
2626 ctx->data[62] = (uint8_t)(ctx->bitlen >> 8);
2627 ctx->data[61] = (uint8_t)(ctx->bitlen >> 16);
2628 ctx->data[60] = (uint8_t)(ctx->bitlen >> 24);
2629 ctx->data[59] = (uint8_t)(ctx->bitlen >> 32);
2630 ctx->data[58] = (uint8_t)(ctx->bitlen >> 40);
2631 ctx->data[57] = (uint8_t)(ctx->bitlen >> 48);
2632 ctx->data[56] = (uint8_t)(ctx->bitlen >> 56);
2633 sha256_transform_runtime(ctx, ctx->data);
2634 for (index = 0; index < 4; index++) {
2635 hash[index] = (uint8_t)((ctx->state[0] >> (24 - index * 8)) & 0xffU);
2636 hash[index + 4] = (uint8_t)((ctx->state[1] >> (24 - index * 8)) & 0xffU);
2637 hash[index + 8] = (uint8_t)((ctx->state[2] >> (24 - index * 8)) & 0xffU);
2638 hash[index + 12] = (uint8_t)((ctx->state[3] >> (24 - index * 8)) & 0xffU);
2639 hash[index + 16] = (uint8_t)((ctx->state[4] >> (24 - index * 8)) & 0xffU);
2640 hash[index + 20] = (uint8_t)((ctx->state[5] >> (24 - index * 8)) & 0xffU);
2641 hash[index + 24] = (uint8_t)((ctx->state[6] >> (24 - index * 8)) & 0xffU);
2642 hash[index + 28] = (uint8_t)((ctx->state[7] >> (24 - index * 8)) & 0xffU);
2643 }
2644}
2645
2646static void bytes_to_hex_runtime(const uint8_t *bytes, size_t len, char *hex_out) {
2647 static const char hex_chars[] = "0123456789abcdef";
2648 size_t index;
2649 for (index = 0; index < len; index++) {
2650 hex_out[index * 2] = hex_chars[(bytes[index] >> 4) & 0x0fU];
2651 hex_out[index * 2 + 1] = hex_chars[bytes[index] & 0x0fU];
2652 }
2653 hex_out[len * 2] = '\0';
2654}
2655
2656static void hmac_sha256_runtime(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t out[SHA256_DIGEST_SIZE]) {
2657 uint8_t key_block[SHA256_BLOCK_SIZE];
2658 uint8_t inner_hash[SHA256_DIGEST_SIZE];
2659 uint8_t ipad[SHA256_BLOCK_SIZE];
2660 uint8_t opad[SHA256_BLOCK_SIZE];
2661 Sha256Runtime ctx;
2662 size_t index;
2663 memset(key_block, 0, sizeof(key_block));
2664 if (key_len > SHA256_BLOCK_SIZE) {
2665 sha256_init_runtime(&ctx);
2666 sha256_update_runtime(&ctx, key, key_len);
2667 sha256_final_runtime(&ctx, key_block);
2668 } else if (key_len > 0) {
2669 memcpy(key_block, key, key_len);
2670 }
2671 for (index = 0; index < SHA256_BLOCK_SIZE; index++) {
2672 ipad[index] = (uint8_t)(key_block[index] ^ 0x36U);
2673 opad[index] = (uint8_t)(key_block[index] ^ 0x5cU);
2674 }
2675 sha256_init_runtime(&ctx);
2676 sha256_update_runtime(&ctx, ipad, sizeof(ipad));
2677 sha256_update_runtime(&ctx, data, data_len);
2678 sha256_final_runtime(&ctx, inner_hash);
2679 sha256_init_runtime(&ctx);
2680 sha256_update_runtime(&ctx, opad, sizeof(opad));
2681 sha256_update_runtime(&ctx, inner_hash, sizeof(inner_hash));
2682 sha256_final_runtime(&ctx, out);
2683}
2684
2685static int pbkdf2_sha256_runtime(const uint8_t *password, size_t password_len, const uint8_t *salt, size_t salt_len, uint32_t iterations, uint8_t *out, size_t out_len) {
2686 uint32_t block_index = 1;
2687 size_t produced = 0;
2688 if (iterations == 0) {
2689 return 0;
2690 }
2691 while (produced < out_len) {
2692 uint8_t salt_block[512];
2693 uint8_t u[SHA256_DIGEST_SIZE];
2694 uint8_t t[SHA256_DIGEST_SIZE];
2695 size_t copy_len;
2696 uint32_t iter;
2697 if (salt_len + 4 > sizeof(salt_block)) {
2698 return 0;
2699 }
2700 memcpy(salt_block, salt, salt_len);
2701 salt_block[salt_len] = (uint8_t)((block_index >> 24) & 0xffU);
2702 salt_block[salt_len + 1] = (uint8_t)((block_index >> 16) & 0xffU);
2703 salt_block[salt_len + 2] = (uint8_t)((block_index >> 8) & 0xffU);
2704 salt_block[salt_len + 3] = (uint8_t)(block_index & 0xffU);
2705 hmac_sha256_runtime(password, password_len, salt_block, salt_len + 4, u);
2706 memcpy(t, u, sizeof(t));
2707 for (iter = 1; iter < iterations; iter++) {
2708 size_t index;
2709 hmac_sha256_runtime(password, password_len, u, sizeof(u), u);
2710 for (index = 0; index < sizeof(t); index++) {
2711 t[index] ^= u[index];
2712 }
2713 }
2714 copy_len = (out_len - produced) < sizeof(t) ? (out_len - produced) : sizeof(t);
2715 memcpy(out + produced, t, copy_len);
2716 produced += copy_len;
2717 block_index++;
2718 }
2719 return 1;
2720}
2721
2722static int verify_password_hash_runtime(const char *stored_hash, const char *password_value, const char *username_value) {
2723 if (stored_hash == NULL || password_value == NULL) {
2724 return 0;
2725 }
2726 if (strncmp(stored_hash, "pharos-pbkdf2-sha256$", strlen("pharos-pbkdf2-sha256$")) == 0) {
2727 char *copy = strdup(stored_hash);
2728 char *saveptr = NULL;
2729 char *algorithm;
2730 char *iterations_text;
2731 char *salt_text;
2732 char *expected_hex;
2733 uint8_t derived[SHA256_DIGEST_SIZE];
2734 char derived_hex[SHA256_DIGEST_SIZE * 2 + 1];
2735 uint32_t iterations;
2736 int ok = 0;
2737 if (copy == NULL) {
2738 return 0;
2739 }
2740 algorithm = strtok_r(copy, "$", &saveptr);
2741 iterations_text = strtok_r(NULL, "$", &saveptr);
2742 salt_text = strtok_r(NULL, "$", &saveptr);
2743 expected_hex = strtok_r(NULL, "$", &saveptr);
2744 if (algorithm != NULL && iterations_text != NULL && salt_text != NULL && expected_hex != NULL) {
2745 iterations = (uint32_t)strtoul(iterations_text, NULL, 10);
2746 if (iterations > 0 && pbkdf2_sha256_runtime((const uint8_t *)password_value, strlen(password_value), (const uint8_t *)salt_text, strlen(salt_text), iterations, derived, sizeof(derived))) {
2747 bytes_to_hex_runtime(derived, sizeof(derived), derived_hex);
2748 ok = secure_text_equals(derived_hex, expected_hex);
2749 }
2750 }
2751 free(copy);
2752 return ok;
2753 }
2754 if (strncmp(stored_hash, "dev$", 4) == 0) {
2755 char *copy = strdup(stored_hash);
2756 char *saveptr = NULL;
2757 char *prefix;
2758 char *legacy_username;
2759 char *legacy_password;
2760 int ok = 0;
2761 if (copy == NULL) {
2762 return 0;
2763 }
2764 prefix = strtok_r(copy, "$", &saveptr);
2765 legacy_username = strtok_r(NULL, "$", &saveptr);
2766 legacy_password = saveptr;
2767 (void)prefix;
2768 if (legacy_password != NULL) {
2769 if (username_value != NULL && username_value[0] != '\0' && legacy_username != NULL && !streq(legacy_username, username_value)) {
2770 ok = 0;
2771 } else {
2772 ok = secure_text_equals(legacy_password, password_value);
2773 }
2774 }
2775 free(copy);
2776 return ok;
2777 }
2778 return 0;
2779}
2780
2781static char *sha256_hex_dup_runtime(const char *value) {
2782 Sha256Runtime ctx;
2783 uint8_t digest[SHA256_DIGEST_SIZE];
2784 char *hex = NULL;
2785 if (value == NULL) {
2786 return NULL;
2787 }
2788 hex = (char *)malloc(SHA256_DIGEST_SIZE * 2 + 1);
2789 if (hex == NULL) {
2790 return NULL;
2791 }
2792 sha256_init_runtime(&ctx);
2793 sha256_update_runtime(&ctx, (const uint8_t *)value, strlen(value));
2794 sha256_final_runtime(&ctx, digest);
2795 bytes_to_hex_runtime(digest, sizeof(digest), hex);
2796 return hex;
2797}
2798
2799static char *token_hash_dup_runtime(const char *token_value) {
2800 char *sha_hex = NULL;
2801 char *out = NULL;
2802 size_t needed;
2803 if (token_value == NULL || token_value[0] == '\0') {
2804 return NULL;
2805 }
2806 sha_hex = sha256_hex_dup_runtime(token_value);
2807 if (sha_hex == NULL) {
2808 return NULL;
2809 }
2810 needed = strlen("pharos-sha256$") + strlen(sha_hex) + 1;
2811 out = (char *)malloc(needed);
2812 if (out != NULL) {
2813 snprintf(out, needed, "pharos-sha256$%s", sha_hex);
2814 }
2815 free(sha_hex);
2816 return out;
2817}
2818
2819static char *password_hash_dup_runtime(const char *password_value) {
2820 const uint32_t iterations = 310000;
2821 char salt_text[33];
2822 uint8_t derived[SHA256_DIGEST_SIZE];
2823 char derived_hex[SHA256_DIGEST_SIZE * 2 + 1];
2824 char *out = NULL;
2825 size_t needed;
2826 if (password_value == NULL || password_value[0] == '\0') {
2827 return NULL;
2828 }
2829 csrf_generate_random_hex(salt_text, 16);
2830 if (!pbkdf2_sha256_runtime((const uint8_t *)password_value, strlen(password_value), (const uint8_t *)salt_text, strlen(salt_text), iterations, derived, sizeof(derived))) {
2831 return NULL;
2832 }
2833 bytes_to_hex_runtime(derived, sizeof(derived), derived_hex);
2834 needed = strlen("pharos-pbkdf2-sha256$") + 10 + 1 + strlen(salt_text) + 1 + strlen(derived_hex) + 1;
2835 out = (char *)malloc(needed);
2836 if (out != NULL) {
2837 snprintf(out, needed, "pharos-pbkdf2-sha256$%u$%s$%s", iterations, salt_text, derived_hex);
2838 }
2839 return out;
2840}
2841
2842static int now_iso_utc_runtime(char out[21]) {
2843 time_t now = time(NULL);
2844 struct tm tm_value;
2845#if defined(_WIN32)
2846 if (gmtime_s(&tm_value, &now) != 0) return 0;
2847#else
2848 if (gmtime_r(&now, &tm_value) == NULL) return 0;
2849#endif
2850 return strftime(out, 21, "%Y-%m-%dT%H:%M:%SZ", &tm_value) == 20;
2851}
2852
2853static int iso_after_seconds_runtime(long delta_seconds, char out[21]) {
2854 time_t now = time(NULL) + (time_t)delta_seconds;
2855 struct tm tm_value;
2856#if defined(_WIN32)
2857 if (gmtime_s(&tm_value, &now) != 0) return 0;
2858#else
2859 if (gmtime_r(&now, &tm_value) == NULL) return 0;
2860#endif
2861 return strftime(out, 21, "%Y-%m-%dT%H:%M:%SZ", &tm_value) == 20;
2862}
2863
2864static yyjson_val *state_array_runtime_local(yyjson_val *root, const char *field_name) {
2865 yyjson_val *value = native_json_obj_get(root, field_name);
2866 return value != NULL && yyjson_is_arr(value) ? value : NULL;
2867}
2868
2869static yyjson_val *find_user_by_username_runtime_local(yyjson_val *root, const char *username) {
2870 yyjson_val *users = state_array_runtime_local(root, "users");
2871 yyjson_arr_iter iter;
2872 yyjson_val *user = NULL;
2873 if (users == NULL || username == NULL || !yyjson_arr_iter_init(users, &iter)) {
2874 return NULL;
2875 }
2876 while ((user = yyjson_arr_iter_next(&iter)) != NULL) {
2877 yyjson_val *username_value = native_json_obj_get(user, "username");
2878 if (yyjson_is_str(username_value) && yyjson_equals_str(username_value, username)) {
2879 return user;
2880 }
2881 }
2882 return NULL;
2883}
2884
2886 yyjson_val *users = state_array_runtime_local(root, "users");
2887 yyjson_arr_iter iter;
2888 yyjson_val *user = NULL;
2889 if (users == NULL || !yyjson_arr_iter_init(users, &iter)) {
2890 return NULL;
2891 }
2892 while ((user = yyjson_arr_iter_next(&iter)) != NULL) {
2893 if (native_json_obj_get_long_default(user, "id", 0, NULL) == user_id) {
2894 return user;
2895 }
2896 }
2897 return NULL;
2898}
2899
2900static yyjson_val *find_business_by_slug_runtime_local(yyjson_val *root, const char *business_slug) {
2901 yyjson_val *businesses = state_array_runtime_local(root, "businesses");
2902 yyjson_arr_iter iter;
2903 yyjson_val *business = NULL;
2904 if (businesses == NULL || business_slug == NULL || !yyjson_arr_iter_init(businesses, &iter)) {
2905 return NULL;
2906 }
2907 while ((business = yyjson_arr_iter_next(&iter)) != NULL) {
2908 yyjson_val *slug_value = native_json_obj_get(business, "slug");
2909 if (yyjson_is_str(slug_value) && yyjson_equals_str(slug_value, business_slug)) {
2910 return business;
2911 }
2912 }
2913 return NULL;
2914}
2915
2916/**
2917 * @brief Finds one business record by numeric id in the runtime state.
2918 *
2919 * @param root Loaded runtime state root.
2920 * @param business_id Business id to match.
2921 * @return Matching business object, or NULL when not found.
2922 */
2924 yyjson_val *businesses = state_array_runtime_local(root, "businesses");
2925 yyjson_arr_iter iter;
2926 yyjson_val *business = NULL;
2927 if (businesses == NULL || !yyjson_arr_iter_init(businesses, &iter)) {
2928 return NULL;
2929 }
2930 while ((business = yyjson_arr_iter_next(&iter)) != NULL) {
2931 if (yyjson_is_obj(business) && native_json_obj_get_long_default(business, "id", 0, NULL) == business_id) {
2932 return business;
2933 }
2934 }
2935 return NULL;
2936}
2937
2938static int long_from_value_runtime_local(yyjson_val *value, long *out_value) {
2939 if (out_value != NULL) {
2940 *out_value = 0;
2941 }
2942 if (value == NULL) {
2943 return 0;
2944 }
2945 if (yyjson_is_int(value) || yyjson_is_uint(value) || yyjson_is_real(value)) {
2946 if (out_value != NULL) {
2947 *out_value = yyjson_get_sint(value);
2948 }
2949 return 1;
2950 }
2951 return 0;
2952}
2953
2954static int value_long_equals_runtime_local(yyjson_val *value, long expected_value) {
2955 long actual_value = 0;
2956 return long_from_value_runtime_local(value, &actual_value) && actual_value == expected_value;
2957}
2958
2959static yyjson_val *find_service_for_business_runtime_local(yyjson_val *root, long service_id, long business_id) {
2960 yyjson_val *services = state_array_runtime_local(root, "services");
2961 yyjson_arr_iter iter;
2962 yyjson_val *service = NULL;
2963 if (services == NULL || !yyjson_arr_iter_init(services, &iter)) {
2964 return NULL;
2965 }
2966 while ((service = yyjson_arr_iter_next(&iter)) != NULL) {
2967 if (!yyjson_is_obj(service)) {
2968 continue;
2969 }
2970 if (native_json_obj_get_long_default(service, "id", 0, NULL) == service_id
2971 && native_json_obj_get_long_default(service, "business_id", 0, NULL) == business_id) {
2972 return service;
2973 }
2974 }
2975 return NULL;
2976}
2977
2979 yyjson_val *appointments = state_array_runtime_local(root, "appointments");
2980 yyjson_arr_iter iter;
2981 yyjson_val *appointment = NULL;
2982 if (appointments == NULL || !yyjson_arr_iter_init(appointments, &iter)) {
2983 return NULL;
2984 }
2985 while ((appointment = yyjson_arr_iter_next(&iter)) != NULL) {
2986 if (yyjson_is_obj(appointment) && native_json_obj_get_long_default(appointment, "id", 0, NULL) == appointment_id) {
2987 return appointment;
2988 }
2989 }
2990 return NULL;
2991}
2992
2993static int parse_ymd_runtime_local(const char *date_value, int *year_out, int *month_out, int *day_out) {
2994 int year = 0;
2995 int month = 0;
2996 int day = 0;
2997 if (year_out != NULL) *year_out = 0;
2998 if (month_out != NULL) *month_out = 0;
2999 if (day_out != NULL) *day_out = 0;
3000 if (date_value == NULL || sscanf(date_value, "%d-%d-%d", &year, &month, &day) != 3 || month < 1 || month > 12 || day < 1 || day > 31) {
3001 return 0;
3002 }
3003 if (year_out != NULL) *year_out = year;
3004 if (month_out != NULL) *month_out = month;
3005 if (day_out != NULL) *day_out = day;
3006 return 1;
3007}
3008
3009static int time_ranges_overlap_runtime_local(int start_a_minutes, int duration_a, int start_b_minutes, int duration_b) {
3010 int end_a_minutes = start_a_minutes + duration_a;
3011 int end_b_minutes = start_b_minutes + duration_b;
3012 return start_a_minutes < end_b_minutes && start_b_minutes < end_a_minutes;
3013}
3014
3015static const char *day_abbrev_for_ymd_runtime_local(int year, int month, int day) {
3016 static const char *names[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
3017 static const int offsets[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
3018 int adjusted_year = year;
3019 int weekday = 0;
3020 if (month < 3) {
3021 adjusted_year -= 1;
3022 }
3023 weekday = (adjusted_year + adjusted_year / 4 - adjusted_year / 100 + adjusted_year / 400 + offsets[month - 1] + day) % 7;
3024 if (weekday < 0 || weekday > 6) {
3025 return NULL;
3026 }
3027 return names[weekday];
3028}
3029
3030static int business_open_for_slot_runtime_local(yyjson_val *root, long business_id, const char *date_value, const char *time_value, long duration_minutes) {
3031 yyjson_val *availability_rows = state_array_runtime_local(root, "business_availability");
3032 yyjson_arr_iter iter;
3033 yyjson_val *row = NULL;
3034 int year = 0;
3035 int month = 0;
3036 int day = 0;
3037 int slot_start = 0;
3038 int slot_end = 0;
3039 const char *day_name = NULL;
3040 if (root == NULL || business_id <= 0 || duration_minutes <= 0 || !parse_ymd_runtime_local(date_value, &year, &month, &day) || !time_text_to_minutes_runtime(time_value, &slot_start)) {
3041 return 0;
3042 }
3043 day_name = day_abbrev_for_ymd_runtime_local(year, month, day);
3044 slot_end = slot_start + (int)duration_minutes;
3045 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
3046 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
3047 char *row_day = NULL;
3048 char *start_text = NULL;
3049 char *end_text = NULL;
3050 int start_minutes = 0;
3051 int end_minutes = 0;
3052 int matches = 0;
3053 if (!yyjson_is_obj(row) || !value_long_equals_runtime_local(native_json_obj_get(row, "business_id"), business_id)) {
3054 continue;
3055 }
3056 row_day = native_json_obj_get_string_dup(row, "day_of_week");
3057 start_text = native_json_obj_get_string_dup(row, "start_time");
3058 end_text = native_json_obj_get_string_dup(row, "end_time");
3059 matches = row_day != NULL && day_name != NULL && streq(row_day, day_name)
3060 && time_text_to_minutes_runtime(start_text, &start_minutes)
3061 && time_text_to_minutes_runtime(end_text, &end_minutes)
3062 && slot_start >= start_minutes && slot_end <= end_minutes;
3063 free(row_day);
3064 free(start_text);
3065 free(end_text);
3066 if (matches) {
3067 return 1;
3068 }
3069 }
3070 }
3071 return 0;
3072}
3073
3074static int technician_booked_for_slot_runtime_local(yyjson_val *root, long technician_id, const char *date_value, const char *time_value, long duration_minutes) {
3075 yyjson_val *appointments = state_array_runtime_local(root, "appointments");
3076 yyjson_arr_iter iter;
3077 yyjson_val *row = NULL;
3078 int slot_start = 0;
3079 if (root == NULL || technician_id <= 0 || duration_minutes <= 0 || !time_text_to_minutes_runtime(time_value, &slot_start)) {
3080 return 0;
3081 }
3082 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
3083 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
3084 char *existing_date = NULL;
3085 char *existing_time = NULL;
3086 char *existing_status = NULL;
3087 long service_id = 0;
3088 yyjson_val *service = NULL;
3089 int existing_start = 0;
3090 int existing_duration = 45;
3091 int matches = 0;
3092 if (!yyjson_is_obj(row) || !value_long_equals_runtime_local(native_json_obj_get(row, "technician_id"), technician_id)) {
3093 continue;
3094 }
3095 existing_date = native_json_obj_get_string_dup(row, "date");
3096 existing_time = native_json_obj_get_string_dup(row, "time");
3097 existing_status = native_json_obj_get_string_dup(row, "status");
3098 if (existing_status != NULL && streq(existing_status, "cancelled")) {
3099 free(existing_date);
3100 free(existing_time);
3101 free(existing_status);
3102 continue;
3103 }
3104 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
3105 service = find_service_for_business_runtime_local(root, service_id, native_json_obj_get_long_default(row, "business_id", 0, NULL));
3106 if (service != NULL) {
3107 existing_duration = (int)native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
3108 }
3109 matches = existing_date != NULL && streq(existing_date, date_value != NULL ? date_value : "")
3110 && time_text_to_minutes_runtime(existing_time, &existing_start)
3111 && time_ranges_overlap_runtime_local(slot_start, (int)duration_minutes, existing_start, existing_duration);
3112 free(existing_date);
3113 free(existing_time);
3114 free(existing_status);
3115 if (matches) {
3116 return 1;
3117 }
3118 }
3119 }
3120 return 0;
3121}
3122
3123static long assign_technician_for_slot_runtime_local(yyjson_val *root, long business_id, long service_id, const char *date_value, const char *time_value) {
3124 yyjson_val *service = find_service_for_business_runtime_local(root, service_id, business_id);
3125 yyjson_val *technician_ids = service != NULL ? native_json_obj_get_array(service, "technician_ids") : NULL;
3126 yyjson_val *availability_rows = state_array_runtime_local(root, "technician_availability");
3127 yyjson_arr_iter tech_iter;
3128 yyjson_val *tech_value = NULL;
3129 int year = 0;
3130 int month = 0;
3131 int day = 0;
3132 int slot_start = 0;
3133 int slot_end = 0;
3134 int duration_minutes = 45;
3135 const char *day_name = NULL;
3136 if (service != NULL) {
3137 duration_minutes = (int)native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
3138 }
3139 if (duration_minutes <= 0) {
3140 duration_minutes = 45;
3141 }
3142 if (root == NULL || business_id <= 0 || service_id <= 0 || technician_ids == NULL || !yyjson_is_arr(technician_ids) || !parse_ymd_runtime_local(date_value, &year, &month, &day) || !time_text_to_minutes_runtime(time_value, &slot_start)) {
3143 return 0;
3144 }
3145 day_name = day_abbrev_for_ymd_runtime_local(year, month, day);
3146 slot_end = slot_start + duration_minutes;
3147 if (yyjson_arr_iter_init(technician_ids, &tech_iter)) {
3148 while ((tech_value = yyjson_arr_iter_next(&tech_iter)) != NULL) {
3149 long technician_id = 0;
3150 yyjson_arr_iter avail_iter;
3151 yyjson_val *row = NULL;
3152 if (!long_from_value_runtime_local(tech_value, &technician_id) || technician_id <= 0) {
3153 continue;
3154 }
3155 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &avail_iter)) {
3156 while ((row = yyjson_arr_iter_next(&avail_iter)) != NULL) {
3157 char *row_day = NULL;
3158 char *start_text = NULL;
3159 char *end_text = NULL;
3160 int start_minutes = 0;
3161 int end_minutes = 0;
3162 int available = 0;
3163 if (!yyjson_is_obj(row) || !value_long_equals_runtime_local(native_json_obj_get(row, "technician_id"), technician_id)) {
3164 continue;
3165 }
3166 row_day = native_json_obj_get_string_dup(row, "day_of_week");
3167 start_text = native_json_obj_get_string_dup(row, "start_time");
3168 end_text = native_json_obj_get_string_dup(row, "end_time");
3169 available = row_day != NULL && day_name != NULL && streq(row_day, day_name)
3170 && time_text_to_minutes_runtime(start_text, &start_minutes)
3171 && time_text_to_minutes_runtime(end_text, &end_minutes)
3172 && slot_start >= start_minutes && slot_end <= end_minutes;
3173 free(row_day);
3174 free(start_text);
3175 free(end_text);
3176 if (available && !technician_booked_for_slot_runtime_local(root, technician_id, date_value, time_value, duration_minutes)) {
3177 return technician_id;
3178 }
3179 }
3180 }
3181 }
3182 }
3183 return 0;
3184}
3185
3187 yyjson_arr_iter iter;
3188 yyjson_val *row = NULL;
3189 long next_id = 1;
3190 if (rows != NULL && yyjson_arr_iter_init(rows, &iter)) {
3191 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
3192 long existing_id = native_json_obj_get_long_default(row, "id", 0, NULL);
3193 if (existing_id >= next_id) {
3194 next_id = existing_id + 1;
3195 }
3196 }
3197 }
3198 return next_id;
3199}
3200
3201/**
3202 * @brief Resolves the tenant id that owns a business record.
3203 *
3204 * @param root Loaded runtime state root.
3205 * @param business_id Business id to map.
3206 * @return Matching tenant id, or 0 when not found.
3207 */
3208static long state_tenant_id_for_business_runtime_local(yyjson_val *root, long business_id) {
3209 yyjson_val *tenants = state_array_runtime_local(root, "tenants");
3210 yyjson_arr_iter iter;
3211 yyjson_val *row = NULL;
3212 if (tenants == NULL || business_id <= 0 || !yyjson_arr_iter_init(tenants, &iter)) {
3213 return 0;
3214 }
3215 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
3216 if (!yyjson_is_obj(row)) {
3217 continue;
3218 }
3219 if (native_json_obj_get_long_default(row, "source_business_id", 0, NULL) == business_id) {
3220 return native_json_obj_get_long_default(row, "id", 0, NULL);
3221 }
3222 }
3223 return 0;
3224}
3225
3226/**
3227 * @brief Checks whether a user has an active membership on a tenant.
3228 *
3229 * @param root Loaded runtime state root.
3230 * @param user_id User id to test.
3231 * @param tenant_id Tenant id to test.
3232 * @return 1 when active, otherwise 0.
3233 */
3234static int user_has_active_tenant_membership_runtime_local(yyjson_val *root, long user_id, long tenant_id) {
3235 yyjson_val *memberships = state_array_runtime_local(root, "tenant_memberships");
3236 yyjson_arr_iter iter;
3237 yyjson_val *row = NULL;
3238 if (memberships == NULL || user_id <= 0 || tenant_id <= 0 || !yyjson_arr_iter_init(memberships, &iter)) {
3239 return 0;
3240 }
3241 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
3242 const char *membership_status = NULL;
3243 if (!yyjson_is_obj(row)) {
3244 continue;
3245 }
3246 if (native_json_obj_get_long_default(row, "user_id", 0, NULL) != user_id
3247 || native_json_obj_get_long_default(row, "tenant_id", 0, NULL) != tenant_id) {
3248 continue;
3249 }
3250 membership_status = yyjson_get_str(native_json_obj_get(row, "membership_status"));
3251 if (membership_status == NULL || membership_status[0] == '\0' || streq(membership_status, "active")) {
3252 return 1;
3253 }
3254 }
3255 return 0;
3256}
3257
3258/**
3259 * @brief Maps invite role hints onto the canonical tenant membership role id.
3260 *
3261 * @param role_hint Requested invite role hint.
3262 * @param account_type Requested invite account type.
3263 * @return Canonical role id string.
3264 */
3265static const char *membership_role_id_for_invite_runtime_local(const char *role_hint, const char *account_type) {
3266 if (role_hint != NULL && role_hint[0] != '\0') {
3267 if (streq(role_hint, "owner") || streq(role_hint, "business_owner")) {
3268 return "business_owner";
3269 }
3270 if (streq(role_hint, "manager") || streq(role_hint, "business_manager")) {
3271 return "business_manager";
3272 }
3273 if (streq(role_hint, "technician") || streq(role_hint, "business_technician")) {
3274 return "business_technician";
3275 }
3276 if (streq(role_hint, "customer") || streq(role_hint, "business_customer")) {
3277 return "business_customer";
3278 }
3279 }
3280 if (account_type != NULL && account_type[0] != '\0') {
3281 if (streq(account_type, "business_owner")) {
3282 return "business_owner";
3283 }
3284 if (streq(account_type, "business_manager")) {
3285 return "business_manager";
3286 }
3287 if (streq(account_type, "business_technician")) {
3288 return "business_technician";
3289 }
3290 }
3291 return "business_customer";
3292}
3293
3294/**
3295 * @brief Appends a native auth/rate-limit event row to runtime state.
3296 *
3297 * @param state_path Runtime state JSON path.
3298 * @param policy_key Policy identifier.
3299 * @param bucket_key Bucket key or actor key.
3300 * @param outcome Result label.
3301 * @return 0 on success, otherwise non-zero.
3302 */
3303static int persist_app_state_text_runtime_local(const AppRuntime *app, const char *state_json_text) {
3304 int code = 69;
3305 char *backend = NULL;
3306 char *export_json = NULL;
3307 if (app == NULL || app->state_path == NULL || state_json_text == NULL) {
3308 return 69;
3309 }
3310 code = write_text_file(app->state_path, state_json_text);
3311 if (code != 0) {
3312 return code;
3313 }
3314 if (app->app_root != NULL && app->app_id != NULL) {
3316 if (backend != NULL) {
3318 if (code != 0) {
3319 free(backend);
3320 return code;
3321 }
3323 if (export_json != NULL) {
3324 code = write_text_file(app->state_path, export_json);
3325 }
3326 free(export_json);
3327 free(backend);
3328 if (code != 0) {
3329 return code;
3330 }
3331 }
3332 }
3333 return 0;
3334}
3335
3336static int append_rate_limit_event_runtime_local(const AppRuntime *app, const char *policy_key, const char *bucket_key, const char *outcome) {
3337 yyjson_doc *doc = NULL;
3338 yyjson_mut_doc *mut_doc = NULL;
3339 yyjson_mut_val *mut_root = NULL;
3340 yyjson_mut_val *events = NULL;
3341 yyjson_mut_val *event_obj = NULL;
3342 yyjson_val *source_events = NULL;
3343 char created_at[21];
3344 char *text = NULL;
3345 int code = 69;
3346 if (app == NULL || app->state_path == NULL || policy_key == NULL || policy_key[0] == '\0' || !now_iso_utc_runtime(created_at)) {
3347 return 69;
3348 }
3350 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
3351 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
3352 source_events = doc != NULL ? state_array_runtime_local(yyjson_doc_get_root(doc), "rate_limit_events") : NULL;
3353 events = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "rate_limit_events") : NULL;
3354 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
3356 yyjson_mut_doc_free(mut_doc);
3357 return 69;
3358 }
3359 if (events == NULL || !yyjson_mut_is_arr(events)) {
3360 events = yyjson_mut_arr(mut_doc);
3361 if (events == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "rate_limit_events"), events)) {
3363 yyjson_mut_doc_free(mut_doc);
3364 return 69;
3365 }
3366 }
3367 event_obj = yyjson_mut_obj(mut_doc);
3368 if (event_obj == NULL
3369 || !yyjson_mut_obj_add_int(mut_doc, event_obj, "id", next_array_id_runtime_local(source_events))
3370 || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "policy_key", policy_key)
3371 || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "bucket_key", bucket_key != NULL ? bucket_key : "")
3372 || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "outcome", outcome != NULL ? outcome : "observed")
3373 || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "created_at", created_at)
3374 || !yyjson_mut_arr_append(events, event_obj)) {
3376 yyjson_mut_doc_free(mut_doc);
3377 return 69;
3378 }
3379 text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
3380 if (text != NULL) {
3381 code = persist_app_state_text_runtime_local(app, text);
3382 }
3383 free(text);
3385 yyjson_mut_doc_free(mut_doc);
3386 return code;
3387}
3388
3389static long active_user_id_from_cookie_runtime(const char *state_path, const char *cookie_header) {
3390 yyjson_doc *doc = NULL;
3391 yyjson_val *root = NULL;
3392 yyjson_val *sessions = NULL;
3393 yyjson_arr_iter iter;
3394 yyjson_val *session = NULL;
3395 char *session_token = NULL;
3396 char *session_token_hash = NULL;
3397 char now_iso[21];
3398 long user_id = 0;
3399 if (state_path == NULL || cookie_header == NULL || !now_iso_utc_runtime(now_iso)) {
3400 return 0;
3401 }
3402 session_token = native_dynamic_cookie_value(cookie_header, SESSION_COOKIE_NAME);
3403 if (session_token == NULL || session_token[0] == '\0') {
3404 free(session_token);
3405 return 0;
3406 }
3407 session_token_hash = token_hash_dup_runtime(session_token);
3408 free(session_token);
3409 if (session_token_hash == NULL) {
3410 return 0;
3411 }
3412 doc = native_json_doc_load_file(state_path);
3413 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
3414 sessions = root != NULL ? state_array_runtime_local(root, "sessions") : NULL;
3415 if (sessions != NULL && yyjson_arr_iter_init(sessions, &iter)) {
3416 while ((session = yyjson_arr_iter_next(&iter)) != NULL) {
3417 yyjson_val *hash_value = native_json_obj_get(session, "session_token_hash");
3418 char *expires_at = NULL;
3419 char *revoked_at = NULL;
3420 if (!yyjson_is_obj(session) || !yyjson_is_str(hash_value) || !yyjson_equals_str(hash_value, session_token_hash)) {
3421 continue;
3422 }
3423 expires_at = native_json_obj_get_string_dup(session, "expires_at");
3424 revoked_at = native_json_obj_get_string_dup(session, "revoked_at");
3425 if ((revoked_at == NULL || revoked_at[0] == '\0') && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
3426 user_id = native_json_obj_get_long_default(session, "user_id", 0, NULL);
3427 free(expires_at);
3428 free(revoked_at);
3429 break;
3430 }
3431 free(expires_at);
3432 free(revoked_at);
3433 }
3434 }
3435 free(session_token_hash);
3437 return user_id;
3438}
3439
3440static char *session_cookie_header_dup_runtime(const AppRuntime *app, const HttpRequest *request, const char *session_token, int clear_cookie) {
3441 const char *base_path = app != NULL && app->base_path != NULL && app->base_path[0] != '\0' ? app->base_path : "";
3442 const char *same_site = "Lax";
3443 const char *headers = request != NULL ? request->headers : NULL;
3444 int secure = headers != NULL && (strstr(headers, "X-Forwarded-Proto:https") != NULL || strstr(headers, "X-Forwarded-Proto: https") != NULL || strstr(headers, "Origin:https://") != NULL || strstr(headers, "Origin: https://") != NULL);
3445 const char *path_suffix = (base_path[0] == '\0' || streq(base_path, "/")) ? "/" : NULL;
3446 const char *token = clear_cookie ? "" : (session_token != NULL ? session_token : "");
3447 const char *max_age = clear_cookie ? "0" : "604800";
3448 char *header = NULL;
3449 size_t needed;
3450 needed = strlen("Set-Cookie: ") + strlen(SESSION_COOKIE_NAME) + 1 + strlen(token)
3451 + strlen("; Path=") + (path_suffix != NULL ? strlen(path_suffix) : strlen(base_path) + 1)
3452 + strlen("; Max-Age=") + strlen(max_age)
3453 + strlen("; HttpOnly; SameSite=") + strlen(same_site)
3454 + (secure ? strlen("; Secure") : 0) + 1;
3455 header = (char *)malloc(needed);
3456 if (header == NULL) {
3457 return NULL;
3458 }
3459 snprintf(header, needed, "Set-Cookie: %s=%s; Path=%s%s; Max-Age=%s; HttpOnly; SameSite=%s%s",
3461 token,
3462 path_suffix != NULL ? path_suffix : base_path,
3463 path_suffix != NULL ? "" : "/",
3464 max_age,
3465 same_site,
3466 secure ? "; Secure" : "");
3467 return header;
3468}
3469
3471 const char *account_type = yyjson_get_str(native_json_obj_get(user, "account_type"));
3472 if (account_type != NULL && (streq(account_type, "business_owner") || streq(account_type, "business_manager") || streq(account_type, "business_technician"))) {
3473 return "/businesses/";
3474 }
3475 return "/explore/";
3476}
3477
3478static int revoke_session_by_cookie_runtime(const AppRuntime *app, const char *cookie_header) {
3479 yyjson_doc *doc = NULL;
3480 yyjson_mut_doc *mut_doc = NULL;
3481 yyjson_mut_val *mut_root = NULL;
3482 yyjson_mut_val *sessions = NULL;
3483 yyjson_mut_val *session = NULL;
3484 char *session_token = NULL;
3485 char *session_token_hash = NULL;
3486 char revoked_at[21];
3487 size_t idx = 0;
3488 size_t max = 0;
3489 int changed = 0;
3490 if (app == NULL || app->state_path == NULL || cookie_header == NULL || !now_iso_utc_runtime(revoked_at)) {
3491 return 0;
3492 }
3493 session_token = native_dynamic_cookie_value(cookie_header, SESSION_COOKIE_NAME);
3494 if (session_token == NULL || session_token[0] == '\0') {
3495 free(session_token);
3496 return 0;
3497 }
3498 session_token_hash = token_hash_dup_runtime(session_token);
3499 free(session_token);
3500 if (session_token_hash == NULL) {
3501 return 69;
3502 }
3504 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
3505 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
3506 sessions = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "sessions") : NULL;
3507 if (sessions != NULL && yyjson_mut_is_arr(sessions)) {
3508 yyjson_mut_arr_foreach(sessions, idx, max, session) {
3509 yyjson_mut_val *hash_value = yyjson_mut_obj_get(session, "session_token_hash");
3510 yyjson_mut_val *revoked_value = yyjson_mut_obj_get(session, "revoked_at");
3511 if (!yyjson_mut_is_obj(session)) continue;
3512 if (hash_value != NULL && yyjson_is_str((yyjson_val *)hash_value) && yyjson_equals_str((yyjson_val *)hash_value, session_token_hash)
3513 && (revoked_value == NULL || yyjson_is_null((yyjson_val *)revoked_value) || (yyjson_is_str((yyjson_val *)revoked_value) && yyjson_get_len((yyjson_val *)revoked_value) == 0))) {
3514 if (!yyjson_mut_obj_put(session, yyjson_mut_strcpy(mut_doc, "revoked_at"), yyjson_mut_strcpy(mut_doc, revoked_at))) {
3515 free(session_token_hash);
3517 yyjson_mut_doc_free(mut_doc);
3518 return 69;
3519 }
3520 changed = 1;
3521 }
3522 }
3523 }
3524 free(session_token_hash);
3525 if (changed) {
3526 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
3527 int code = text != NULL ? persist_app_state_text_runtime_local(app, text) : 69;
3528 free(text);
3530 yyjson_mut_doc_free(mut_doc);
3531 return code;
3532 }
3534 yyjson_mut_doc_free(mut_doc);
3535 return 0;
3536}
3537
3538static int issue_browser_session_runtime(const AppRuntime *app, const HttpRequest *request, long user_id, char **session_token_out, char **session_cookie_header_out) {
3539 yyjson_doc *doc = NULL;
3540 yyjson_mut_doc *mut_doc = NULL;
3541 yyjson_mut_val *mut_root = NULL;
3542 yyjson_mut_val *sessions = NULL;
3543 yyjson_mut_val *session_obj = NULL;
3544 yyjson_val *existing_sessions = NULL;
3545 yyjson_arr_iter existing_iter;
3546 yyjson_val *item = NULL;
3547 long session_id = 1;
3548 char *session_token = NULL;
3549 char *session_token_hash = NULL;
3550 char created_at[21];
3551 char expires_at[21];
3552 char *text = NULL;
3553 size_t idx = 0;
3554 if (session_token_out != NULL) *session_token_out = NULL;
3555 if (session_cookie_header_out != NULL) *session_cookie_header_out = NULL;
3556 if (app == NULL || app->state_path == NULL || user_id <= 0 || !now_iso_utc_runtime(created_at) || !iso_after_seconds_runtime(604800, expires_at)) {
3557 return 69;
3558 }
3559 session_token = (char *)malloc(CSRF_TOKEN_HEX_LEN + 1);
3560 if (session_token == NULL) {
3561 return 69;
3562 }
3563 csrf_generate_random_hex(session_token, 24);
3564 session_token_hash = token_hash_dup_runtime(session_token);
3565 if (session_token_hash == NULL) {
3566 free(session_token);
3567 return 69;
3568 }
3570 existing_sessions = doc != NULL ? state_array_runtime_local(yyjson_doc_get_root(doc), "sessions") : NULL;
3571 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
3572 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
3573 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
3574 free(session_token_hash);
3575 free(session_token);
3577 yyjson_mut_doc_free(mut_doc);
3578 return 69;
3579 }
3580 sessions = yyjson_mut_obj_get(mut_root, "sessions");
3581 if (existing_sessions != NULL && yyjson_arr_iter_init(existing_sessions, &existing_iter)) {
3582 while ((item = yyjson_arr_iter_next(&existing_iter)) != NULL) {
3583 long existing_id;
3584 if (!yyjson_is_obj(item)) continue;
3585 existing_id = native_json_obj_get_long_default(item, "id", 0, NULL);
3586 if (existing_id >= session_id) session_id = existing_id + 1;
3587 }
3588 }
3589 if (sessions == NULL || !yyjson_mut_is_arr(sessions)) {
3590 sessions = yyjson_mut_arr(mut_doc);
3591 if (sessions == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "sessions"), sessions)) {
3592 free(session_token_hash);
3593 free(session_token);
3595 yyjson_mut_doc_free(mut_doc);
3596 return 69;
3597 }
3598 }
3599 session_obj = yyjson_mut_obj(mut_doc);
3600 if (session_obj == NULL
3601 || !yyjson_mut_obj_add_int(mut_doc, session_obj, "id", session_id)
3602 || !yyjson_mut_obj_add_int(mut_doc, session_obj, "user_id", user_id)
3603 || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "session_token_hash", session_token_hash)
3604 || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "session_kind", "browser_session")
3605 || !yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "current_tenant_id"), yyjson_mut_null(mut_doc))
3606 || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "expires_at", expires_at)
3607 || !yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "revoked_at"), yyjson_mut_null(mut_doc))
3608 || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "created_at", created_at)
3609 || !yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "user_agent"), yyjson_mut_null(mut_doc))
3610 || !yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "ip_address"), yyjson_mut_null(mut_doc))
3611 || !yyjson_mut_arr_append(sessions, session_obj)) {
3612 free(session_token_hash);
3613 free(session_token);
3615 yyjson_mut_doc_free(mut_doc);
3616 return 69;
3617 }
3618 text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
3619 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
3620 free(text);
3621 free(session_token_hash);
3622 free(session_token);
3624 yyjson_mut_doc_free(mut_doc);
3625 return 69;
3626 }
3627 free(text);
3628 free(session_token_hash);
3629 if (session_cookie_header_out != NULL) {
3630 *session_cookie_header_out = session_cookie_header_dup_runtime(app, request, session_token, 0);
3631 }
3632 if (session_token_out != NULL) {
3633 *session_token_out = session_token;
3634 } else {
3635 free(session_token);
3636 }
3638 yyjson_mut_doc_free(mut_doc);
3639 return 0;
3640}
3641
3642/**
3643 * @brief Injects CSRF hidden inputs into form tags in an HTML string.
3644 *
3645 * Scans @p html for <form ... method="post" ...> tags and inserts
3646 * a hidden input field carrying the CSRF token after the opening tag.
3647 *
3648 * @param html Original HTML string.
3649 * @param csrf_token CSRF token value to inject.
3650 * @return Newly allocated modified HTML string, or NULL on failure.
3651 * Caller must free. If injection is not needed, returns a
3652 * strdup of the original html.
3653 */
3654static char *csrf_inject_hidden_inputs(const char *html, const char *csrf_token) {
3655 const char *scan;
3656 char *result;
3657 size_t result_cap;
3658 size_t result_len;
3659 char input_tag[256];
3660 int input_len;
3661
3662 if (html == NULL || csrf_token == NULL) {
3663 return html != NULL ? strdup(html) : NULL;
3664 }
3665
3666 input_len = snprintf(input_tag, sizeof(input_tag),
3667 "<input type=\"hidden\" name=\"csrf_token\" value=\"%s\" />",
3668 csrf_token);
3669 if (input_len < 0 || (size_t)input_len >= sizeof(input_tag)) {
3670 return strdup(html);
3671 }
3672
3673 result_cap = strlen(html) + (size_t)input_len + 64;
3674 result = (char *)malloc(result_cap);
3675 if (result == NULL) {
3676 return strdup(html);
3677 }
3678 result_len = 0;
3679 scan = html;
3680
3681 while (*scan != '\0') {
3682 const char *form_start = NULL;
3683 const char *form_open_end = NULL;
3684 const char *lower_check;
3685 size_t tag_len;
3686
3687 while (*scan != '\0' && *scan != '<') {
3688 if (result_len + 2 > result_cap) {
3689 size_t new_cap = result_cap * 2;
3690 char *grown = (char *)realloc(result, new_cap);
3691 if (grown == NULL) {
3692 free(result);
3693 return strdup(html);
3694 }
3695 result = grown;
3696 result_cap = new_cap;
3697 }
3698 result[result_len++] = *scan++;
3699 }
3700 if (*scan == '\0') {
3701 break;
3702 }
3703
3704 form_start = scan;
3705 lower_check = scan + 1;
3706 while (*lower_check == ' ') {
3707 lower_check++;
3708 }
3709 if ((lower_check[0] == 'f' || lower_check[0] == 'F') &&
3710 (lower_check[1] == 'o' || lower_check[1] == 'O') &&
3711 (lower_check[2] == 'r' || lower_check[2] == 'R') &&
3712 (lower_check[3] == 'm' || lower_check[3] == 'M') &&
3713 (lower_check[4] == ' ' || lower_check[4] == '>' || lower_check[4] == '\t' || lower_check[4] == '\n')) {
3714 int is_post_form = 0;
3715 form_open_end = lower_check + 4;
3716 while (*form_open_end != '\0' && *form_open_end != '>') {
3717 form_open_end++;
3718 }
3719 if (*form_open_end == '>') {
3720 const char *method_attr = lower_check + 4;
3721 while ((method_attr = strstr(method_attr, "method")) != NULL && method_attr < form_open_end) {
3722 const char *equals = method_attr + 6;
3723 while (equals < form_open_end && (*equals == ' ' || *equals == '\t' || *equals == '\n')) {
3724 equals++;
3725 }
3726 if (equals < form_open_end && *equals == '=') {
3727 const char *value = equals + 1;
3728 while (value < form_open_end && (*value == ' ' || *value == '\t' || *value == '\n')) {
3729 value++;
3730 }
3731 if (value < form_open_end) {
3732 char quote = (*value == '\'' || *value == '"') ? *value++ : '\0';
3733 if ((form_open_end - value) >= 4) {
3734 if ((value[0] == 'p' || value[0] == 'P') &&
3735 (value[1] == 'o' || value[1] == 'O') &&
3736 (value[2] == 's' || value[2] == 'S') &&
3737 (value[3] == 't' || value[3] == 'T') &&
3738 (quote == '\0' || value[4] == quote || value[4] == ' ' || value[4] == '\t' || value[4] == '\n' || value[4] == '>')) {
3739 is_post_form = 1;
3740 break;
3741 }
3742 }
3743 }
3744 }
3745 method_attr += 6;
3746 }
3747
3748 tag_len = (size_t)(form_open_end - form_start) + 1;
3749 while (result_len + tag_len + (is_post_form ? (size_t)input_len : 0) + 1 > result_cap) {
3750 size_t new_cap = result_cap * 2;
3751 char *grown = (char *)realloc(result, new_cap);
3752 if (grown == NULL) {
3753 free(result);
3754 return strdup(html);
3755 }
3756 result = grown;
3757 result_cap = new_cap;
3758 }
3759 memcpy(result + result_len, form_start, tag_len);
3760 result_len += tag_len;
3761 if (is_post_form) {
3762 memcpy(result + result_len, input_tag, (size_t)input_len);
3763 result_len += (size_t)input_len;
3764 }
3765 scan = form_open_end + 1;
3766 continue;
3767 }
3768 }
3769
3770 if (result_len + 2 > result_cap) {
3771 size_t new_cap = result_cap * 2;
3772 char *grown = (char *)realloc(result, new_cap);
3773 if (grown == NULL) {
3774 free(result);
3775 return strdup(html);
3776 }
3777 result = grown;
3778 result_cap = new_cap;
3779 }
3780 result[result_len++] = *scan++;
3781 }
3782 result[result_len] = '\0';
3783 return result;
3784}
3785
3786/**
3787 * @brief Writes an HTML response to a connection, optionally with a CSRF cookie.
3788 *
3789 * @param connection Connection to write to.
3790 * @param status HTTP status code.
3791 * @param status_text HTTP reason phrase.
3792 * @param html HTML body content.
3793 * @param csrf_token CSRF token to set as cookie, or NULL to skip.
3794 */
3796 NativeConnection *connection,
3797 int status,
3798 const char *status_text,
3799 const char *html,
3800 const char *csrf_token,
3801 const char *extra_headers
3802) {
3803 int body_len;
3804
3805 if (html == NULL) {
3806 native_send_text_response(connection, 500, "Internal Server Error", "null response body\n");
3807 return;
3808 }
3809 body_len = (int)strlen(html);
3811 connection,
3812 "HTTP/1.1 %d %s\r\n"
3813 "Content-Type: text/html; charset=utf-8\r\n"
3814 "Content-Length: %d\r\n"
3815 "Cache-Control: no-store\r\n"
3816 "Connection: close\r\n"
3817 "Set-Cookie: %s=%s; Path=/; HttpOnly; SameSite=Lax; Max-Age=%s\r\n"
3818 "%s"
3819 "\r\n%s",
3820 status,
3821 status_text,
3822 body_len,
3824 csrf_token != NULL ? csrf_token : "",
3826 extra_headers != NULL ? extra_headers : "",
3827 html
3828 );
3829}
3830
3832 NativeConnection *connection,
3833 int status,
3834 const char *status_text,
3835 const char *html,
3836 const char *csrf_token
3837) {
3838 write_html_response_with_headers_runtime(connection, status, status_text, html, csrf_token, NULL);
3839}
3840
3842 NativeConnection *connection,
3843 const AppRuntime *app,
3844 const HttpRequest *request,
3845 const char *page_id,
3846 const char *request_path,
3847 const char *body_params_json,
3848 const char *extra_headers
3849) {
3850
3851 char *page_spec_rel = NULL;
3852 char *page_spec_path = NULL;
3853 char *page_spec_text = NULL;
3854 yyjson_doc *page_spec_doc = NULL;
3855 yyjson_val *page_spec_root = NULL;
3856 char *template_file = NULL;
3857 char *layout_id = NULL;
3858 char *body_template_path = NULL;
3859 yyjson_doc *body_params_doc = NULL;
3860 yyjson_val *body_params_root = NULL;
3861 char *body_html = NULL;
3862 char *full_html = NULL;
3863 char *injected_html = NULL;
3864 char *title = NULL;
3865 char *csrf_token = NULL;
3866 int status = 500;
3867
3868 if (connection == NULL || app == NULL || app->app_root == NULL || page_id == NULL || body_params_json == NULL) {
3869 return 500;
3870 }
3871 page_spec_rel = declared_page_relative_path_dup_runtime(app->app_root, page_id);
3872 page_spec_path = page_spec_rel != NULL ? join_base_relative_path_dup_runtime(app->app_root, page_spec_rel) : NULL;
3873 page_spec_text = page_spec_path != NULL ? read_file_text(page_spec_path) : NULL;
3874 page_spec_root = page_spec_text != NULL ? load_root_from_text_runtime(page_spec_text, &page_spec_doc) : NULL;
3875 template_file = page_spec_root != NULL ? native_json_obj_get_string_dup(page_spec_root, "template_file") : NULL;
3876 layout_id = page_spec_root != NULL ? native_json_obj_get_string_dup(page_spec_root, "layout_id") : NULL;
3877 body_template_path = template_file != NULL ? join_base_relative_path_dup_runtime(app->app_root, template_file) : NULL;
3878 body_params_root = load_root_from_text_runtime(body_params_json, &body_params_doc);
3879 if (page_spec_root == NULL || template_file == NULL || layout_id == NULL || body_template_path == NULL || !path_exists(body_template_path) || body_params_root == NULL) {
3880 goto render_declared_cleanup;
3881 }
3882 title = page_title_dup_runtime(page_spec_root, body_params_root);
3883 body_html = render_file_template_dup_runtime(body_template_path, body_params_root);
3884 if (body_html == NULL) {
3885 goto render_declared_cleanup;
3886 }
3887 full_html = render_page_html_dup_runtime(
3888 app->app_root,
3889 app->base_path,
3890 layout_id,
3891 title,
3892 body_html,
3893 "en",
3894 request_path != NULL ? request_path : (request != NULL ? request->target : "/"),
3895 active_user_id_from_cookie_runtime(app->state_path, request != NULL ? request->cookie : NULL) > 0 ? 1 : 0,
3896 0
3897 );
3898 if (full_html == NULL) {
3899 goto render_declared_cleanup;
3900 }
3901 csrf_token = csrf_token_for_response(request != NULL ? request->cookie : NULL);
3902 injected_html = csrf_inject_hidden_inputs(full_html, csrf_token != NULL ? csrf_token : "");
3903 write_html_response_with_headers_runtime(connection, 200, "OK", injected_html != NULL ? injected_html : full_html, csrf_token != NULL ? csrf_token : "", extra_headers);
3904 status = 200;
3905
3906render_declared_cleanup:
3907 free(page_spec_rel);
3908 free(page_spec_path);
3909 free(page_spec_text);
3910 native_json_doc_free(page_spec_doc);
3911 free(template_file);
3912 free(layout_id);
3913 free(body_template_path);
3914 native_json_doc_free(body_params_doc);
3915 free(body_html);
3916 free(full_html);
3917 free(injected_html);
3918 free(title);
3919 free(csrf_token);
3920 return status;
3921}
3922
3924 NativeConnection *connection,
3925 const AppRuntime *app,
3926 const HttpRequest *request,
3927 const char *page_id,
3928 const char *request_path,
3929 const char *body_params_json
3930) {
3931 return render_declared_page_response_with_headers_runtime(connection, app, request, page_id, request_path, body_params_json, NULL);
3932}
3933
3934/**
3935 * @brief Extracts the query portion from a target string.
3936 *
3937 * @param target Raw request target (may contain a @c ? ).
3938 * @param dest Destination buffer.
3939 * @param dest_size Size of the destination buffer.
3940 */
3941static void extract_query(const char *target, char *dest, size_t dest_size) {
3942 const char *qmark;
3943
3944 dest[0] = '\0';
3945 if (target == NULL || dest_size == 0) {
3946 return;
3947 }
3948
3949 qmark = strchr(target, '?');
3950 if (qmark != NULL && *(qmark + 1) != '\0') {
3951 qmark++;
3952 snprintf(dest, dest_size, "%s", qmark);
3953 }
3954}
3955
3956/**
3957 * @brief Resolves invite-accept page parameters from the current app state.
3958 *
3959 * @param app Active app runtime.
3960 * @param match Matched route used to extract path parameters.
3961 * @param request_path Request path for parameter extraction.
3962 *
3963 * @return Newly allocated page-params JSON string, or @c NULL when no
3964 * active invite can be resolved.
3965 */
3967 const AppRuntime *app,
3968 const NativeDynamicRouteMatch *match,
3969 const char *request_path
3970) {
3971 char *invite_token = NULL;
3972 char *token_hash = NULL;
3973 yyjson_doc *state_doc = NULL;
3974 yyjson_val *state_root = NULL;
3975 yyjson_val *invites = NULL;
3976 yyjson_arr_iter invite_iter;
3977 yyjson_val *invite_row = NULL;
3978 char now_iso[21];
3979 char *body_params_json = NULL;
3980
3981 if (app == NULL || match == NULL || request_path == NULL) {
3982 return NULL;
3983 }
3984 invite_token = native_dynamic_dispatch_extract_param(match->path, request_path, "invite_token");
3985 token_hash = token_hash_dup_runtime(invite_token != NULL ? invite_token : "");
3986 state_doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
3987 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
3988 invites = state_root != NULL ? state_array_runtime_local(state_root, "invites") : NULL;
3989 if (invites != NULL && token_hash != NULL && now_iso_utc_runtime(now_iso) && yyjson_arr_iter_init(invites, &invite_iter)) {
3990 while ((invite_row = yyjson_arr_iter_next(&invite_iter)) != NULL) {
3991 yyjson_val *hash_value = native_json_obj_get(invite_row, "token_hash");
3992 yyjson_val *accepted_user_id = native_json_obj_get(invite_row, "accepted_user_id");
3993 char *expires_at = NULL;
3994 if (!yyjson_is_obj(invite_row) || !yyjson_is_str(hash_value) || !yyjson_equals_str(hash_value, token_hash)) {
3995 continue;
3996 }
3997 expires_at = native_json_obj_get_string_dup(invite_row, "expires_at");
3998 if ((accepted_user_id == NULL || yyjson_is_null(accepted_user_id) || native_json_obj_get_long_default(invite_row, "accepted_user_id", 0, NULL) == 0)
3999 && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
4000 char *invite_kind = native_json_obj_get_string_dup(invite_row, "invite_kind");
4001 char *invite_email = native_json_obj_get_string_dup(invite_row, "email");
4002 long business_id = 0;
4003 long tenant_id = native_json_obj_get_long_default(invite_row, "target_tenant_id", 0, NULL);
4004 char *target_business_name = NULL;
4005 if (tenant_id > 0) {
4006 yyjson_val *tenants = state_array_runtime_local(state_root, "tenants");
4007 yyjson_arr_iter tenant_iter;
4008 yyjson_val *tenant_row = NULL;
4009 if (tenants != NULL && yyjson_arr_iter_init(tenants, &tenant_iter)) {
4010 while ((tenant_row = yyjson_arr_iter_next(&tenant_iter)) != NULL) {
4011 if (yyjson_is_obj(tenant_row) && native_json_obj_get_long_default(tenant_row, "id", 0, NULL) == tenant_id) {
4012 business_id = native_json_obj_get_long_default(tenant_row, "source_business_id", 0, NULL);
4013 break;
4014 }
4015 }
4016 }
4017 }
4018 if (business_id > 0) {
4019 yyjson_val *business = find_business_by_id_runtime_local(state_root, business_id);
4020 target_business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
4021 }
4023 invite_kind != NULL ? invite_kind : "business_invite",
4024 target_business_name != NULL ? target_business_name : "",
4025 "",
4026 invite_email != NULL ? invite_email : "",
4027 request_path
4028 );
4029 free(invite_kind);
4030 free(invite_email);
4031 free(target_business_name);
4032 free(expires_at);
4033 break;
4034 }
4035 free(expires_at);
4036 }
4037 }
4038 free(invite_token);
4039 free(token_hash);
4040 native_json_doc_free(state_doc);
4041 return body_params_json;
4042}
4043
4044/**
4045 * @brief Resolves declared page parameter JSON for the matched page route.
4046 *
4047 * @param app Active app runtime.
4048 * @param request Active HTTP request.
4049 * @param match Matched page route.
4050 * @param request_path Request path used for route-param extraction.
4051 * @param query_string Raw query string.
4052 * @param active_user_id Authenticated user id, or @c 0 when anonymous.
4053 *
4054 * @return Newly allocated page-params JSON string.
4055 */
4057 const AppRuntime *app,
4058 const HttpRequest *request,
4059 const NativeDynamicRouteMatch *match,
4060 const char *request_path,
4061 const char *query_string,
4062 long active_user_id
4063) {
4064 char *body_params_json = NULL;
4065
4066 if (app == NULL || match == NULL || match->page_id == NULL) {
4067 return strdup("{\"error_html\":\"\",\"next_input\":\"\"}");
4068 }
4069
4070 if (streq(match->page_id, "profile") && active_user_id > 0) {
4071 yyjson_doc *state_doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
4072 yyjson_val *state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
4073 yyjson_val *user = state_root != NULL ? find_user_by_id_runtime_local(state_root, active_user_id) : NULL;
4074 char *selected_payment_preference = user != NULL ? native_json_obj_get_string_dup(user, "payment_preference") : NULL;
4075 char *payment_preference_options_html = payment_preference_options_html_dup_runtime(selected_payment_preference != NULL ? selected_payment_preference : "pay_on_site");
4077 app->state_path,
4078 active_user_id,
4079 app->app_root,
4080 payment_preference_options_html != NULL ? payment_preference_options_html : ""
4081 );
4082 free(selected_payment_preference);
4083 free(payment_preference_options_html);
4084 native_json_doc_free(state_doc);
4085 return body_params_json;
4086 }
4087 if (streq(match->page_id, "login")) {
4088 return login_page_params_json_dup_runtime(query_string, "");
4089 }
4090 if (streq(match->page_id, "register")) {
4091 return register_page_params_json_dup_runtime(query_string);
4092 }
4093 if (streq(match->page_id, "password_reset_request")) {
4095 }
4096 if (streq(match->page_id, "password_reset_complete")) {
4098 }
4099 if (streq(match->page_id, "invite_accept")) {
4100 return resolve_invite_accept_page_params_json_runtime(app, match, request_path);
4101 }
4102 if (streq(match->page_id, "explore")) {
4104 }
4105 if (streq(match->page_id, "appointments") && active_user_id > 0) {
4106 return appointments_render_page_params_json_dup_runtime(app->state_path, active_user_id, app->app_root);
4107 }
4108 if (streq(match->page_id, "business_dashboard") && active_user_id > 0) {
4110 }
4111 if (streq(match->page_id, "business_landing")) {
4112 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4114 app->state_path,
4115 business_slug != NULL ? business_slug : "",
4116 active_user_id,
4117 app->app_root
4118 );
4119 free(business_slug);
4120 return body_params_json;
4121 }
4122 if (streq(match->page_id, "business_calendar")) {
4123 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4124 char *selected_year = native_dynamic_query_value(query_string, "year");
4125 char *selected_month = native_dynamic_query_value(query_string, "month");
4126 char *selected_date = native_dynamic_query_value(query_string, "date");
4128 app->state_path,
4129 business_slug != NULL ? business_slug : "",
4130 app->app_root,
4131 selected_year,
4132 selected_month,
4133 selected_date
4134 );
4135 free(business_slug);
4136 free(selected_year);
4137 free(selected_month);
4138 free(selected_date);
4139 return body_params_json;
4140 }
4141 if (streq(match->page_id, "diagnostics")) {
4143 }
4144 if (streq(match->page_id, "business_detail_admin")) {
4145 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4147 app->state_path,
4148 business_slug != NULL ? business_slug : "",
4149 app->app_root
4150 );
4151 free(business_slug);
4152 return body_params_json;
4153 }
4154 if (streq(match->page_id, "service_book_form")) {
4155 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4156 char *service_id_text = native_dynamic_dispatch_extract_param(match->path, request_path, "service_id");
4157 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
4159 app->state_path,
4160 business_slug != NULL ? business_slug : "",
4161 service_id,
4162 app->app_root,
4163 NULL
4164 );
4165 free(business_slug);
4166 free(service_id_text);
4167 return body_params_json;
4168 }
4169 if (streq(match->page_id, "business_edit")) {
4170 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4172 app->state_path,
4173 business_slug != NULL ? business_slug : ""
4174 );
4175 free(business_slug);
4176 return body_params_json;
4177 }
4178 if (streq(match->page_id, "service_edit")) {
4179 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4180 char *service_id_text = native_dynamic_dispatch_extract_param(match->path, request_path, "service_id");
4181 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
4183 app->state_path,
4184 business_slug != NULL ? business_slug : "",
4185 service_id
4186 );
4187 free(business_slug);
4188 free(service_id_text);
4189 return body_params_json;
4190 }
4191 if (streq(match->page_id, "generated_admin_manage")) {
4192 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4193 const char *surface_kind = "services";
4194 if (match->route_id != NULL && streq(match->route_id, "availability_manage")) {
4195 surface_kind = "availability";
4196 } else if (match->route_id != NULL && streq(match->route_id, "technician_manage")) {
4197 surface_kind = "technicians";
4198 }
4200 app->state_path,
4201 business_slug != NULL ? business_slug : "",
4202 surface_kind,
4203 ""
4204 );
4205 free(business_slug);
4206 return body_params_json;
4207 }
4208 if (app->app_id != NULL && streq(app->app_id, "dynamic_app_template") && streq(match->page_id, "home")) {
4210 }
4211 if (streq(match->page_id, "business_availability_edit")) {
4212 char *business_slug = native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug");
4213 char *availability_id_text = native_dynamic_dispatch_extract_param(match->path, request_path, "availability_id");
4214 long availability_id = availability_id_text != NULL ? strtol(availability_id_text, NULL, 10) : 0;
4216 app->state_path,
4217 business_slug != NULL ? business_slug : "",
4218 availability_id,
4219 ""
4220 );
4221 free(business_slug);
4222 free(availability_id_text);
4223 return body_params_json;
4224 }
4225
4226 (void)request;
4227 return strdup("{\"error_html\":\"\",\"next_input\":\"\"}");
4228}
4229
4230/**
4231 * @brief Handles password-reset request action processing.
4232 *
4233 * @param connection Active client connection.
4234 * @param app Active app runtime.
4235 * @param request Active HTTP request.
4236 *
4237 * @return HTTP status code written to the connection.
4238 */
4240 NativeConnection *connection,
4241 const AppRuntime *app,
4242 const HttpRequest *request
4243) {
4244 char *email = request_form_value_dup_runtime(request, "email");
4245 char *preview_path = NULL;
4246 yyjson_doc *doc = NULL;
4247 yyjson_mut_doc *mut_doc = NULL;
4248 yyjson_val *root = NULL;
4249 yyjson_mut_val *mut_root = NULL;
4250 yyjson_mut_val *password_resets = NULL;
4251 yyjson_val *user = NULL;
4252 char created_at[21];
4253 char expires_at[21];
4254 int write_code = 0;
4255
4256 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
4257 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
4258 if (root != NULL && email != NULL && email[0] != '\0' && now_iso_utc_runtime(created_at) && iso_after_seconds_runtime(7200, expires_at)) {
4259 yyjson_val *users = state_array_runtime_local(root, "users");
4260 yyjson_arr_iter iter;
4261 yyjson_val *row = NULL;
4262 if (users != NULL && yyjson_arr_iter_init(users, &iter)) {
4263 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
4264 char *row_email = native_json_obj_get_string_dup(row, "email");
4265 int matches = row_email != NULL && streq(row_email, email) && yyjson_get_bool(native_json_obj_get(row, "is_active"));
4266 free(row_email);
4267 if (matches) {
4268 user = row;
4269 break;
4270 }
4271 }
4272 }
4273 if (user != NULL) {
4274 char token[CSRF_TOKEN_HEX_LEN + 1];
4275 char *token_hash = NULL;
4276 long user_id = native_json_obj_get_long_default(user, "id", 0, NULL);
4277 mut_doc = yyjson_doc_mut_copy(doc, NULL);
4278 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
4279 password_resets = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "password_resets") : NULL;
4280 if (mut_doc != NULL && mut_root != NULL && yyjson_mut_is_obj(mut_root)) {
4281 if (password_resets == NULL || !yyjson_mut_is_arr(password_resets)) {
4282 password_resets = yyjson_mut_arr(mut_doc);
4283 if (password_resets != NULL) {
4284 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "password_resets"), password_resets);
4285 }
4286 }
4287 }
4288 csrf_generate_random_hex(token, 24);
4289 token_hash = token_hash_dup_runtime(token);
4290 if (mut_doc != NULL && mut_root != NULL && password_resets != NULL && token_hash != NULL) {
4291 yyjson_val *source_resets = state_array_runtime_local(root, "password_resets");
4292 yyjson_mut_val *reset_obj = yyjson_mut_obj(mut_doc);
4293 long reset_id = next_array_id_runtime_local(source_resets);
4294 if (reset_obj != NULL
4295 && yyjson_mut_obj_add_int(mut_doc, reset_obj, "id", reset_id)
4296 && yyjson_mut_obj_add_int(mut_doc, reset_obj, "user_id", user_id)
4297 && yyjson_mut_obj_add_strcpy(mut_doc, reset_obj, "token_hash", token_hash)
4298 && yyjson_mut_obj_add_strcpy(mut_doc, reset_obj, "expires_at", expires_at)
4299 && yyjson_mut_obj_put(reset_obj, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_null(mut_doc))
4300 && yyjson_mut_obj_add_strcpy(mut_doc, reset_obj, "created_at", created_at)
4301 && yyjson_mut_arr_append(password_resets, reset_obj)) {
4302 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
4303 if (text != NULL) {
4304 write_code = persist_app_state_text_runtime_local(app, text);
4305 free(text);
4306 } else {
4307 write_code = 69;
4308 }
4309 if (write_code == 0) {
4310 size_t needed = strlen("/accounts/password-reset/complete//") + strlen(token) + 1;
4311 preview_path = (char *)malloc(needed);
4312 if (preview_path != NULL) {
4313 snprintf(preview_path, needed, "/accounts/password-reset/complete/%s/", token);
4314 }
4315 }
4316 }
4317 }
4318 free(token_hash);
4319 }
4320 }
4321 free(email);
4323 yyjson_mut_doc_free(mut_doc);
4324 {
4325 char *body_params_json = password_reset_sent_page_params_json_dup_runtime(preview_path != NULL ? preview_path : "");
4326 int status = body_params_json != NULL
4327 ? render_declared_page_response_runtime(connection, app, request, "password_reset_sent", "/accounts/password-reset/", body_params_json)
4328 : 500;
4329 free(body_params_json);
4330 free(preview_path);
4331 return status;
4332 }
4333}
4334
4335/**
4336 * @brief Handles password-reset completion action processing.
4337 *
4338 * @param connection Active client connection.
4339 * @param app Active app runtime.
4340 * @param request Active HTTP request.
4341 * @param match Matched action route.
4342 *
4343 * @return HTTP status code written to the connection.
4344 */
4346 NativeConnection *connection,
4347 const AppRuntime *app,
4348 const HttpRequest *request,
4349 const NativeDynamicRouteMatch *match
4350) {
4351 char *request_path = request_path_dup_runtime(request);
4352 char *reset_token = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "reset_token") : NULL;
4353 char *password = request_form_value_dup_runtime(request, "password");
4354 char *password_confirm = request_form_value_dup_runtime(request, "password_confirm");
4355 yyjson_doc *doc = NULL;
4356 yyjson_mut_doc *mut_doc = NULL;
4357 yyjson_val *root = NULL;
4358 yyjson_mut_val *mut_root = NULL;
4359 yyjson_mut_val *password_resets = NULL;
4360 yyjson_mut_val *users = NULL;
4361 yyjson_mut_val *sessions = NULL;
4362 yyjson_mut_val *reset_row = NULL;
4363 yyjson_mut_val *user_row = NULL;
4364 char now_iso[21];
4365 char *token_hash = NULL;
4366 char *password_hash = NULL;
4367
4368 if (reset_token == NULL || reset_token[0] == '\0' || password == NULL || password[0] == '\0'
4369 || password_confirm == NULL || !streq(password, password_confirm) || strlen(password) < 8) {
4370 free(request_path);
4371 free(reset_token);
4372 free(password);
4373 free(password_confirm);
4374 write_json_error(connection, 422, "Unprocessable Entity", "invalid_password_reset", "Invalid password reset submission.");
4375 return 422;
4376 }
4377
4378 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
4379 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
4380 token_hash = token_hash_dup_runtime(reset_token);
4381 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
4382 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
4383 password_resets = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "password_resets") : NULL;
4384 users = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "users") : NULL;
4385 sessions = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "sessions") : NULL;
4386 if (root == NULL || mut_doc == NULL || mut_root == NULL || token_hash == NULL || !now_iso_utc_runtime(now_iso)
4387 || password_resets == NULL || !yyjson_mut_is_arr(password_resets) || users == NULL || !yyjson_mut_is_arr(users)) {
4388 free(request_path);
4389 free(reset_token);
4390 free(password);
4391 free(password_confirm);
4392 free(token_hash);
4394 yyjson_mut_doc_free(mut_doc);
4395 native_send_text_response(connection, 500, "Internal Server Error", "password reset state unavailable\n");
4396 return 500;
4397 }
4398
4399 {
4400 yyjson_val *source_resets = state_array_runtime_local(root, "password_resets");
4401 yyjson_val *source_users = state_array_runtime_local(root, "users");
4402 yyjson_arr_iter reset_iter;
4403 yyjson_val *source_reset_row = NULL;
4404 size_t reset_index = 0;
4405 size_t found_reset_index = (size_t)-1;
4406 size_t found_user_index = (size_t)-1;
4407 long matched_user_id = 0;
4408 if (source_resets != NULL && yyjson_arr_iter_init(source_resets, &reset_iter)) {
4409 while ((source_reset_row = yyjson_arr_iter_next(&reset_iter)) != NULL) {
4410 yyjson_val *hash_value = native_json_obj_get(source_reset_row, "token_hash");
4411 yyjson_val *claimed_value = native_json_obj_get(source_reset_row, "claimed_at");
4412 char *expires_at = NULL;
4413 if (!yyjson_is_obj(source_reset_row) || !yyjson_is_str(hash_value) || !yyjson_equals_str(hash_value, token_hash)) {
4414 reset_index++;
4415 continue;
4416 }
4417 expires_at = native_json_obj_get_string_dup(source_reset_row, "expires_at");
4418 if ((claimed_value == NULL || yyjson_is_null(claimed_value) || (yyjson_is_str(claimed_value) && yyjson_get_len(claimed_value) == 0))
4419 && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
4420 found_reset_index = reset_index;
4421 matched_user_id = native_json_obj_get_long_default(source_reset_row, "user_id", 0, NULL);
4422 free(expires_at);
4423 break;
4424 }
4425 free(expires_at);
4426 reset_index++;
4427 }
4428 }
4429 if (source_users != NULL && found_reset_index != (size_t)-1) {
4430 yyjson_arr_iter user_iter;
4431 yyjson_val *source_user_row = NULL;
4432 size_t user_index = 0;
4433 if (yyjson_arr_iter_init(source_users, &user_iter)) {
4434 while ((source_user_row = yyjson_arr_iter_next(&user_iter)) != NULL) {
4435 if (yyjson_is_obj(source_user_row) && native_json_obj_get_long_default(source_user_row, "id", 0, NULL) == matched_user_id) {
4436 found_user_index = user_index;
4437 break;
4438 }
4439 user_index++;
4440 }
4441 }
4442 }
4443 if (found_reset_index != (size_t)-1) {
4444 reset_row = yyjson_mut_arr_get(password_resets, found_reset_index);
4445 }
4446 if (found_user_index != (size_t)-1) {
4447 user_row = yyjson_mut_arr_get(users, found_user_index);
4448 }
4449 }
4450
4451 if (reset_row == NULL || user_row == NULL) {
4452 free(request_path);
4453 free(reset_token);
4454 free(password);
4455 free(password_confirm);
4456 free(token_hash);
4458 yyjson_mut_doc_free(mut_doc);
4459 write_json_error(connection, 404, "Not Found", "password_reset_not_found", "Password reset token not found.");
4460 return 404;
4461 }
4462
4463 password_hash = password_hash_dup_runtime(password);
4464 if (password_hash == NULL
4465 || !yyjson_mut_obj_put(reset_row, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_strcpy(mut_doc, now_iso))
4466 || !yyjson_mut_obj_put(user_row, yyjson_mut_strcpy(mut_doc, "password_hash"), yyjson_mut_strcpy(mut_doc, password_hash))) {
4467 free(request_path);
4468 free(reset_token);
4469 free(password);
4470 free(password_confirm);
4471 free(token_hash);
4472 free(password_hash);
4474 yyjson_mut_doc_free(mut_doc);
4475 native_send_text_response(connection, 500, "Internal Server Error", "password reset update failed\n");
4476 return 500;
4477 }
4478 if (sessions != NULL) {
4479 (void)sessions;
4480 }
4481 {
4482 char *render_path = request_path != NULL ? strdup(request_path) : strdup("/accounts/password-reset/");
4483 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
4484 int write_code = text != NULL ? persist_app_state_text_runtime_local(app, text) : 69;
4485 free(text);
4486 free(request_path);
4487 free(reset_token);
4488 free(password);
4489 free(password_confirm);
4490 free(token_hash);
4491 free(password_hash);
4493 yyjson_mut_doc_free(mut_doc);
4494 if (write_code != 0) {
4495 free(render_path);
4496 native_send_text_response(connection, 500, "Internal Server Error", "password reset persist failed\n");
4497 return 500;
4498 }
4499 {
4500 char *body_params_json = strdup("{}");
4501 int status = body_params_json != NULL && render_path != NULL
4502 ? render_declared_page_response_runtime(connection, app, request, "password_reset_complete_done", render_path, body_params_json)
4503 : 500;
4504 free(body_params_json);
4505 free(render_path);
4506 return status;
4507 }
4508 }
4509}
4510
4511/**
4512 * @brief Handles invite creation for tenant-backed business membership flows.
4513 *
4514 * @param connection Active client connection.
4515 * @param app Active app runtime.
4516 * @param request Active HTTP request.
4517 * @param match Matched action route.
4518 *
4519 * @return HTTP status code written to the connection.
4520 */
4522 NativeConnection *connection,
4523 const AppRuntime *app,
4524 const HttpRequest *request,
4525 const NativeDynamicRouteMatch *match
4526) {
4527 long active_user_id = active_user_id_from_cookie_runtime(app->state_path, request->cookie);
4528 char *request_path = request_path_dup_runtime(request);
4529 char *business_slug = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug") : NULL;
4530 char *email = request_form_value_dup_runtime(request, "email");
4531 char *account_type = request_form_value_dup_runtime(request, "account_type");
4532 char *role_hint = request_form_value_dup_runtime(request, "role_hint");
4533 yyjson_doc *doc = NULL;
4534 yyjson_val *root = NULL;
4535 yyjson_val *business = NULL;
4536 yyjson_mut_doc *mut_doc = NULL;
4537 yyjson_mut_val *mut_root = NULL;
4538 yyjson_mut_val *invites = NULL;
4539 long business_id = 0;
4540 long tenant_id = 0;
4541 char token[CSRF_TOKEN_HEX_LEN + 1];
4542 char *token_hash = NULL;
4543 char created_at[21];
4544 char expires_at[21];
4545 char *preview_path = NULL;
4546 char *return_path = NULL;
4547 char *render_path = NULL;
4548 char *invite_email_copy = NULL;
4549 int write_code = 69;
4550
4551 if (active_user_id <= 0 || business_slug == NULL || business_slug[0] == '\0' || email == NULL || email[0] == '\0' || !now_iso_utc_runtime(created_at) || !iso_after_seconds_runtime(604800, expires_at)) {
4552 free(request_path);
4553 free(business_slug);
4554 free(email);
4555 free(account_type);
4556 free(role_hint);
4557 write_json_error(connection, 422, "Unprocessable Entity", "invalid_invite_request", "Invalid invite submission.");
4558 return 422;
4559 }
4560
4561 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
4562 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
4563 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
4564 business_id = business != NULL ? native_json_obj_get_long_default(business, "id", 0, NULL) : 0;
4565 tenant_id = root != NULL ? state_tenant_id_for_business_runtime_local(root, business_id) : 0;
4566 if (root == NULL || business == NULL || tenant_id <= 0 || !user_has_active_tenant_membership_runtime_local(root, active_user_id, tenant_id)) {
4567 free(request_path);
4568 free(business_slug);
4569 free(email);
4570 free(account_type);
4571 free(role_hint);
4573 write_json_error(connection, 403, "Forbidden", "invite_forbidden", "Invite creation requires business membership.");
4574 return 403;
4575 }
4576
4577 mut_doc = yyjson_doc_mut_copy(doc, NULL);
4578 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
4579 invites = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "invites") : NULL;
4580 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
4581 free(request_path);
4582 free(business_slug);
4583 free(email);
4584 free(account_type);
4585 free(role_hint);
4587 yyjson_mut_doc_free(mut_doc);
4588 native_send_text_response(connection, 500, "Internal Server Error", "invite state unavailable\n");
4589 return 500;
4590 }
4591 if (invites == NULL || !yyjson_mut_is_arr(invites)) {
4592 invites = yyjson_mut_arr(mut_doc);
4593 if (invites == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "invites"), invites)) {
4594 free(request_path);
4595 free(business_slug);
4596 free(email);
4597 free(account_type);
4598 free(role_hint);
4600 yyjson_mut_doc_free(mut_doc);
4601 native_send_text_response(connection, 500, "Internal Server Error", "invite array unavailable\n");
4602 return 500;
4603 }
4604 }
4605
4606 csrf_generate_random_hex(token, 24);
4607 token_hash = token_hash_dup_runtime(token);
4608 if (token_hash != NULL) {
4609 yyjson_val *source_invites = state_array_runtime_local(root, "invites");
4610 yyjson_mut_val *invite_obj = yyjson_mut_obj(mut_doc);
4611 long invite_id = next_array_id_runtime_local(source_invites);
4612 const char *invite_kind = "business_invite";
4613 const char *membership_role_id = membership_role_id_for_invite_runtime_local(role_hint, account_type);
4614 if (invite_obj != NULL
4615 && yyjson_mut_obj_add_int(mut_doc, invite_obj, "id", invite_id)
4616 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "email", email)
4617 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "account_type", account_type != NULL && account_type[0] != '\0' ? account_type : "business_customer")
4618 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "invite_kind", invite_kind)
4619 && yyjson_mut_obj_add_int(mut_doc, invite_obj, "target_tenant_id", tenant_id)
4620 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "role_hint", membership_role_id)
4621 && yyjson_mut_obj_add_int(mut_doc, invite_obj, "invited_by_user_id", active_user_id)
4622 && yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "accepted_user_id"), yyjson_mut_null(mut_doc))
4623 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "token_hash", token_hash)
4624 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "expires_at", expires_at)
4625 && yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_null(mut_doc))
4626 && yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "created_at", created_at)
4627 && yyjson_mut_arr_append(invites, invite_obj)) {
4628 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
4629 if (text != NULL) {
4630 write_code = persist_app_state_text_runtime_local(app, text);
4631 }
4632 free(text);
4633 if (write_code == 0) {
4634 size_t preview_needed = strlen("/accounts/invite//") + strlen(token) + 1;
4635 size_t return_needed = strlen("/businesses//") + strlen(business_slug) + 1;
4636 preview_path = (char *)malloc(preview_needed);
4637 return_path = (char *)malloc(return_needed);
4638 render_path = request_path != NULL ? strdup(request_path) : strdup("/businesses/");
4639 invite_email_copy = email != NULL ? strdup(email) : strdup("");
4640 if (preview_path != NULL) {
4641 snprintf(preview_path, preview_needed, "/accounts/invite/%s/", token);
4642 }
4643 if (return_path != NULL) {
4644 snprintf(return_path, return_needed, "/businesses/%s/", business_slug);
4645 }
4646 }
4647 }
4648 }
4649
4650 free(request_path);
4651 free(business_slug);
4652 free(email);
4653 free(account_type);
4654 free(role_hint);
4655 free(token_hash);
4657 yyjson_mut_doc_free(mut_doc);
4658 if (write_code != 0 || preview_path == NULL || return_path == NULL || render_path == NULL || invite_email_copy == NULL) {
4659 free(preview_path);
4660 free(return_path);
4661 free(render_path);
4662 free(invite_email_copy);
4663 native_send_text_response(connection, 500, "Internal Server Error", "invite persist failed\n");
4664 return 500;
4665 }
4666 {
4667 char *body_params_json = invite_created_page_params_json_dup_runtime(invite_email_copy, preview_path, return_path);
4668 int status = body_params_json != NULL
4669 ? render_declared_page_response_runtime(connection, app, request, "invite_created", render_path, body_params_json)
4670 : 500;
4671 free(body_params_json);
4672 free(preview_path);
4673 free(return_path);
4674 free(render_path);
4675 free(invite_email_copy);
4676 return status;
4677 }
4678}
4679
4680/**
4681 * @brief Handles invite acceptance and initial session creation.
4682 *
4683 * @param connection Active client connection.
4684 * @param app Active app runtime.
4685 * @param request Active HTTP request.
4686 * @param match Matched action route.
4687 *
4688 * @return HTTP status code written to the connection.
4689 */
4691 NativeConnection *connection,
4692 const AppRuntime *app,
4693 const HttpRequest *request,
4694 const NativeDynamicRouteMatch *match
4695) {
4696 char *request_path = request_path_dup_runtime(request);
4697 char *invite_token = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "invite_token") : NULL;
4698 char *username = request_form_value_dup_runtime(request, "username");
4699 char *full_name = request_form_value_dup_runtime(request, "full_name");
4700 char *phone_number = request_form_value_dup_runtime(request, "phone_number");
4701 char *password = request_form_value_dup_runtime(request, "password");
4702 char *password_confirm = request_form_value_dup_runtime(request, "password_confirm");
4703 yyjson_doc *doc = NULL;
4704 yyjson_val *root = NULL;
4705 yyjson_mut_doc *mut_doc = NULL;
4706 yyjson_mut_val *mut_root = NULL;
4707 yyjson_mut_val *invites = NULL;
4708 yyjson_mut_val *users = NULL;
4709 yyjson_mut_val *memberships = NULL;
4710 yyjson_mut_val *invite_row = NULL;
4711 char *token_hash = NULL;
4712 char *password_hash = NULL;
4713 char created_at[21];
4714 char *session_cookie_header = NULL;
4715 char *session_token = NULL;
4716 long tenant_id = 0;
4717 long invited_by_user_id = 0;
4718 long next_user_id = 1;
4719 long next_membership_id = 1;
4720 const char *membership_role_id = "business_customer";
4721 const char *account_type_value = "business_customer";
4722 const char *invite_email_value = "";
4723 const char *continue_path = "/explore/";
4724 size_t invite_index = (size_t)-1;
4725
4726 if (invite_token == NULL || invite_token[0] == '\0' || username == NULL || username[0] == '\0'
4727 || full_name == NULL || full_name[0] == '\0' || password == NULL || password[0] == '\0'
4728 || password_confirm == NULL || !streq(password, password_confirm) || strlen(password) < 8 || !now_iso_utc_runtime(created_at)) {
4729 free(request_path);
4730 free(invite_token);
4731 free(username);
4732 free(full_name);
4733 free(phone_number);
4734 free(password);
4735 free(password_confirm);
4736 write_json_error(connection, 422, "Unprocessable Entity", "invalid_invite_accept", "Invalid invite acceptance submission.");
4737 return 422;
4738 }
4739
4740 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
4741 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
4742 token_hash = token_hash_dup_runtime(invite_token);
4743 {
4744 int username_taken = root != NULL && find_user_by_username_runtime_local(root, username) != NULL;
4745 if (root == NULL || token_hash == NULL || username_taken) {
4746 free(request_path);
4747 free(invite_token);
4748 free(username);
4749 free(full_name);
4750 free(phone_number);
4751 free(password);
4752 free(password_confirm);
4753 free(token_hash);
4755 if (username_taken) {
4756 write_json_error(connection, 422, "Unprocessable Entity", "username_taken", "Username already exists.");
4757 return 422;
4758 }
4759 write_json_error(connection, 404, "Not Found", "invite_not_found", "Invite token not found.");
4760 return 404;
4761 }
4762 }
4763
4764 {
4765 yyjson_val *source_invites = state_array_runtime_local(root, "invites");
4766 yyjson_val *source_users = state_array_runtime_local(root, "users");
4767 yyjson_val *source_memberships = state_array_runtime_local(root, "tenant_memberships");
4768 yyjson_arr_iter iter;
4769 yyjson_val *row = NULL;
4770 char now_iso[21];
4771 size_t scan_index = 0;
4772 now_iso_utc_runtime(now_iso);
4773 next_user_id = next_array_id_runtime_local(source_users);
4774 next_membership_id = next_array_id_runtime_local(source_memberships);
4775 if (source_invites != NULL && yyjson_arr_iter_init(source_invites, &iter)) {
4776 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
4777 yyjson_val *hash_value = native_json_obj_get(row, "token_hash");
4778 yyjson_val *accepted_user_value = native_json_obj_get(row, "accepted_user_id");
4779 char *expires_at = NULL;
4780 if (!yyjson_is_obj(row) || !yyjson_is_str(hash_value) || !yyjson_equals_str(hash_value, token_hash)) {
4781 scan_index++;
4782 continue;
4783 }
4784 expires_at = native_json_obj_get_string_dup(row, "expires_at");
4785 if ((accepted_user_value == NULL || yyjson_is_null(accepted_user_value) || native_json_obj_get_long_default(row, "accepted_user_id", 0, NULL) == 0)
4786 && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
4787 invite_index = scan_index;
4788 tenant_id = native_json_obj_get_long_default(row, "target_tenant_id", 0, NULL);
4789 invited_by_user_id = native_json_obj_get_long_default(row, "invited_by_user_id", 0, NULL);
4790 membership_role_id = membership_role_id_for_invite_runtime_local(yyjson_get_str(native_json_obj_get(row, "role_hint")), yyjson_get_str(native_json_obj_get(row, "account_type")));
4791 account_type_value = yyjson_get_str(native_json_obj_get(row, "account_type"));
4792 invite_email_value = yyjson_get_str(native_json_obj_get(row, "email"));
4793 free(expires_at);
4794 break;
4795 }
4796 free(expires_at);
4797 scan_index++;
4798 }
4799 }
4800 }
4801
4802 if (invite_index == (size_t)-1) {
4803 free(request_path);
4804 free(invite_token);
4805 free(username);
4806 free(full_name);
4807 free(phone_number);
4808 free(password);
4809 free(password_confirm);
4810 free(token_hash);
4812 write_json_error(connection, 404, "Not Found", "invite_not_found", "Invite token not found.");
4813 return 404;
4814 }
4815
4816 password_hash = password_hash_dup_runtime(password);
4817 mut_doc = yyjson_doc_mut_copy(doc, NULL);
4818 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
4819 invites = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "invites") : NULL;
4820 users = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "users") : NULL;
4821 memberships = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "tenant_memberships") : NULL;
4822 if (password_hash == NULL || mut_doc == NULL || mut_root == NULL || invites == NULL || !yyjson_mut_is_arr(invites) || users == NULL || !yyjson_mut_is_arr(users)) {
4823 free(request_path);
4824 free(invite_token);
4825 free(username);
4826 free(full_name);
4827 free(phone_number);
4828 free(password);
4829 free(password_confirm);
4830 free(token_hash);
4831 free(password_hash);
4833 yyjson_mut_doc_free(mut_doc);
4834 native_send_text_response(connection, 500, "Internal Server Error", "invite accept state unavailable\n");
4835 return 500;
4836 }
4837 if (memberships == NULL || !yyjson_mut_is_arr(memberships)) {
4838 memberships = yyjson_mut_arr(mut_doc);
4839 if (memberships == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "tenant_memberships"), memberships)) {
4840 free(request_path);
4841 free(invite_token);
4842 free(username);
4843 free(full_name);
4844 free(phone_number);
4845 free(password);
4846 free(password_confirm);
4847 free(token_hash);
4848 free(password_hash);
4850 yyjson_mut_doc_free(mut_doc);
4851 native_send_text_response(connection, 500, "Internal Server Error", "membership array unavailable\n");
4852 return 500;
4853 }
4854 }
4855
4856 invite_row = yyjson_mut_arr_get(invites, invite_index);
4857 if (invite_row == NULL) {
4858 free(request_path);
4859 free(invite_token);
4860 free(username);
4861 free(full_name);
4862 free(phone_number);
4863 free(password);
4864 free(password_confirm);
4865 free(token_hash);
4866 free(password_hash);
4868 yyjson_mut_doc_free(mut_doc);
4869 native_send_text_response(connection, 500, "Internal Server Error", "invite row unavailable\n");
4870 return 500;
4871 }
4872 {
4873 yyjson_mut_val *user_obj = yyjson_mut_obj(mut_doc);
4874 yyjson_mut_val *membership_obj = yyjson_mut_obj(mut_doc);
4875 int ok = user_obj != NULL
4876 && yyjson_mut_obj_add_int(mut_doc, user_obj, "id", next_user_id)
4877 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "username", username)
4878 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "email", invite_email_value != NULL ? invite_email_value : "")
4879 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "password_hash", password_hash)
4880 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "display_name", full_name)
4881 && yyjson_mut_obj_add_bool(mut_doc, user_obj, "is_active", 1)
4882 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "account_type", account_type_value != NULL && account_type_value[0] != '\0' ? account_type_value : "business_customer")
4883 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "full_name", full_name)
4884 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "phone_number", phone_number != NULL ? phone_number : "")
4885 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "address", "")
4886 && yyjson_mut_obj_add_strcpy(mut_doc, user_obj, "payment_preference", "pay_on_site")
4887 && yyjson_mut_obj_add_bool(mut_doc, user_obj, "email_verified", 1)
4888 && yyjson_mut_arr_append(users, user_obj)
4889 && yyjson_mut_obj_put(invite_row, yyjson_mut_strcpy(mut_doc, "accepted_user_id"), yyjson_mut_int(mut_doc, next_user_id))
4890 && yyjson_mut_obj_put(invite_row, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_strcpy(mut_doc, created_at));
4891 if (ok && tenant_id > 0) {
4892 ok = membership_obj != NULL
4893 && yyjson_mut_obj_add_int(mut_doc, membership_obj, "id", next_membership_id)
4894 && yyjson_mut_obj_add_int(mut_doc, membership_obj, "tenant_id", tenant_id)
4895 && yyjson_mut_obj_add_int(mut_doc, membership_obj, "user_id", next_user_id)
4896 && yyjson_mut_obj_add_strcpy(mut_doc, membership_obj, "role_id", membership_role_id)
4897 && yyjson_mut_obj_add_strcpy(mut_doc, membership_obj, "membership_status", "active")
4898 && yyjson_mut_obj_add_int(mut_doc, membership_obj, "invited_by_user_id", invited_by_user_id)
4899 && yyjson_mut_obj_add_strcpy(mut_doc, membership_obj, "created_at", created_at)
4900 && yyjson_mut_obj_add_strcpy(mut_doc, membership_obj, "revoked_at", "")
4901 && yyjson_mut_arr_append(memberships, membership_obj);
4902 }
4903 if (!ok) {
4904 free(request_path);
4905 free(invite_token);
4906 free(username);
4907 free(full_name);
4908 free(phone_number);
4909 free(password);
4910 free(password_confirm);
4911 free(token_hash);
4912 free(password_hash);
4914 yyjson_mut_doc_free(mut_doc);
4915 native_send_text_response(connection, 500, "Internal Server Error", "invite accept update failed\n");
4916 return 500;
4917 }
4918 }
4919
4920 {
4921 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
4922 int write_code = text != NULL ? persist_app_state_text_runtime_local(app, text) : 69;
4923 free(text);
4924 if (write_code != 0) {
4925 free(request_path);
4926 free(invite_token);
4927 free(username);
4928 free(full_name);
4929 free(phone_number);
4930 free(password);
4931 free(password_confirm);
4932 free(token_hash);
4933 free(password_hash);
4935 yyjson_mut_doc_free(mut_doc);
4936 native_send_text_response(connection, 500, "Internal Server Error", "invite accept persist failed\n");
4937 return 500;
4938 }
4939 }
4940
4941 {
4942 yyjson_val *user_after = NULL;
4943 yyjson_val *root_after = NULL;
4945 root_after = doc_after != NULL ? yyjson_doc_get_root(doc_after) : NULL;
4946 user_after = root_after != NULL ? find_user_by_id_runtime_local(root_after, next_user_id) : NULL;
4947 if (tenant_id > 0) {
4948 yyjson_val *tenants = root_after != NULL ? state_array_runtime_local(root_after, "tenants") : NULL;
4949 yyjson_arr_iter tenant_iter;
4950 yyjson_val *tenant_row = NULL;
4951 long business_id = 0;
4952 if (tenants != NULL && yyjson_arr_iter_init(tenants, &tenant_iter)) {
4953 while ((tenant_row = yyjson_arr_iter_next(&tenant_iter)) != NULL) {
4954 if (yyjson_is_obj(tenant_row) && native_json_obj_get_long_default(tenant_row, "id", 0, NULL) == tenant_id) {
4955 business_id = native_json_obj_get_long_default(tenant_row, "source_business_id", 0, NULL);
4956 break;
4957 }
4958 }
4959 }
4960 if (business_id > 0) {
4961 yyjson_val *business = find_business_by_id_runtime_local(root_after, business_id);
4962 const char *business_slug_value = business != NULL ? yyjson_get_str(native_json_obj_get(business, "slug")) : NULL;
4963 if (business_slug_value != NULL && business_slug_value[0] != '\0') {
4964 static char continue_buffer[256];
4965 snprintf(continue_buffer, sizeof(continue_buffer), "/b/%s/", business_slug_value);
4966 continue_path = continue_buffer;
4967 }
4968 }
4969 } else if (user_after != NULL) {
4970 continue_path = default_signed_in_path_runtime(user_after);
4971 }
4972 native_json_doc_free(doc_after);
4973 }
4974
4975 if (issue_browser_session_runtime(app, request, next_user_id, &session_token, &session_cookie_header) != 0 || session_cookie_header == NULL) {
4976 free(request_path);
4977 free(invite_token);
4978 free(username);
4979 free(full_name);
4980 free(phone_number);
4981 free(password);
4982 free(password_confirm);
4983 free(token_hash);
4984 free(password_hash);
4985 free(session_token);
4986 free(session_cookie_header);
4988 yyjson_mut_doc_free(mut_doc);
4989 native_send_text_response(connection, 500, "Internal Server Error", "invite session issuance failed\n");
4990 return 500;
4991 }
4992
4993 {
4994 size_t header_needed = strlen(session_cookie_header) + 3;
4995 char *extra_headers = (char *)malloc(header_needed);
4996 char *body_params_json = invite_accept_done_page_params_json_dup_runtime(continue_path);
4997 int status = 500;
4998 if (extra_headers != NULL) {
4999 snprintf(extra_headers, header_needed, "%s\r\n", session_cookie_header);
5000 }
5001 if (body_params_json != NULL && extra_headers != NULL) {
5002 status = render_declared_page_response_with_headers_runtime(connection, app, request, "invite_accept_done", request_path != NULL ? request_path : "/accounts/invite/", body_params_json, extra_headers);
5003 }
5004 free(extra_headers);
5005 free(body_params_json);
5006 free(request_path);
5007 free(invite_token);
5008 free(username);
5009 free(full_name);
5010 free(phone_number);
5011 free(password);
5012 free(password_confirm);
5013 free(token_hash);
5014 free(password_hash);
5015 free(session_token);
5016 free(session_cookie_header);
5018 yyjson_mut_doc_free(mut_doc);
5019 return status;
5020 }
5021}
5022
5023/**
5024 * @brief Serves the Pharos dynamic-app home page or a generic landing page.
5025 *
5026 * This is a placeholder that will be replaced by full template
5027 * rendering in later Phase 3 slices. For now it provides a
5028 * functional HTML page so the app is browsable.
5029 */
5031 NativeConnection *connection,
5032 const char *app_id,
5033 const char *route_id,
5034 const char *csrf_token
5035) {
5036 char html[2048];
5037 int len;
5038
5039 len = snprintf(html, sizeof(html),
5040 "<!doctype html>\n"
5041 "<html lang=\"en\">\n"
5042 "<head>\n"
5043 " <meta charset=\"utf-8\">\n"
5044 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
5045 " <title>%s</title>\n"
5046 "</head>\n"
5047 "<body>\n"
5048 " <main>\n"
5049 " <h1>%s</h1>\n"
5050 " <p>Served by the Pharos native in-process dynamic runtime.</p>\n"
5051 " <p>Route: <code>%s</code></p>\n"
5052 " </main>\n"
5053 "</body>\n"
5054 "</html>\n",
5055 app_id != NULL ? app_id : "Pharos App",
5056 app_id != NULL ? app_id : "Pharos App",
5057 route_id != NULL ? route_id : "/"
5058 );
5059
5060 if (len < 0 || (size_t)len >= sizeof(html)) {
5061 native_send_text_response(connection, 500, "Internal Server Error", "rendering error\n");
5062 return;
5063 }
5064 write_html_response(connection, 200, "OK", html, csrf_token);
5065}
5066
5067/**
5068 * @brief Handles a matched page route using dynamic template rendering.
5069 *
5070 * Primary path: resolve the page spec from page_id, render the body
5071 * template, wrap in the layout template, inject CSRF, and send with
5072 * the CSRF cookie.
5073 *
5074 * Fallback: serve a static file from the artifact directory, or
5075 * generate a generic landing page.
5076 */
5078 NativeConnection *connection,
5079 const AppRuntime *app,
5080 const HttpRequest *request,
5081 const NativeDynamicRouteMatch *match,
5082 const char *request_path,
5083 const char *query_string,
5084 const char *csrf_token
5085) {
5086 char *page_spec_rel = NULL;
5087 char *page_spec_path = NULL;
5088 char *page_spec_text = NULL;
5089 yyjson_doc *page_spec_doc = NULL;
5090 yyjson_val *page_spec_root = NULL;
5091 char *template_file = NULL;
5092 char *title = NULL;
5093 char *layout_id = NULL;
5094 char *body_template_path = NULL;
5095 yyjson_doc *body_params_doc = NULL;
5096 yyjson_val *body_params_root = NULL;
5097 char *body_params_json = NULL;
5098 char *body_html = NULL;
5099 char *full_html = NULL;
5100 char *injected_html = NULL;
5101 int status = 500;
5102
5103 if (app == NULL || connection == NULL || match == NULL) {
5104 if (connection != NULL) {
5105 native_send_text_response(connection, 500, "Internal Server Error", "null page route params\n");
5106 }
5107 return 500;
5108 }
5109
5110 long active_user_id = active_user_id_from_cookie_runtime(app->state_path, request != NULL ? request->cookie : NULL);
5111
5112 if (match->auth_gate != NULL && streq(match->auth_gate, "session_required") && active_user_id <= 0) {
5113 const char *base_path = app->base_path != NULL ? app->base_path : "";
5115 connection,
5116 "HTTP/1.1 302 Found\r\n"
5117 "Location: %s/accounts/login/\r\n"
5118 "Content-Length: 0\r\n"
5119 "Cache-Control: no-store\r\n"
5120 "Connection: close\r\n"
5121 "\r\n",
5122 base_path
5123 );
5124 return 302;
5125 }
5126
5127 if (app->app_id != NULL && streq(app->app_id, "dynamic_app_template") && match->page_id != NULL && streq(match->page_id, "state_api")) {
5128 return write_proof_state_api_response(connection, app->state_path);
5129 }
5130
5131 /* ---- Primary path: dynamic template rendering ---- */
5132 if (app->app_root != NULL && match->page_id != NULL) {
5133 page_spec_rel = declared_page_relative_path_dup_runtime(app->app_root, match->page_id);
5134 if (page_spec_rel != NULL) {
5135 page_spec_path = join_base_relative_path_dup_runtime(app->app_root, page_spec_rel);
5136 if (page_spec_path != NULL && path_exists(page_spec_path)) {
5137 page_spec_text = read_file_text(page_spec_path);
5138 if (page_spec_text != NULL) {
5139 page_spec_root = load_root_from_text_runtime(page_spec_text, &page_spec_doc);
5140 if (page_spec_root != NULL) {
5141 template_file = native_json_obj_get_string_dup(page_spec_root, "template_file");
5142 title = native_json_obj_get_string_dup(page_spec_root, "title");
5143 layout_id = native_json_obj_get_string_dup(page_spec_root, "layout_id");
5144 }
5145 }
5146 }
5147 }
5148
5149 if (template_file != NULL && layout_id != NULL) {
5150 body_template_path = join_base_relative_path_dup_runtime(app->app_root, template_file);
5151 if (body_template_path != NULL && path_exists(body_template_path)) {
5153 app,
5154 request,
5155 match,
5156 request_path,
5157 query_string,
5158 active_user_id
5159 );
5160 body_params_root = load_root_from_text_runtime(
5161 body_params_json != NULL ? body_params_json : "{\"error_html\":\"\",\"next_input\":\"\"}",
5162 &body_params_doc);
5163 if (body_params_root != NULL) {
5164 free(title);
5165 title = page_title_dup_runtime(page_spec_root, body_params_root);
5166 body_html = render_file_template_dup_runtime(body_template_path, body_params_root);
5167 }
5168 }
5169 }
5170
5171 if (body_html != NULL) {
5172 full_html = render_page_html_dup_runtime(
5173 app->app_root,
5174 app->base_path,
5175 layout_id,
5176 title,
5177 body_html,
5178 "en",
5179 request != NULL ? request->target : "/",
5180 active_user_id > 0 ? 1 : 0,
5181 0
5182 );
5183
5184 if (full_html != NULL) {
5185 injected_html = csrf_inject_hidden_inputs(full_html, csrf_token);
5186 write_html_response(connection, 200, "OK",
5187 injected_html != NULL ? injected_html : full_html,
5188 csrf_token);
5189 status = 200;
5190 }
5191 }
5192 }
5193
5194 /* Cleanup */
5195 native_json_doc_free(body_params_doc);
5196 native_json_doc_free(page_spec_doc);
5197 free(body_params_json);
5198 free(body_html);
5199 free(full_html);
5200 free(injected_html);
5201 free(template_file);
5202 free(title);
5203 free(layout_id);
5204 free(body_template_path);
5205 free(page_spec_text);
5206 free(page_spec_path);
5207 free(page_spec_rel);
5208
5209 /* If dynamic rendering succeeded, we are done */
5210 if (status == 200) {
5211 return status;
5212 }
5213
5214 /* ---- Fallback: static file serving ---- */
5215 if (app->artifact_dir != NULL && match->page_id != NULL) {
5216 char *page_file = path_join(app->artifact_dir, match->page_id);
5217 if (page_file != NULL) {
5218 char *html_file = NULL;
5219 size_t page_file_len = strlen(page_file);
5220
5221 if (page_file_len > 5 && strcmp(page_file + page_file_len - 5, ".html") == 0) {
5222 html_file = strdup(page_file);
5223 } else {
5224 html_file = (char *)malloc(page_file_len + 6);
5225 if (html_file != NULL) {
5226 snprintf(html_file, page_file_len + 6, "%s.html", page_file);
5227 }
5228 }
5229
5230 if (html_file != NULL && path_exists(html_file)) {
5231 status = native_dynamic_media_serve_asset(connection, app->artifact_dir, match->page_id, request);
5232 free(html_file);
5233 free(page_file);
5234 return status;
5235 }
5236 free(html_file);
5237 free(page_file);
5238 }
5239 }
5240
5241 /* ---- Final fallback: generic landing page ---- */
5242 serve_dynamic_home_page(connection, app->app_id, match->route_id, csrf_token);
5243 return 200;
5244}
5245
5246/**
5247 * @brief Handles a matched action route.
5248 *
5249 * For now, POST actions return a JSON acknowledgement. Full action
5250 * processing will be added in later slices.
5251 */
5253 NativeConnection *connection,
5254 const AppRuntime *app,
5255 const HttpRequest *request,
5256 const NativeDynamicRouteMatch *match
5257) {
5258 if (connection == NULL || app == NULL || request == NULL || match == NULL) {
5259 if (connection != NULL) {
5260 native_send_text_response(connection, 500, "Internal Server Error", "null action route params\n");
5261 }
5262 return 500;
5263 }
5264
5265 if (!streq(request->method != NULL ? request->method : "", "POST")) {
5266 write_json_error(connection, 405, "Method Not Allowed", "action_requires_post", "Action routes require POST.");
5267 return 405;
5268 }
5269
5270 {
5271 char *csrf_form_body = request_body_text_dup_runtime(request);
5272 int csrf_valid = csrf_request_valid_runtime(request, csrf_form_body != NULL ? csrf_form_body : "");
5273 free(csrf_form_body);
5274 if (!csrf_valid) {
5275 native_send_text_response(connection, 403, "Forbidden", "Security verification failed\n");
5276 return 403;
5277 }
5278 }
5279
5280
5281 if (app->app_id != NULL && streq(app->app_id, "dynamic_app_template") && match->action_id != NULL && streq(match->action_id, "create_task")) {
5282 char *form_body = request_body_text_dup_runtime(request);
5283 char *title = native_dynamic_form_value(form_body != NULL ? form_body : "", "title");
5284 yyjson_doc *state_doc = NULL;
5285 yyjson_val *state_root = proof_state_root_load_runtime(app->state_path, &state_doc);
5286 yyjson_val *items = state_root != NULL ? native_json_obj_get(state_root, "items") : NULL;
5287 yyjson_mut_doc *mut_doc = NULL;
5288 yyjson_mut_val *mut_root = NULL;
5289 yyjson_mut_val *mut_items = NULL;
5290 yyjson_arr_iter iter;
5291 yyjson_val *item = NULL;
5292 long next_id = state_root != NULL ? native_json_obj_get_long_default(state_root, "next_id", 1, NULL) : 1;
5293 int status = 500;
5294
5295 if (title == NULL || title[0] == '\0') {
5296 free(form_body);
5297 free(title);
5298 native_json_doc_free(state_doc);
5299 native_send_text_response(connection, 422, "Unprocessable Entity", "missing title\n");
5300 return 422;
5301 }
5302 if (state_root == NULL || items == NULL || !yyjson_is_arr(items)) {
5303 free(form_body);
5304 free(title);
5305 native_json_doc_free(state_doc);
5306 native_send_text_response(connection, 500, "Internal Server Error", "proof state unavailable\n");
5307 return 500;
5308 }
5309 mut_doc = yyjson_mut_doc_new(NULL);
5310 mut_root = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
5311 mut_items = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
5312 if (mut_doc == NULL || mut_root == NULL || mut_items == NULL
5313 || !yyjson_mut_obj_add_strcpy(mut_doc, mut_root, "schema", "pharos.dynamic_app_state.v1")
5314 || !yyjson_mut_obj_add_int(mut_doc, mut_root, "next_id", next_id + 1)
5315 || !yyjson_mut_obj_add_strcpy(mut_doc, mut_root, "last_action", "task_created")
5316 || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "items"), mut_items)) {
5317 free(form_body);
5318 free(title);
5319 native_json_doc_free(state_doc);
5320 yyjson_mut_doc_free(mut_doc);
5321 native_send_text_response(connection, 500, "Internal Server Error", "task create failed\n");
5322 return 500;
5323 }
5324 yyjson_mut_doc_set_root(mut_doc, mut_root);
5325 if (yyjson_arr_iter_init(items, &iter)) {
5326 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
5327 yyjson_mut_val *copied = yyjson_val_mut_copy(mut_doc, item);
5328 if (copied == NULL || !yyjson_mut_arr_append(mut_items, copied)) {
5329 free(form_body);
5330 free(title);
5331 native_json_doc_free(state_doc);
5332 yyjson_mut_doc_free(mut_doc);
5333 native_send_text_response(connection, 500, "Internal Server Error", "task copy failed\n");
5334 return 500;
5335 }
5336 }
5337 }
5338 {
5339 yyjson_mut_val *task = yyjson_mut_obj(mut_doc);
5340 if (task == NULL
5341 || !yyjson_mut_obj_add_int(mut_doc, task, "id", next_id)
5342 || !yyjson_mut_obj_add_bool(mut_doc, task, "done", 0)
5343 || !yyjson_mut_obj_add_strcpy(mut_doc, task, "title", title)
5344 || !yyjson_mut_arr_append(mut_items, task)) {
5345 free(form_body);
5346 free(title);
5347 native_json_doc_free(state_doc);
5348 yyjson_mut_doc_free(mut_doc);
5349 native_send_text_response(connection, 500, "Internal Server Error", "task append failed\n");
5350 return 500;
5351 }
5352 }
5353 {
5354 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
5355 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
5356 free(text);
5357 free(form_body);
5358 free(title);
5359 native_json_doc_free(state_doc);
5360 yyjson_mut_doc_free(mut_doc);
5361 native_send_text_response(connection, 500, "Internal Server Error", "task persist failed\n");
5362 return 500;
5363 }
5364 free(text);
5365 }
5366 free(form_body);
5367 free(title);
5368 native_json_doc_free(state_doc);
5369 yyjson_mut_doc_free(mut_doc);
5371 connection,
5372 "HTTP/1.1 303 See Other\r\n"
5373 "Location: %s/\r\n"
5374 "Cache-Control: no-store\r\n"
5375 "Content-Length: 0\r\n"
5376 "Connection: close\r\n"
5377 "\r\n",
5378 app->base_path != NULL ? app->base_path : ""
5379 );
5380 return 303;
5381 }
5382
5383 if (app->app_id != NULL && streq(app->app_id, "dynamic_app_template") && match->action_id != NULL && streq(match->action_id, "toggle_task")) {
5384 char *form_body = request_body_text_dup_runtime(request);
5385 char *task_id_text = native_dynamic_form_value(form_body != NULL ? form_body : "", "id");
5386 long task_id = task_id_text != NULL ? strtol(task_id_text, NULL, 10) : 0;
5387 yyjson_doc *state_doc = NULL;
5388 yyjson_val *state_root = proof_state_root_load_runtime(app->state_path, &state_doc);
5389 yyjson_val *items = state_root != NULL ? native_json_obj_get(state_root, "items") : NULL;
5390 yyjson_mut_doc *mut_doc = NULL;
5391 yyjson_mut_val *mut_root = NULL;
5392 yyjson_mut_val *mut_items = NULL;
5393 yyjson_arr_iter iter;
5394 yyjson_val *item = NULL;
5395 long next_id = state_root != NULL ? native_json_obj_get_long_default(state_root, "next_id", 1, NULL) : 1;
5396 int found = 0;
5397
5398 if (task_id <= 0) {
5399 free(form_body);
5400 free(task_id_text);
5401 native_json_doc_free(state_doc);
5402 native_send_text_response(connection, 422, "Unprocessable Entity", "missing id\n");
5403 return 422;
5404 }
5405 if (state_root == NULL || items == NULL || !yyjson_is_arr(items)) {
5406 free(form_body);
5407 free(task_id_text);
5408 native_json_doc_free(state_doc);
5409 native_send_text_response(connection, 500, "Internal Server Error", "proof state unavailable\n");
5410 return 500;
5411 }
5412 mut_doc = yyjson_mut_doc_new(NULL);
5413 mut_root = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
5414 mut_items = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
5415 if (mut_doc == NULL || mut_root == NULL || mut_items == NULL
5416 || !yyjson_mut_obj_add_strcpy(mut_doc, mut_root, "schema", "pharos.dynamic_app_state.v1")
5417 || !yyjson_mut_obj_add_int(mut_doc, mut_root, "next_id", next_id)
5418 || !yyjson_mut_obj_add_strcpy(mut_doc, mut_root, "last_action", "task_toggled")
5419 || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "items"), mut_items)) {
5420 free(form_body);
5421 free(task_id_text);
5422 native_json_doc_free(state_doc);
5423 yyjson_mut_doc_free(mut_doc);
5424 native_send_text_response(connection, 500, "Internal Server Error", "task toggle failed\n");
5425 return 500;
5426 }
5427 yyjson_mut_doc_set_root(mut_doc, mut_root);
5428 if (yyjson_arr_iter_init(items, &iter)) {
5429 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
5430 long row_id = native_json_obj_get_long_default(item, "id", 0, NULL);
5431 int done = yyjson_get_bool(native_json_obj_get(item, "done"));
5432 char *title = native_json_obj_get_string_dup(item, "title");
5433 yyjson_mut_val *task = yyjson_mut_obj(mut_doc);
5434 int append_ok = 0;
5435 if (task != NULL
5436 && yyjson_mut_obj_add_int(mut_doc, task, "id", row_id)
5437 && yyjson_mut_obj_add_bool(mut_doc, task, "done", row_id == task_id ? !done : done)
5438 && yyjson_mut_obj_add_strcpy(mut_doc, task, "title", title != NULL ? title : "")) {
5439 append_ok = yyjson_mut_arr_append(mut_items, task);
5440 }
5441 if (row_id == task_id) {
5442 found = 1;
5443 }
5444 free(title);
5445 if (!append_ok) {
5446 free(form_body);
5447 free(task_id_text);
5448 native_json_doc_free(state_doc);
5449 yyjson_mut_doc_free(mut_doc);
5450 native_send_text_response(connection, 500, "Internal Server Error", "task rebuild failed\n");
5451 return 500;
5452 }
5453 }
5454 }
5455 if (!found) {
5456 free(form_body);
5457 free(task_id_text);
5458 native_json_doc_free(state_doc);
5459 yyjson_mut_doc_free(mut_doc);
5460 write_json_error(connection, 404, "Not Found", "task_not_found", "Task not found.");
5461 return 404;
5462 }
5463 {
5464 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
5465 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
5466 free(text);
5467 free(form_body);
5468 free(task_id_text);
5469 native_json_doc_free(state_doc);
5470 yyjson_mut_doc_free(mut_doc);
5471 native_send_text_response(connection, 500, "Internal Server Error", "task persist failed\n");
5472 return 500;
5473 }
5474 free(text);
5475 }
5476 free(form_body);
5477 free(task_id_text);
5478 native_json_doc_free(state_doc);
5479 yyjson_mut_doc_free(mut_doc);
5481 connection,
5482 "HTTP/1.1 303 See Other\r\n"
5483 "Location: %s/\r\n"
5484 "Cache-Control: no-store\r\n"
5485 "Content-Length: 0\r\n"
5486 "Connection: close\r\n"
5487 "\r\n",
5488 app->base_path != NULL ? app->base_path : ""
5489 );
5490 return 303;
5491 }
5492
5493 if (match->action_id != NULL && streq(match->action_id, "password_reset_request")) {
5494 return handle_password_reset_request_action_runtime(connection, app, request);
5495 }
5496
5497 if (match->action_id != NULL && streq(match->action_id, "password_reset_complete")) {
5498 return handle_password_reset_complete_action_runtime(connection, app, request, match);
5499 }
5500
5501 if (match->action_id != NULL && streq(match->action_id, "issue_business_invite")) {
5502 return handle_issue_business_invite_action_runtime(connection, app, request, match);
5503 }
5504
5505 if (match->action_id != NULL && streq(match->action_id, "accept_invite")) {
5506 return handle_accept_invite_action_runtime(connection, app, request, match);
5507 }
5508
5509 if (match->action_id != NULL && streq(match->action_id, "login")) {
5510 char *form_body = request_body_text_dup_runtime(request);
5511 char *username = native_dynamic_form_value(form_body != NULL ? form_body : "", "username");
5512 char *password = native_dynamic_form_value(form_body != NULL ? form_body : "", "password");
5513 char *next_path = native_dynamic_form_value(form_body != NULL ? form_body : "", "next");
5514 yyjson_doc *doc = NULL;
5515 yyjson_val *root = NULL;
5516 yyjson_val *user = NULL;
5517 char *stored_hash = NULL;
5518 char *session_cookie_header = NULL;
5519 char *session_token = NULL;
5520 int status = 422;
5521 const char *target_path = "/explore/";
5522
5523 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
5524 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
5525 user = root != NULL ? find_user_by_username_runtime_local(root, username != NULL ? username : "") : NULL;
5526 stored_hash = user != NULL ? native_json_obj_get_string_dup(user, "password_hash") : NULL;
5527
5528 if (stored_hash != NULL && verify_password_hash_runtime(stored_hash, password != NULL ? password : "", username != NULL ? username : "")) {
5529 long user_id = native_json_obj_get_long_default(user, "id", 0, NULL);
5530 (void)revoke_session_by_cookie_runtime(app, request->cookie);
5531 if (issue_browser_session_runtime(app, request, user_id, &session_token, &session_cookie_header) == 0 && session_cookie_header != NULL) {
5532 (void)append_rate_limit_event_runtime_local(app, "auth.login", username != NULL ? username : "", "allowed");
5533 target_path = (next_path != NULL && next_path[0] != '\0') ? next_path : default_signed_in_path_runtime(user);
5535 connection,
5536 "HTTP/1.1 302 Found\r\n"
5537 "Location: %s%s\r\n"
5538 "%s\r\n"
5539 "Cache-Control: no-store\r\n"
5540 "Content-Length: 0\r\n"
5541 "Connection: close\r\n"
5542 "\r\n",
5543 app->base_path != NULL ? app->base_path : "",
5544 target_path,
5545 session_cookie_header
5546 );
5547 status = 302;
5548 } else {
5549 native_send_text_response(connection, 500, "Internal Server Error", "session issuance failed\n");
5550 status = 500;
5551 }
5552 } else {
5553 char *error_params_json = NULL;
5554 (void)append_rate_limit_event_runtime_local(app, "auth.login", username != NULL ? username : "", "denied");
5555 error_params_json = login_page_params_json_dup_runtime("", "Invalid username or password.");
5556 if (error_params_json != NULL) {
5557 status = render_declared_page_response_runtime(connection, app, request, "login", "/accounts/login/", error_params_json);
5558 free(error_params_json);
5559 } else {
5560 native_send_text_response(connection, 422, "Unprocessable Entity", "Invalid username or password\n");
5561 }
5562 }
5563
5564 free(form_body);
5565 free(username);
5566 free(password);
5567 free(next_path);
5568 free(stored_hash);
5569 free(session_cookie_header);
5570 free(session_token);
5572 return status;
5573 }
5574
5575 if (match->action_id != NULL && streq(match->action_id, "update_profile")) {
5576 long active_user_id = active_user_id_from_cookie_runtime(app->state_path, request->cookie);
5577 char *form_body = request_body_text_dup_runtime(request);
5578 char *full_name = native_dynamic_form_value(form_body != NULL ? form_body : "", "full_name");
5579 char *phone_number = native_dynamic_form_value(form_body != NULL ? form_body : "", "phone_number");
5580 char *address = native_dynamic_form_value(form_body != NULL ? form_body : "", "address");
5581 char *payment_preference = native_dynamic_form_value(form_body != NULL ? form_body : "", "payment_preference");
5582 yyjson_doc *doc = NULL;
5583 yyjson_mut_doc *mut_doc = NULL;
5584 yyjson_mut_val *mut_root = NULL;
5585 yyjson_mut_val *users = NULL;
5586 yyjson_mut_val *user = NULL;
5587 size_t idx = 0;
5588 size_t max = 0;
5589 int updated = 0;
5590
5591 if (active_user_id <= 0) {
5592 free(form_body);
5593 free(full_name);
5594 free(phone_number);
5595 free(address);
5596 free(payment_preference);
5597 write_json_error(connection, 403, "Forbidden", "session_required", "Profile updates require an active session.");
5598 return 403;
5599 }
5600
5601 if (payment_preference != NULL && payment_preference[0] != '\0'
5602 && !streq(payment_preference, "pay_on_site")
5603 && !streq(payment_preference, "credit_card")) {
5604 free(form_body);
5605 free(full_name);
5606 free(phone_number);
5607 free(address);
5608 free(payment_preference);
5609 write_json_error(connection, 422, "Unprocessable Entity", "invalid_payment_preference", "Unsupported payment preference.");
5610 return 422;
5611 }
5612
5613 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
5614 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
5615 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
5616 users = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "users") : NULL;
5617 if (mut_doc == NULL || mut_root == NULL || users == NULL || !yyjson_mut_is_arr(users)) {
5618 free(form_body);
5619 free(full_name);
5620 free(phone_number);
5621 free(address);
5622 free(payment_preference);
5624 yyjson_mut_doc_free(mut_doc);
5625 native_send_text_response(connection, 500, "Internal Server Error", "profile state unavailable\n");
5626 return 500;
5627 }
5628
5629 yyjson_mut_arr_foreach(users, idx, max, user) {
5630 if (!yyjson_mut_is_obj(user)) continue;
5631 if (native_json_obj_get_long_default((yyjson_val *)user, "id", 0, NULL) != active_user_id) continue;
5632 if (!yyjson_mut_obj_put(user, yyjson_mut_strcpy(mut_doc, "full_name"), yyjson_mut_strcpy(mut_doc, full_name != NULL ? full_name : ""))
5633 || !yyjson_mut_obj_put(user, yyjson_mut_strcpy(mut_doc, "phone_number"), yyjson_mut_strcpy(mut_doc, phone_number != NULL ? phone_number : ""))
5634 || !yyjson_mut_obj_put(user, yyjson_mut_strcpy(mut_doc, "address"), yyjson_mut_strcpy(mut_doc, address != NULL ? address : ""))
5635 || !yyjson_mut_obj_put(user, yyjson_mut_strcpy(mut_doc, "payment_preference"), yyjson_mut_strcpy(mut_doc, payment_preference != NULL && payment_preference[0] != '\0' ? payment_preference : "pay_on_site"))) {
5636 free(form_body);
5637 free(full_name);
5638 free(phone_number);
5639 free(address);
5640 free(payment_preference);
5642 yyjson_mut_doc_free(mut_doc);
5643 native_send_text_response(connection, 500, "Internal Server Error", "profile update failed\n");
5644 return 500;
5645 }
5646 updated = 1;
5647 break;
5648 }
5649
5650 if (!updated) {
5651 free(form_body);
5652 free(full_name);
5653 free(phone_number);
5654 free(address);
5655 free(payment_preference);
5657 yyjson_mut_doc_free(mut_doc);
5658 write_json_error(connection, 404, "Not Found", "user_not_found", "Profile user not found.");
5659 return 404;
5660 }
5661
5662 {
5663 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
5664 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
5665 free(text);
5666 free(form_body);
5667 free(full_name);
5668 free(phone_number);
5669 free(address);
5670 free(payment_preference);
5672 yyjson_mut_doc_free(mut_doc);
5673 native_send_text_response(connection, 500, "Internal Server Error", "profile persist failed\n");
5674 return 500;
5675 }
5676 free(text);
5677 }
5678
5679 free(form_body);
5680 free(full_name);
5681 free(phone_number);
5682 free(address);
5683 free(payment_preference);
5685 yyjson_mut_doc_free(mut_doc);
5687 connection,
5688 "HTTP/1.1 302 Found\r\n"
5689 "Location: %s/profile/\r\n"
5690 "Cache-Control: no-store\r\n"
5691 "Content-Length: 0\r\n"
5692 "Connection: close\r\n"
5693 "\r\n",
5694 app->base_path != NULL ? app->base_path : ""
5695 );
5696 return 302;
5697 }
5698
5699 if (match->action_id != NULL && streq(match->action_id, "create_appointment")) {
5700 long active_user_id = active_user_id_from_cookie_runtime(app->state_path, request->cookie);
5701 char *request_path = request_path_dup_runtime(request);
5702 char *business_slug = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug") : NULL;
5703 char *service_id_text = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "service_id") : NULL;
5704 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
5705 char *form_body = request_body_text_dup_runtime(request);
5706 char *date_value = native_dynamic_form_value(form_body != NULL ? form_body : "", "date");
5707 char *time_hour = native_dynamic_form_value(form_body != NULL ? form_body : "", "time_hour");
5708 char *time_minute = native_dynamic_form_value(form_body != NULL ? form_body : "", "time_minute");
5709 char *time_period = native_dynamic_form_value(form_body != NULL ? form_body : "", "time_period");
5710 char *notes = native_dynamic_form_value(form_body != NULL ? form_body : "", "notes");
5711 yyjson_doc *doc = NULL;
5712 yyjson_mut_doc *mut_doc = NULL;
5713 yyjson_val *root = NULL;
5714 yyjson_mut_val *mut_root = NULL;
5715 yyjson_val *business = NULL;
5716 yyjson_val *service = NULL;
5717 yyjson_mut_val *appointments = NULL;
5718 yyjson_mut_val *appointment_obj = NULL;
5719 char time_value[6];
5720 int hour_value = 0;
5721 int minute_value = 0;
5722 int duration_minutes = 45;
5723 long business_id = 0;
5724 long technician_id = 0;
5725 long appointment_id = 0;
5726 int normalized_hour = 0;
5727
5728 if (active_user_id <= 0) {
5729 free(request_path);
5730 free(business_slug);
5731 free(service_id_text);
5732 free(form_body);
5733 free(date_value);
5734 free(time_hour);
5735 free(time_minute);
5736 free(time_period);
5737 free(notes);
5738 write_json_error(connection, 403, "Forbidden", "session_required", "Appointments require an active session.");
5739 return 403;
5740 }
5741 if (business_slug == NULL || business_slug[0] == '\0' || service_id <= 0 || date_value == NULL || date_value[0] == '\0'
5742 || time_hour == NULL || time_hour[0] == '\0' || time_minute == NULL || time_minute[0] == '\0') {
5743 free(request_path);
5744 free(business_slug);
5745 free(service_id_text);
5746 free(form_body);
5747 free(date_value);
5748 free(time_hour);
5749 free(time_minute);
5750 free(time_period);
5751 free(notes);
5752 write_json_error(connection, 422, "Unprocessable Entity", "invalid_appointment_request", "Missing appointment booking fields.");
5753 return 422;
5754 }
5755
5756 hour_value = atoi(time_hour);
5757 minute_value = atoi(time_minute);
5758 if (minute_value < 0 || minute_value > 59) {
5759 minute_value = -1;
5760 }
5761 if (time_period != NULL && time_period[0] != '\0') {
5762 if (hour_value < 1 || hour_value > 12 || minute_value < 0) {
5763 free(request_path);
5764 free(business_slug);
5765 free(service_id_text);
5766 free(form_body);
5767 free(date_value);
5768 free(time_hour);
5769 free(time_minute);
5770 free(time_period);
5771 free(notes);
5772 write_json_error(connection, 422, "Unprocessable Entity", "invalid_appointment_time", "Invalid appointment time.");
5773 return 422;
5774 }
5775 normalized_hour = hour_value % 12;
5776 if (streq(time_period, "PM") || streq(time_period, "pm")) {
5777 normalized_hour += 12;
5778 }
5779 } else {
5780 if (hour_value < 0 || hour_value > 23 || minute_value < 0) {
5781 free(request_path);
5782 free(business_slug);
5783 free(service_id_text);
5784 free(form_body);
5785 free(date_value);
5786 free(time_hour);
5787 free(time_minute);
5788 free(time_period);
5789 free(notes);
5790 write_json_error(connection, 422, "Unprocessable Entity", "invalid_appointment_time", "Invalid appointment time.");
5791 return 422;
5792 }
5793 normalized_hour = hour_value;
5794 }
5795 snprintf(time_value, sizeof(time_value), "%02d:%02d", normalized_hour, minute_value);
5796
5797 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
5798 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
5799 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
5800 business_id = business != NULL ? native_json_obj_get_long_default(business, "id", 0, NULL) : 0;
5801 service = root != NULL ? find_service_for_business_runtime_local(root, service_id, business_id) : NULL;
5802 if (business == NULL || service == NULL) {
5803 free(request_path);
5804 free(business_slug);
5805 free(service_id_text);
5806 free(form_body);
5807 free(date_value);
5808 free(time_hour);
5809 free(time_minute);
5810 free(time_period);
5811 free(notes);
5813 write_json_error(connection, 404, "Not Found", "not_found", "Business or service not found.");
5814 return 404;
5815 }
5816
5817 duration_minutes = (int)native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
5818 if (duration_minutes <= 0) {
5819 duration_minutes = 45;
5820 }
5821 if (!business_open_for_slot_runtime_local(root, business_id, date_value, time_value, duration_minutes)) {
5822 free(request_path);
5823 free(business_slug);
5824 free(service_id_text);
5825 free(form_body);
5826 free(date_value);
5827 free(time_hour);
5828 free(time_minute);
5829 free(time_period);
5830 free(notes);
5832 native_send_text_response(connection, 422, "Unprocessable Entity", "Requested time is outside business availability.\n");
5833 return 422;
5834 }
5835
5836 technician_id = assign_technician_for_slot_runtime_local(root, business_id, service_id, date_value, time_value);
5837 if (technician_id <= 0) {
5838 free(request_path);
5839 free(business_slug);
5840 free(service_id_text);
5841 free(form_body);
5842 free(date_value);
5843 free(time_hour);
5844 free(time_minute);
5845 free(time_period);
5846 free(notes);
5848 native_send_text_response(connection, 422, "Unprocessable Entity", "No technician is available for that slot.\n");
5849 return 422;
5850 }
5851
5852 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
5853 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
5854 appointments = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "appointments") : NULL;
5855 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
5856 free(request_path);
5857 free(business_slug);
5858 free(service_id_text);
5859 free(form_body);
5860 free(date_value);
5861 free(time_hour);
5862 free(time_minute);
5863 free(time_period);
5864 free(notes);
5866 yyjson_mut_doc_free(mut_doc);
5867 native_send_text_response(connection, 500, "Internal Server Error", "appointment state unavailable\n");
5868 return 500;
5869 }
5870 if (appointments == NULL || !yyjson_mut_is_arr(appointments)) {
5871 appointments = yyjson_mut_arr(mut_doc);
5872 if (appointments == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "appointments"), appointments)) {
5873 free(request_path);
5874 free(business_slug);
5875 free(service_id_text);
5876 free(form_body);
5877 free(date_value);
5878 free(time_hour);
5879 free(time_minute);
5880 free(time_period);
5881 free(notes);
5883 yyjson_mut_doc_free(mut_doc);
5884 native_send_text_response(connection, 500, "Internal Server Error", "appointment array unavailable\n");
5885 return 500;
5886 }
5887 }
5888
5889 appointment_id = next_array_id_runtime_local(state_array_runtime_local(root, "appointments"));
5890 appointment_obj = yyjson_mut_obj(mut_doc);
5891 if (appointment_obj == NULL
5892 || !yyjson_mut_obj_add_int(mut_doc, appointment_obj, "id", appointment_id)
5893 || !yyjson_mut_obj_add_int(mut_doc, appointment_obj, "user_id", active_user_id)
5894 || !yyjson_mut_obj_add_int(mut_doc, appointment_obj, "business_id", business_id)
5895 || !yyjson_mut_obj_add_int(mut_doc, appointment_obj, "service_id", service_id)
5896 || !yyjson_mut_obj_add_int(mut_doc, appointment_obj, "technician_id", technician_id)
5897 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "date", date_value)
5898 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "time", time_value)
5899 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "status", "confirmed")
5900 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "notes", notes != NULL ? notes : "")
5901 || !yyjson_mut_arr_append(appointments, appointment_obj)) {
5902 free(request_path);
5903 free(business_slug);
5904 free(service_id_text);
5905 free(form_body);
5906 free(date_value);
5907 free(time_hour);
5908 free(time_minute);
5909 free(time_period);
5910 free(notes);
5912 yyjson_mut_doc_free(mut_doc);
5913 native_send_text_response(connection, 500, "Internal Server Error", "appointment create failed\n");
5914 return 500;
5915 }
5916
5917 {
5918 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
5919 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
5920 free(text);
5921 free(request_path);
5922 free(business_slug);
5923 free(service_id_text);
5924 free(form_body);
5925 free(date_value);
5926 free(time_hour);
5927 free(time_minute);
5928 free(time_period);
5929 free(notes);
5931 yyjson_mut_doc_free(mut_doc);
5932 native_send_text_response(connection, 500, "Internal Server Error", "appointment persist failed\n");
5933 return 500;
5934 }
5935 free(text);
5936 }
5937
5938 free(request_path);
5939 free(business_slug);
5940 free(service_id_text);
5941 free(form_body);
5942 free(date_value);
5943 free(time_hour);
5944 free(time_minute);
5945 free(time_period);
5946 free(notes);
5948 yyjson_mut_doc_free(mut_doc);
5950 connection,
5951 "HTTP/1.1 302 Found\r\n"
5952 "Location: %s/appointments/\r\n"
5953 "Cache-Control: no-store\r\n"
5954 "Content-Length: 0\r\n"
5955 "Connection: close\r\n"
5956 "\r\n",
5957 app->base_path != NULL ? app->base_path : ""
5958 );
5959 return 302;
5960 }
5961
5962 if (match->action_id != NULL && streq(match->action_id, "delete_appointment")) {
5963 long active_user_id = active_user_id_from_cookie_runtime(app->state_path, request->cookie);
5964 char *request_path = request_path_dup_runtime(request);
5965 char *appointment_id_text = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "appointment_id") : NULL;
5966 long appointment_id = appointment_id_text != NULL ? strtol(appointment_id_text, NULL, 10) : 0;
5967 yyjson_doc *doc = NULL;
5968 yyjson_val *root = NULL;
5969 yyjson_val *source_appointments = NULL;
5970 yyjson_arr_iter source_iter;
5971 yyjson_val *source_row = NULL;
5972 yyjson_mut_doc *mut_doc = NULL;
5973 yyjson_mut_val *mut_root = NULL;
5974 yyjson_mut_val *appointments = NULL;
5975 size_t idx = 0;
5976 size_t max = 0;
5977 size_t remove_index = 0;
5978 int removed = 0;
5979
5980 if (active_user_id <= 0) {
5981 free(request_path);
5982 free(appointment_id_text);
5983 write_json_error(connection, 403, "Forbidden", "session_required", "Appointment deletion requires an active session.");
5984 return 403;
5985 }
5986 if (appointment_id <= 0) {
5987 free(request_path);
5988 free(appointment_id_text);
5989 write_json_error(connection, 422, "Unprocessable Entity", "invalid_appointment_id", "Invalid appointment id.");
5990 return 422;
5991 }
5992
5993 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
5994 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
5995 source_appointments = root != NULL ? native_json_obj_get(root, "appointments") : NULL;
5996 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
5997 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
5998 appointments = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "appointments") : NULL;
5999 if (mut_doc == NULL || mut_root == NULL || source_appointments == NULL || !yyjson_is_arr(source_appointments)
6000 || appointments == NULL || !yyjson_mut_is_arr(appointments)) {
6001 free(request_path);
6002 free(appointment_id_text);
6004 yyjson_mut_doc_free(mut_doc);
6005 native_send_text_response(connection, 500, "Internal Server Error", "appointment state unavailable\n");
6006 return 500;
6007 }
6008
6009 if (yyjson_arr_iter_init(source_appointments, &source_iter)) {
6010 while ((source_row = yyjson_arr_iter_next(&source_iter)) != NULL) {
6011 if (yyjson_is_obj(source_row)
6012 && native_json_obj_get_long_default(source_row, "id", 0, NULL) == appointment_id
6013 && native_json_obj_get_long_default(source_row, "user_id", 0, NULL) == active_user_id) {
6014 removed = 1;
6015 break;
6016 }
6017 remove_index++;
6018 }
6019 }
6020
6021 if (removed) {
6022 yyjson_mut_arr_remove(appointments, remove_index);
6023 }
6024
6025 if (!removed) {
6026 free(request_path);
6027 free(appointment_id_text);
6029 yyjson_mut_doc_free(mut_doc);
6030 write_json_error(connection, 404, "Not Found", "appointment_not_found", "Appointment not found.");
6031 return 404;
6032 }
6033
6034 {
6035 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
6036 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
6037 free(text);
6038 free(request_path);
6039 free(appointment_id_text);
6041 yyjson_mut_doc_free(mut_doc);
6042 native_send_text_response(connection, 500, "Internal Server Error", "appointment delete persist failed\n");
6043 return 500;
6044 }
6045 free(text);
6046 }
6047
6048 free(request_path);
6049 free(appointment_id_text);
6051 yyjson_mut_doc_free(mut_doc);
6053 connection,
6054 "HTTP/1.1 302 Found\r\n"
6055 "Location: %s/appointments/\r\n"
6056 "Cache-Control: no-store\r\n"
6057 "Content-Length: 0\r\n"
6058 "Connection: close\r\n"
6059 "\r\n",
6060 app->base_path != NULL ? app->base_path : ""
6061 );
6062 return 302;
6063 }
6064
6065 if (match->action_id != NULL && streq(match->action_id, "update_service")) {
6066 char *request_path = request_path_dup_runtime(request);
6067 char *business_slug = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug") : NULL;
6068 char *service_id_text = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "service_id") : NULL;
6069 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
6070 char *name = request_form_value_dup_runtime(request, "name");
6071 char *description = request_form_value_dup_runtime(request, "description");
6072 char *cost = request_form_value_dup_runtime(request, "cost");
6073 char *duration_text = request_form_value_dup_runtime(request, "duration_minutes");
6074 char *sort_order_text = request_form_value_dup_runtime(request, "sort_order");
6075 char *booking_option_count_text = request_form_value_dup_runtime(request, "booking_option_count");
6076 char *remove_image = request_form_value_dup_runtime(request, "remove_image");
6077 yyjson_doc *doc = NULL;
6078 yyjson_mut_doc *mut_doc = NULL;
6079 yyjson_val *root = NULL;
6080 yyjson_mut_val *mut_root = NULL;
6081 yyjson_mut_val *services = NULL;
6082 yyjson_mut_val *service_row = NULL;
6083 yyjson_val *business = NULL;
6084 yyjson_val *source_services = NULL;
6085 yyjson_val *source_technicians = NULL;
6086 long business_id = 0;
6087 long duration_minutes = duration_text != NULL ? strtol(duration_text, NULL, 10) : 0;
6088 long sort_order = sort_order_text != NULL ? strtol(sort_order_text, NULL, 10) : 0;
6089 long booking_option_count = booking_option_count_text != NULL ? strtol(booking_option_count_text, NULL, 10) : 0;
6090
6091 if (business_slug == NULL || business_slug[0] == '\0' || service_id <= 0 || name == NULL || name[0] == '\0' || cost == NULL || cost[0] == '\0') {
6092 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6093 write_json_error(connection, 422, "Unprocessable Entity", "invalid_service_update", "Missing service update fields.");
6094 return 422;
6095 }
6096 if (duration_minutes <= 0) duration_minutes = 45;
6097 if (sort_order <= 0) sort_order = service_id;
6098 if (booking_option_count < 0) booking_option_count = 0;
6099
6100 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
6101 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
6102 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
6103 business_id = business != NULL ? native_json_obj_get_long_default(business, "id", 0, NULL) : 0;
6104 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
6105 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
6106 services = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "services") : NULL;
6107 source_services = root != NULL ? state_array_runtime_local(root, "services") : NULL;
6108 source_technicians = root != NULL ? state_array_runtime_local(root, "technicians") : NULL;
6109 if (business == NULL || mut_doc == NULL || mut_root == NULL || services == NULL || !yyjson_mut_is_arr(services) || source_services == NULL || !yyjson_is_arr(source_services)) {
6110 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6112 native_send_text_response(connection, 500, "Internal Server Error", "service state unavailable\n");
6113 return 500;
6114 }
6115 {
6116 size_t service_count = yyjson_arr_size(source_services);
6117 size_t service_index = 0;
6118 for (service_index = 0; service_index < service_count; service_index++) {
6119 yyjson_val *source_service = yyjson_arr_get(source_services, service_index);
6120 if (yyjson_is_obj(source_service)
6121 && native_json_obj_get_long_default(source_service, "id", 0, NULL) == service_id
6122 && native_json_obj_get_long_default(source_service, "business_id", 0, NULL) == business_id) {
6123 service_row = yyjson_mut_arr_get(services, service_index);
6124 break;
6125 }
6126 }
6127 }
6128 if (service_row == NULL) {
6129 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6131 write_json_error(connection, 404, "Not Found", "service_not_found", "Service not found.");
6132 return 404;
6133 }
6134 if (!yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "name"), yyjson_mut_strcpy(mut_doc, name))
6135 || !yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "description"), yyjson_mut_strcpy(mut_doc, description != NULL ? description : ""))
6136 || !yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "cost"), yyjson_mut_strcpy(mut_doc, cost))
6137 || !yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "duration_minutes"), yyjson_mut_sint(mut_doc, duration_minutes))
6138 || !yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "sort_order"), yyjson_mut_sint(mut_doc, sort_order))) {
6139 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6141 native_send_text_response(connection, 500, "Internal Server Error", "service update failed\n");
6142 return 500;
6143 }
6144 {
6145 yyjson_mut_val *booking_options = yyjson_mut_arr(mut_doc);
6146 if (booking_options == NULL || !yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "booking_options"), booking_options)) {
6147 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6149 native_send_text_response(connection, 500, "Internal Server Error", "service options update failed\n");
6150 return 500;
6151 }
6152 for (long option_index = 1; option_index <= booking_option_count; option_index++) {
6153 char key[64];
6154 char *label = NULL;
6155 char *option_duration_text = NULL;
6156 char *option_cost = NULL;
6157 long option_duration = 0;
6158 yyjson_mut_val *option_obj = NULL;
6159 snprintf(key, sizeof(key), "booking_option_label_%ld", option_index);
6160 label = request_form_value_dup_runtime(request, key);
6161 snprintf(key, sizeof(key), "booking_option_duration_%ld", option_index);
6162 option_duration_text = request_form_value_dup_runtime(request, key);
6163 snprintf(key, sizeof(key), "booking_option_cost_%ld", option_index);
6164 option_cost = request_form_value_dup_runtime(request, key);
6165 option_duration = option_duration_text != NULL ? strtol(option_duration_text, NULL, 10) : 0;
6166 if (label != NULL && label[0] != '\0' && option_cost != NULL && option_cost[0] != '\0' && option_duration > 0) {
6167 option_obj = yyjson_mut_obj(mut_doc);
6168 if (option_obj != NULL
6169 && yyjson_mut_obj_add_strcpy(mut_doc, option_obj, "label", label)
6170 && yyjson_mut_obj_add_int(mut_doc, option_obj, "duration_minutes", option_duration)
6171 && yyjson_mut_obj_add_strcpy(mut_doc, option_obj, "cost", option_cost)) {
6172 yyjson_mut_arr_append(booking_options, option_obj);
6173 }
6174 }
6175 free(label);
6176 free(option_duration_text);
6177 free(option_cost);
6178 }
6179 }
6180 {
6181 yyjson_mut_val *assigned_ids = yyjson_mut_arr(mut_doc);
6182 if (assigned_ids == NULL || !yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "technician_ids"), assigned_ids)) {
6183 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6185 native_send_text_response(connection, 500, "Internal Server Error", "service technician update failed\n");
6186 return 500;
6187 }
6188 if (source_technicians != NULL && yyjson_is_arr(source_technicians)) {
6189 size_t tech_count = yyjson_arr_size(source_technicians);
6190 size_t tech_index = 0;
6191 for (tech_index = 0; tech_index < tech_count; tech_index++) {
6192 yyjson_val *tech_row = yyjson_arr_get(source_technicians, tech_index);
6193 char field_name[64];
6194 char *field_value = NULL;
6195 long technician_id = 0;
6196 if (!yyjson_is_obj(tech_row) || native_json_obj_get_long_default(tech_row, "business_id", 0, NULL) != business_id) {
6197 continue;
6198 }
6199 technician_id = native_json_obj_get_long_default(tech_row, "id", 0, NULL);
6200 snprintf(field_name, sizeof(field_name), "technician_%ld", technician_id);
6201 field_value = request_form_value_dup_runtime(request, field_name);
6202 if (field_value != NULL && field_value[0] != '\0') {
6203 yyjson_mut_arr_append(assigned_ids, yyjson_mut_sint(mut_doc, technician_id));
6204 }
6205 free(field_value);
6206 }
6207 }
6208 }
6209 {
6210 char *existing_image = native_json_obj_get_string_dup((yyjson_val *)service_row, "image_url");
6211 if (remove_image != NULL && remove_image[0] != '\0') {
6212 if (existing_image != NULL && existing_image[0] != '\0' && app->artifact_dir != NULL) {
6213 remove_media_url_file(app->artifact_dir, existing_image);
6214 }
6215 yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "image_url"), yyjson_mut_strcpy(mut_doc, ""));
6216 } else if (request->is_multipart && request->body_file_path != NULL && request->body_file_path[0] != '\0' && app->artifact_dir != NULL) {
6217 char target_url_dir[256];
6218 char fallback_filename[128];
6219 char *uploaded_media_url = NULL;
6220 snprintf(target_url_dir, sizeof(target_url_dir), "/media/businesses/%s/services", business_slug);
6221 snprintf(fallback_filename, sizeof(fallback_filename), "%s", name);
6222 ensure_business_media_tree(app->artifact_dir, business_slug);
6223 uploaded_media_url = store_uploaded_media(app->artifact_dir, request->content_type, request->body_file_path, "service_image", target_url_dir, fallback_filename);
6224 if (uploaded_media_url != NULL && uploaded_media_url[0] != '\0') {
6225 if (existing_image != NULL && existing_image[0] != '\0') {
6226 remove_media_url_file(app->artifact_dir, existing_image);
6227 }
6228 yyjson_mut_obj_put(service_row, yyjson_mut_strcpy(mut_doc, "image_url"), yyjson_mut_strcpy(mut_doc, uploaded_media_url));
6229 }
6230 free(uploaded_media_url);
6231 }
6232 free(existing_image);
6233 }
6234 {
6235 char *redirect_slug = business_slug != NULL ? strdup(business_slug) : NULL;
6236 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
6237 int write_code = text != NULL ? persist_app_state_text_runtime_local(app, text) : 69;
6238 free(text);
6239 free(request_path); free(business_slug); free(service_id_text); free(name); free(description); free(cost); free(duration_text); free(sort_order_text); free(booking_option_count_text); free(remove_image);
6241 if (write_code != 0 || redirect_slug == NULL) {
6242 free(redirect_slug);
6243 native_send_text_response(connection, 500, "Internal Server Error", "service persist failed\n");
6244 return 500;
6245 }
6247 connection,
6248 "HTTP/1.1 302 Found\r\n"
6249 "Location: %s/businesses/%s/\r\n"
6250 "Cache-Control: no-store\r\n"
6251 "Content-Length: 0\r\n"
6252 "Connection: close\r\n"
6253 "\r\n",
6254 app->base_path != NULL ? app->base_path : "",
6255 redirect_slug
6256 );
6257 free(redirect_slug);
6258 return 302;
6259 }
6260 }
6261
6262 if (match->action_id != NULL && streq(match->action_id, "create_service")) {
6263 char *form_body = request_body_text_dup_runtime(request);
6264 char *cost = native_dynamic_form_value(form_body != NULL ? form_body : "", "cost");
6265 if (cost == NULL || cost[0] == '\0') {
6266 free(form_body);
6267 free(cost);
6268 native_send_text_response(connection, 422, "Unprocessable Entity", "service_cost_required\n");
6269 return 422;
6270 }
6271 free(form_body);
6272 free(cost);
6273 write_json_error(connection, 501, "Not Implemented", "not_implemented", "Native service creation is not implemented yet.");
6274 return 501;
6275 }
6276
6277 if (match->action_id != NULL && streq(match->action_id, "create_business_availability")) {
6278 char *form_body = request_body_text_dup_runtime(request);
6279 char *day_of_week = native_dynamic_form_value(form_body != NULL ? form_body : "", "day_of_week");
6280 char *start_time = native_dynamic_form_value(form_body != NULL ? form_body : "", "start_time");
6281 char *end_time = native_dynamic_form_value(form_body != NULL ? form_body : "", "end_time");
6282 int start_minutes = 0;
6283 int end_minutes = 0;
6284 int day_valid = valid_day_of_week_runtime(day_of_week);
6285 int start_valid = time_text_to_minutes_runtime(start_time, &start_minutes);
6286 int end_valid = time_text_to_minutes_runtime(end_time, &end_minutes);
6287 if (!day_valid || !start_valid || !end_valid || end_minutes <= start_minutes) {
6288 free(form_body);
6289 free(day_of_week);
6290 free(start_time);
6291 free(end_time);
6292 native_send_text_response(connection, 422, "Unprocessable Entity", "Validation failed: availability_day_valid availability_end_after_start\n");
6293 return 422;
6294 }
6295 free(form_body);
6296 free(day_of_week);
6297 free(start_time);
6298 free(end_time);
6299 write_json_error(connection, 501, "Not Implemented", "not_implemented", "Native availability creation is not implemented yet.");
6300 return 501;
6301 }
6302
6303 if (match->action_id != NULL && streq(match->action_id, "rate_business")) {
6304 long active_user_id = active_user_id_from_cookie_runtime(app->state_path, request->cookie);
6305 char *request_path = request_path_dup_runtime(request);
6306 char *business_slug = request_path != NULL ? native_dynamic_dispatch_extract_param(match->path, request_path, "business_slug") : NULL;
6307 char *score_text = request_form_value_dup_runtime(request, "score");
6308 char *comment = request_form_value_dup_runtime(request, "comment");
6309 long score = score_text != NULL ? strtol(score_text, NULL, 10) : 0;
6310 yyjson_doc *doc = NULL;
6311 yyjson_val *root = NULL;
6312 yyjson_val *business = NULL;
6313 yyjson_val *source_ratings = NULL;
6314 yyjson_mut_doc *mut_doc = NULL;
6315 yyjson_mut_val *mut_root = NULL;
6316 yyjson_mut_val *ratings = NULL;
6317 yyjson_mut_val *rating_row = NULL;
6318 long business_id = 0;
6319 int updated = 0;
6320
6321 if (active_user_id <= 0) {
6322 free(request_path);
6323 free(business_slug);
6324 free(score_text);
6325 free(comment);
6326 write_json_error(connection, 403, "Forbidden", "session_required", "Submitting a rating requires an active session.");
6327 return 403;
6328 }
6329 if (business_slug == NULL || business_slug[0] == '\0') {
6330 free(request_path);
6331 free(business_slug);
6332 free(score_text);
6333 free(comment);
6334 write_json_error(connection, 422, "Unprocessable Entity", "invalid_business_slug", "Missing business slug.");
6335 return 422;
6336 }
6337 if (score < 1 || score > 5) {
6338 free(request_path);
6339 free(business_slug);
6340 free(score_text);
6341 free(comment);
6342 write_json_error(connection, 422, "Unprocessable Entity", "invalid_rating_score", "Rating score must be between 1 and 5.");
6343 return 422;
6344 }
6345
6346 doc = app->state_path != NULL ? native_json_doc_load_file(app->state_path) : NULL;
6347 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
6348 business = root != NULL ? find_business_by_slug_runtime_local(root, business_slug) : NULL;
6349 business_id = business != NULL ? native_json_obj_get_long_default(business, "id", 0, NULL) : 0;
6350 source_ratings = root != NULL ? state_array_runtime_local(root, "ratings") : NULL;
6351 mut_doc = doc != NULL ? yyjson_doc_mut_copy(doc, NULL) : NULL;
6352 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
6353 ratings = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "ratings") : NULL;
6354
6355 if (business == NULL || business_id <= 0 || mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
6356 free(request_path);
6357 free(business_slug);
6358 free(score_text);
6359 free(comment);
6361 yyjson_mut_doc_free(mut_doc);
6362 write_json_error(connection, 404, "Not Found", "business_not_found", "Business not found.");
6363 return 404;
6364 }
6365 if (ratings == NULL || !yyjson_mut_is_arr(ratings)) {
6366 ratings = yyjson_mut_arr(mut_doc);
6367 if (ratings == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, "ratings"), ratings)) {
6368 free(request_path);
6369 free(business_slug);
6370 free(score_text);
6371 free(comment);
6373 yyjson_mut_doc_free(mut_doc);
6374 native_send_text_response(connection, 500, "Internal Server Error", "ratings array unavailable\n");
6375 return 500;
6376 }
6377 }
6378
6379 if (source_ratings != NULL && yyjson_is_arr(source_ratings)) {
6380 size_t rating_count = yyjson_arr_size(source_ratings);
6381 size_t rating_index = 0;
6382 for (rating_index = 0; rating_index < rating_count; rating_index++) {
6383 yyjson_val *source_rating = yyjson_arr_get(source_ratings, rating_index);
6384 if (yyjson_is_obj(source_rating)
6385 && native_json_obj_get_long_default(source_rating, "business_id", 0, NULL) == business_id
6386 && native_json_obj_get_long_default(source_rating, "user_id", 0, NULL) == active_user_id) {
6387 rating_row = yyjson_mut_arr_get(ratings, rating_index);
6388 break;
6389 }
6390 }
6391 }
6392
6393 if (rating_row != NULL) {
6394 if (!yyjson_mut_obj_put(rating_row, yyjson_mut_strcpy(mut_doc, "score"), yyjson_mut_sint(mut_doc, score))
6395 || !yyjson_mut_obj_put(rating_row, yyjson_mut_strcpy(mut_doc, "comment"), yyjson_mut_strcpy(mut_doc, comment != NULL ? comment : ""))) {
6396 free(request_path);
6397 free(business_slug);
6398 free(score_text);
6399 free(comment);
6401 yyjson_mut_doc_free(mut_doc);
6402 native_send_text_response(connection, 500, "Internal Server Error", "rating update failed\n");
6403 return 500;
6404 }
6405 updated = 1;
6406 } else {
6407 long rating_id = next_array_id_runtime_local(source_ratings);
6408 rating_row = yyjson_mut_obj(mut_doc);
6409 if (rating_row == NULL
6410 || !yyjson_mut_obj_add_int(mut_doc, rating_row, "id", rating_id)
6411 || !yyjson_mut_obj_add_int(mut_doc, rating_row, "user_id", active_user_id)
6412 || !yyjson_mut_obj_add_int(mut_doc, rating_row, "business_id", business_id)
6413 || !yyjson_mut_obj_add_int(mut_doc, rating_row, "score", score)
6414 || !yyjson_mut_obj_add_strcpy(mut_doc, rating_row, "comment", comment != NULL ? comment : "")
6415 || !yyjson_mut_arr_append(ratings, rating_row)) {
6416 free(request_path);
6417 free(business_slug);
6418 free(score_text);
6419 free(comment);
6421 yyjson_mut_doc_free(mut_doc);
6422 native_send_text_response(connection, 500, "Internal Server Error", "rating create failed\n");
6423 return 500;
6424 }
6425 }
6426
6427 {
6428 char *text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
6429 if (text == NULL || persist_app_state_text_runtime_local(app, text) != 0) {
6430 free(text);
6431 free(request_path);
6432 free(business_slug);
6433 free(score_text);
6434 free(comment);
6436 yyjson_mut_doc_free(mut_doc);
6437 native_send_text_response(connection, 500, "Internal Server Error", updated ? "rating update persist failed\n" : "rating create persist failed\n");
6438 return 500;
6439 }
6440 free(text);
6441 }
6442
6444 connection,
6445 "HTTP/1.1 302 Found\r\n"
6446 "Location: %s/b/%s/\r\n"
6447 "Cache-Control: no-store\r\n"
6448 "Content-Length: 0\r\n"
6449 "Connection: close\r\n"
6450 "\r\n",
6451 app->base_path != NULL ? app->base_path : "",
6452 business_slug != NULL ? business_slug : ""
6453 );
6454 free(request_path);
6455 free(business_slug);
6456 free(score_text);
6457 free(comment);
6459 yyjson_mut_doc_free(mut_doc);
6460 return 302;
6461 }
6462
6463 if (match->action_id != NULL && streq(match->action_id, "logout")) {
6464 char *clear_cookie_header = session_cookie_header_dup_runtime(app, request, NULL, 1);
6465 (void)revoke_session_by_cookie_runtime(app, request->cookie);
6467 connection,
6468 "HTTP/1.1 302 Found\r\n"
6469 "Location: %s/accounts/login/\r\n"
6470 "%s\r\n"
6471 "Cache-Control: no-store\r\n"
6472 "Content-Length: 0\r\n"
6473 "Connection: close\r\n"
6474 "\r\n",
6475 app->base_path != NULL ? app->base_path : "",
6476 clear_cookie_header != NULL ? clear_cookie_header : ""
6477 );
6478 free(clear_cookie_header);
6479 return 302;
6480 }
6481
6482 write_json_error(connection, 501, "Not Implemented", "not_implemented", "This action is not implemented in the native runtime yet.");
6483 return 501;
6484}
6485
6486/* ------------------------------------------------------------------ */
6487/* Public API */
6488/* ------------------------------------------------------------------ */
6489
6491 return DYNAMIC_HOST_PROFILE;
6492}
6493
6495 const AppRuntime *app,
6496 const HttpRequest *request,
6497 NativeConnection *connection
6498) {
6499 char *route_fixture_path = NULL;
6500 char *request_path = NULL;
6501 char *query = NULL;
6502 NativeDynamicRouteMatch *match = NULL;
6503 NativeDynamicRelationalRequestSync relational_sync = {0};
6504 int status = 500;
6505 const char *target;
6506
6507 if (app == NULL || request == NULL || connection == NULL) {
6508 if (connection != NULL) {
6509 native_send_text_response(connection, 500, "Internal Server Error", "null parameters\n");
6510 }
6511 return 500;
6512 }
6513
6514 target = request->target != NULL ? request->target : "/";
6515
6516 fprintf(stderr, "dynamic_handler_enter: target=%s app_id=%s artifact_dir=%s\n",
6517 target,
6518 app->app_id != NULL ? app->app_id : "NULL",
6519 app->artifact_dir != NULL ? app->artifact_dir : "NULL");
6520 fflush(stderr);
6521
6522 /* Strip query string and remove any mounted app base path for route matching */
6523 request_path = route_request_path_dup_runtime(app, request);
6524 if (request_path == NULL) {
6525 native_send_text_response(connection, 500, "Internal Server Error", "memory allocation failed\n");
6526 return 500;
6527 }
6528
6529 /* Extract query string for parameter handling */
6530 {
6531 char raw_query[MAX_QUERY_LEN];
6532 extract_query(target, raw_query, sizeof(raw_query));
6533 if (raw_query[0] != '\0') {
6534 query = strdup(raw_query);
6535 }
6536 }
6537
6538 if (native_dynamic_relational_prepare_request_state(app, &relational_sync) != 0) {
6539 native_send_text_response(connection, 500, "Internal Server Error", "relational runtime prepare failed\n");
6540 free(request_path);
6541 free(query);
6542 return 500;
6543 }
6544
6545 /* 1. Try static asset serving first */
6546 if (app->artifact_dir != NULL && native_dynamic_media_is_static_path(request_path)) {
6547 status = native_dynamic_media_serve_asset(connection, app->artifact_dir, request_path, request);
6548 goto cleanup;
6549 }
6550
6551 /* 2. Serve runtime state from the app state file */
6552 if (streq(request_path, "/api/state")) {
6553 if (app->app_id != NULL && streq(app->app_id, "dynamic_app_template")) {
6554 status = write_proof_state_api_response(connection, app->state_path);
6555 goto cleanup;
6556 }
6557 if (app->state_path != NULL && path_exists(app->state_path)) {
6559 char *state_text = state_doc != NULL ? yyjson_write(state_doc, YYJSON_WRITE_NOFLAG, NULL) : read_file_text(app->state_path);
6560 if (state_text != NULL) {
6561 int state_len = (int)strlen(state_text);
6563 connection,
6564 "HTTP/1.1 200 OK\r\n"
6565 "Content-Type: application/json\r\n"
6566 "Content-Length: %d\r\n"
6567 "Connection: close\r\n"
6568 "\r\n%s",
6569 state_len,
6570 state_text
6571 );
6572 native_json_doc_free(state_doc);
6573 free(state_text);
6574 status = 200;
6575 goto cleanup;
6576 }
6577 native_json_doc_free(state_doc);
6578 }
6579 /* State file missing or unreadable — return empty object */
6581 connection,
6582 "HTTP/1.1 200 OK\r\n"
6583 "Content-Type: application/json\r\n"
6584 "Content-Length: 2\r\n"
6585 "Connection: close\r\n"
6586 "\r\n{}"
6587 );
6588 status = 200;
6589 goto cleanup;
6590 }
6591
6592 if (request->method != NULL && streq(request->method, "POST")) {
6594 char *body_text = request_body_text_dup_runtime(request);
6595 yyjson_doc *body_doc = NULL;
6596 yyjson_val *body_root = load_root_from_text_runtime(body_text != NULL ? body_text : "{}", &body_doc);
6597 yyjson_val *day_value = body_root != NULL ? native_json_obj_get(body_root, "day_of_week") : NULL;
6598 yyjson_val *start_value = body_root != NULL ? native_json_obj_get(body_root, "start_time") : NULL;
6599 yyjson_val *end_value = body_root != NULL ? native_json_obj_get(body_root, "end_time") : NULL;
6600 char *day_of_week = (day_value != NULL && yyjson_is_str(day_value)) ? strdup(yyjson_get_str(day_value)) : NULL;
6601 char *start_time = (start_value != NULL && yyjson_is_str(start_value)) ? strdup(yyjson_get_str(start_value)) : NULL;
6602 char *end_time = (end_value != NULL && yyjson_is_str(end_value)) ? strdup(yyjson_get_str(end_value)) : NULL;
6603 int start_minutes = 0;
6604 int end_minutes = 0;
6605 int day_valid = valid_day_of_week_runtime(day_of_week);
6606 int start_valid = time_text_to_minutes_runtime(start_time, &start_minutes);
6607 int end_valid = time_text_to_minutes_runtime(end_time, &end_minutes);
6608 if (!day_valid || !start_valid || !end_valid || end_minutes <= start_minutes) {
6610 connection,
6611 "HTTP/1.1 422 Unprocessable Entity\r\n"
6612 "Content-Type: application/json\r\n"
6613 "Connection: close\r\n"
6614 "\r\n{\"errors\":[\"availability_day_valid\",\"availability_end_after_start\"]}"
6615 );
6616 free(body_text);
6617 free(day_of_week);
6618 free(start_time);
6619 free(end_time);
6620 native_json_doc_free(body_doc);
6621 status = 422;
6622 goto cleanup;
6623 }
6624 free(body_text);
6625 free(day_of_week);
6626 free(start_time);
6627 free(end_time);
6628 native_json_doc_free(body_doc);
6629 write_json_error(connection, 501, "Not Implemented", "not_implemented", "Native availability API creation is not implemented yet.");
6630 status = 501;
6631 goto cleanup;
6632 }
6633 }
6634
6635 if (request->method != NULL && streq(request->method, "GET")) {
6637 char *business_slug = native_dynamic_dispatch_extract_param("/api/businesses/:business_slug/services/", request_path, "business_slug");
6638 if (business_slug != NULL) {
6639 status = write_business_services_api_response(connection, app->state_path, business_slug);
6640 free(business_slug);
6641 goto cleanup;
6642 }
6643 }
6645 char *business_slug = native_dynamic_dispatch_extract_param("/api/businesses/:business_slug/availability/", request_path, "business_slug");
6646 if (business_slug != NULL) {
6647 status = write_business_availability_api_response(connection, app->state_path, business_slug);
6648 free(business_slug);
6649 goto cleanup;
6650 }
6651 }
6653 char *business_slug = native_dynamic_dispatch_extract_param("/api/businesses/:business_slug/services/schema/", request_path, "business_slug");
6654 if (business_slug != NULL) {
6655 status = write_generated_surface_schema_response(connection, app, app->state_path, business_slug, "service_generated_api");
6656 free(business_slug);
6657 goto cleanup;
6658 }
6659 }
6661 char *business_slug = native_dynamic_dispatch_extract_param("/api/businesses/:business_slug/availability/schema/", request_path, "business_slug");
6662 if (business_slug != NULL) {
6663 status = write_generated_surface_schema_response(connection, app, app->state_path, business_slug, "business_availability_generated_api");
6664 free(business_slug);
6665 goto cleanup;
6666 }
6667 }
6668 }
6669
6670 /* 2. Try to match against the route fixture */
6671 route_fixture_path = native_dynamic_dispatch_resolve_route_fixture(app->artifact_dir, app->app_id);
6672 if (route_fixture_path == NULL) {
6673 /* No route fixture available; try the dispatch table as a fallback */
6674 if (app->artifact_dir != NULL) {
6675 char *dispatch_file = native_dynamic_dispatch_response_file(app->artifact_dir, request->method, request_path);
6676 if (dispatch_file != NULL) {
6677 status = native_dynamic_media_serve_asset(connection, app->artifact_dir, request_path, request);
6678 free(dispatch_file);
6679 goto cleanup;
6680 }
6681 }
6682 write_json_error(connection, 404, "Not Found", "not_found", "No route fixture available and no dispatch match.");
6683 status = 404;
6684 goto cleanup;
6685 }
6686
6687 /* Match the request path against the route fixture */
6688 {
6689 int match_result = native_dynamic_dispatch_match(
6690 route_fixture_path,
6691 request->method != NULL ? request->method : "GET",
6692 request_path,
6693 &match
6694 );
6695
6696 if (match_result < 0) {
6697 int exists = route_fixture_path != NULL ? path_exists(route_fixture_path) : 0;
6698 fprintf(stderr, "dynamic_dispatch_error: path=%s exists=%d app_id=%s artifact_dir=%s\n",
6699 route_fixture_path != NULL ? route_fixture_path : "NULL",
6700 exists,
6701 app->app_id != NULL ? app->app_id : "NULL",
6702 app->artifact_dir != NULL ? app->artifact_dir : "NULL");
6703 fflush(stderr);
6704 write_json_error(connection, 500, "Internal Server Error", "dispatch_error", "route fixture load failed");
6705 status = 500;
6706 goto cleanup;
6707 }
6708
6709 if (match_result > 0 || match == NULL) {
6710 /* No route matched — try the dispatch table */
6711 if (app->artifact_dir != NULL) {
6712 char *dispatch_file = native_dynamic_dispatch_response_file(app->artifact_dir, request->method, request_path);
6713 if (dispatch_file != NULL) {
6714 status = native_dynamic_media_serve_asset(connection, app->artifact_dir, request_path, request);
6715 free(dispatch_file);
6716 goto cleanup;
6717 }
6718 }
6719 write_json_error(connection, 404, "Not Found", "not_found", "No matching route for this path.");
6720 status = 404;
6721 goto cleanup;
6722 }
6723 }
6724
6725 /* 3. Route matched — dispatch based on kind */
6726 {
6727 char *csrf_token = csrf_token_for_response(request->cookie);
6728 if (match->kind != NULL && streq(match->kind, "action")) {
6729 status = handle_action_route(connection, app, request, match);
6730 } else {
6731 status = handle_page_route(connection, app, request, match, request_path, query, csrf_token);
6732 }
6733 free(csrf_token);
6734 }
6735
6736cleanup:
6737 if (native_dynamic_relational_sync_back_if_changed(app, &relational_sync) != 0 && status < 400) {
6738 status = 500;
6739 }
6742 free(route_fixture_path);
6743 free(request_path);
6744 free(query);
6745 return status;
6746}
char * native_dynamic_dispatch_response_file(const char *artifact_dir, const char *method, const char *path)
Resolves the response artifact file for a matched page route.
int native_dynamic_dispatch_match(const char *route_fixture_path, const char *method, const char *path, NativeDynamicRouteMatch **match_out)
Matches a request path against the route fixture for an app.
char * native_dynamic_dispatch_resolve_route_fixture(const char *artifact_dir, const char *app_id)
Resolves the route fixture path from the artifact directory.
char * native_dynamic_dispatch_extract_param(const char *pattern, const char *path, const char *param_name)
Returns the URL path parameter value extracted from a matched route.
void native_dynamic_dispatch_free_match(NativeDynamicRouteMatch *match)
Frees a route match returned by native_dynamic_dispatch_match().
Route dispatch for in-process dynamic-app request handling.
int remove_media_url_file(const char *artifact_dir, const char *media_url)
Removes a managed business media file from an app artifact when it exists.
char * store_uploaded_media(const char *artifact_dir, const char *content_type, const char *body_path, const char *field_name, const char *target_url_dir, const char *fallback_filename)
Extracts a multipart upload into the app artifact media tree and returns the resulting media URL.
int native_dynamic_media_serve_asset(NativeConnection *connection, const char *artifact_dir, const char *path, const HttpRequest *request)
Serves a static asset from the artifact directory.
int ensure_business_media_tree(const char *artifact_dir, const char *business_slug)
Ensures the managed business media subtree exists under an app artifact.
int native_dynamic_media_is_static_path(const char *path)
Determines whether a request path targets a known static asset.
Media and static asset serving for in-process dynamic handling.
char * native_dynamic_multipart_extract_text(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name)
Extracts a text value from a multipart form field.
Pharos-owned multipart form-data parsing for in-process dynamic handling.
int native_dynamic_relational_runtime_import_state(const char *app_root, const char *runtime_conf_path, const char *app_id, const char *state_path, const char *expected_audit_count)
Imports canonical runtime state JSON into the configured relational backend.
void native_dynamic_relational_request_sync_free(NativeDynamicRelationalRequestSync *sync)
Clears heap-owned fields in a request-local relational sync record.
char * native_dynamic_relational_runtime_export_json_dup(const char *app_root, const char *runtime_conf_path, const char *app_id)
Exports canonical runtime state JSON from the configured relational backend.
int native_dynamic_relational_prepare_request_state(const AppRuntime *app, NativeDynamicRelationalRequestSync *sync)
Exports relational state into the app state file before request handling when enabled.
char * native_dynamic_relational_backend_dup(const char *app_root, const char *runtime_conf_path)
Returns the configured relational backend when it is recognized.
int native_dynamic_relational_sync_back_if_changed(const AppRuntime *app, const NativeDynamicRelationalRequestSync *sync)
Imports the app state file back into the relational backend when it changed.
Declares Pharos-owned native relational runtime helpers for dynamic apps.
char * native_dynamic_form_value(const char *body, const char *key)
Extracts a value from a URL-encoded form body.
char * native_dynamic_query_value(const char *query, const char *key)
Extracts a parameter value from a query string.
char * native_dynamic_cookie_value(const char *cookie_header, const char *name)
Extracts a cookie value from a Cookie header string.
Request field extraction helpers for in-process dynamic handling.
static char * invite_created_page_params_json_dup_runtime(const char *invite_email, const char *invite_path, const char *return_path)
Builds template params for the invite-created confirmation page.
static void extract_query(const char *target, char *dest, size_t dest_size)
Extracts the query portion from a target string.
static char * password_reset_sent_page_params_json_dup_runtime(const char *preview_path)
static char * generated_admin_manage_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *surface_kind, const char *error_text)
static char * proof_task_items_html_dup_runtime(yyjson_val *items)
char * business_calendar_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *app_root, const char *selected_year_text, const char *selected_month_text, const char *selected_date_text)
static int append_generated_admin_surface_form_fields_runtime(const GeneratedAdminSurfaceDescriptor *descriptor, char **form_fields_html, size_t *form_fields_length, size_t *form_fields_capacity, const char *day_options_html)
Appends create-form field HTML for one generated-admin surface.
static char * generated_admin_surface_filter_controls_dup_runtime(const GeneratedAdminSurfaceDescriptor *descriptor)
Duplicates the filter-control HTML for one generated-admin surface.
static int handle_password_reset_request_action_runtime(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request)
Handles password-reset request action processing.
static int resource_link_display_load_runtime(yyjson_val *root, const char *resource_collection_name, long resource_id, ResourceLinkDisplay *display)
Loads slug and display-name fields for one linked resource id.
static char * token_hash_dup_runtime(const char *token_value)
static char * runtime_page_params_write_runtime(yyjson_mut_doc *doc, yyjson_mut_val *root)
Finalizes and writes one page-params document as compact JSON.
static void bytes_to_hex_runtime(const uint8_t *bytes, size_t len, char *hex_out)
static yyjson_val * find_user_by_id_runtime_local(yyjson_val *root, long user_id)
static int csrf_request_valid_runtime(const HttpRequest *request, const char *form_body)
static char * proof_home_page_params_json_dup_runtime(const char *state_path)
static int iso_after_seconds_runtime(long delta_seconds, char out[21])
static char * service_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long service_id)
static char * business_availability_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long availability_id, const char *error_text)
static char * resolve_declared_page_params_json_runtime(const AppRuntime *app, const HttpRequest *request, const NativeDynamicRouteMatch *match, const char *request_path, const char *query_string, long active_user_id)
Resolves declared page parameter JSON for the matched page route.
static yyjson_val * proof_state_root_load_runtime(const char *state_path, yyjson_doc **doc_out)
static int time_ranges_overlap_runtime_local(int start_a_minutes, int duration_a, int start_b_minutes, int duration_b)
static int persist_app_state_text_runtime_local(const AppRuntime *app, const char *state_json_text)
Appends a native auth/rate-limit event row to runtime state.
static const char * day_abbrev_for_ymd_runtime_local(int year, int month, int day)
static int write_business_services_api_response(NativeConnection *connection, const char *state_path, const char *business_slug)
static uint32_t sha256_rotr(uint32_t value, uint32_t bits)
static void sha256_update_runtime(Sha256Runtime *ctx, const uint8_t *data, size_t len)
static yyjson_val * find_business_by_slug_runtime_local(yyjson_val *root, const char *business_slug)
#define CSRF_TOKEN_HEX_LEN
Maximum CSRF token length (24 bytes = 48 hex chars).
static char * session_cookie_header_dup_runtime(const AppRuntime *app, const HttpRequest *request, const char *session_token, int clear_cookie)
static int render_declared_page_response_runtime(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const char *page_id, const char *request_path, const char *body_params_json)
char * business_landing_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long user_id, const char *app_root)
static int append_profile_subscription_rows_runtime(yyjson_val *root, long user_id, char **subscriptions_block, size_t *subscriptions_length, size_t *subscriptions_capacity)
Appends profile subscription rows for one user into an HTML list buffer.
static char * csrf_request_token_dup_runtime(const HttpRequest *request, const char *form_body)
static char * sha256_hex_dup_runtime(const char *value)
static yyjson_val * find_service_for_business_runtime_local(yyjson_val *root, long service_id, long business_id)
static char * invite_accept_page_params_json_dup_runtime(const char *invite_kind, const char *target_business_name, const char *error_message, const char *invite_email, const char *form_action)
Builds template params for the invite-accept page.
static void write_json_error(NativeConnection *connection, int status, const char *status_text, const char *error_kind, const char *message)
Writes a JSON error response to a connection.
static char * csrf_token_for_response(const char *cookie_header)
Returns the CSRF token to use for a response.
static char * password_hash_dup_runtime(const char *password_value)
static void hmac_sha256_runtime(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t out[SHA256_DIGEST_SIZE])
static void sha256_init_runtime(Sha256Runtime *ctx)
#define MAX_QUERY_LEN
Maximum query string length we process.
static char * day_of_week_options_html_dup_runtime(const char *selected_day)
static void write_html_response_with_headers_runtime(NativeConnection *connection, int status, const char *status_text, const char *html, const char *csrf_token, const char *extra_headers)
Writes an HTML response to a connection, optionally with a CSRF cookie.
static int write_business_availability_api_response(NativeConnection *connection, const char *state_path, const char *business_slug)
#define CSRF_COOKIE_MAX_AGE
CSRF cookie max-age in seconds.
static int text_append_format_runtime_local(char **buffer, size_t *length, size_t *capacity, const char *format,...)
static char * request_form_value_dup_runtime(const HttpRequest *request, const char *field_name)
const char * native_dynamic_runtime_host_profile(void)
Returns the canonical host profile string that this module handles.
static int handle_accept_invite_action_runtime(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const NativeDynamicRouteMatch *match)
Handles invite acceptance and initial session creation.
static void serve_dynamic_home_page(NativeConnection *connection, const char *app_id, const char *route_id, const char *csrf_token)
Serves the Pharos dynamic-app home page or a generic landing page.
static int parse_ymd_runtime_local(const char *date_value, int *year_out, int *month_out, int *day_out)
static char * generated_surface_fixture_path_dup_runtime(const AppRuntime *app)
Resolves the generated-surface fixture path for the active app.
static int is_business_services_api_list_path_runtime(const char *request_path)
Tests whether a request path is the exact business services list API path.
static int user_has_active_tenant_membership_runtime_local(yyjson_val *root, long user_id, long tenant_id)
Checks whether a user has an active membership on a tenant.
static char * profile_page_params_json_dup_runtime_local(const char *state_path, long user_id, const char *payment_preference_options_html)
Builds profile-page template params directly from runtime state.
static int is_business_availability_api_list_path_runtime(const char *request_path)
Tests whether a request path is the exact business availability list API path.
static char * invite_accept_done_page_params_json_dup_runtime(const char *continue_path)
Builds template params for the invite-accepted confirmation page.
#define SHA256_DIGEST_SIZE
static int secure_text_equals(const char *left, const char *right)
static char * proof_state_seed_json_dup_runtime(void)
static char * csrf_inject_hidden_inputs(const char *html, const char *csrf_token)
Injects CSRF hidden inputs into form tags in an HTML string.
static int runtime_page_params_add_escaped_string_runtime(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *key, const char *value)
Adds one HTML-escaped string field to a page-params JSON object.
static const char * membership_role_id_for_invite_runtime_local(const char *role_hint, const char *account_type)
Maps invite role hints onto the canonical tenant membership role id.
static int value_long_equals_runtime_local(yyjson_val *value, long expected_value)
static int verify_password_hash_runtime(const char *stored_hash, const char *password_value, const char *username_value)
static int now_iso_utc_runtime(char out[21])
static char * request_body_text_dup_runtime(const HttpRequest *request)
static char * login_page_params_json_dup_runtime(const char *query_string, const char *error_message)
static int write_proof_state_api_response(NativeConnection *connection, const char *state_path)
static void sha256_transform_runtime(Sha256Runtime *ctx, const uint8_t data[])
static int issue_browser_session_runtime(const AppRuntime *app, const HttpRequest *request, long user_id, char **session_token_out, char **session_cookie_header_out)
static char * request_header_value_dup_runtime(const char *headers, const char *header_name)
static void write_html_response(NativeConnection *connection, int status, const char *status_text, const char *html, const char *csrf_token)
static yyjson_val * find_appointment_by_id_runtime_local(yyjson_val *root, long appointment_id)
static const char * default_signed_in_path_runtime(yyjson_val *user)
#define SHA256_BLOCK_SIZE
static long state_tenant_id_for_business_runtime_local(yyjson_val *root, long business_id)
Resolves the tenant id that owns a business record.
static char * route_request_path_dup_runtime(const AppRuntime *app, const HttpRequest *request)
static int business_open_for_slot_runtime_local(yyjson_val *root, long business_id, const char *date_value, const char *time_value, long duration_minutes)
static void generated_admin_surface_page_copy_runtime(yyjson_mut_doc *doc, yyjson_mut_val *page, const GeneratedAdminSurfaceDescriptor *descriptor, const char *filter_controls_html)
Writes descriptor-owned page copy into one generated-admin params document.
static int time_text_to_minutes_runtime(const char *time_text, int *minutes_out)
static int is_business_services_api_schema_path_runtime(const char *request_path)
Tests whether a request path is the exact business services schema API path.
static int revoke_session_by_cookie_runtime(const AppRuntime *app, const char *cookie_header)
static char * payment_preference_options_html_dup_runtime(const char *selected_value)
static void csrf_generate_random_hex(char *buf, size_t byte_count)
Generates a random hex token for CSRF protection.
static char * register_page_params_json_dup_runtime(const char *query_string)
#define DYNAMIC_HOST_PROFILE
Host profile string that triggers in-process dynamic handling.
static long assign_technician_for_slot_runtime_local(yyjson_val *root, long business_id, long service_id, const char *date_value, const char *time_value)
static int handle_issue_business_invite_action_runtime(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const NativeDynamicRouteMatch *match)
Handles invite creation for tenant-backed business membership flows.
static char * page_title_dup_runtime(yyjson_val *page_spec_root, yyjson_val *body_params_root)
static yyjson_val * find_business_by_id_runtime_local(yyjson_val *root, long business_id)
Finds one business record by numeric id in the runtime state.
static int long_from_value_runtime_local(yyjson_val *value, long *out_value)
static int pbkdf2_sha256_runtime(const uint8_t *password, size_t password_len, const uint8_t *salt, size_t salt_len, uint32_t iterations, uint8_t *out, size_t out_len)
static char * password_reset_request_page_params_json_dup_runtime(void)
static void resource_link_display_free_runtime(ResourceLinkDisplay *display)
Frees a linked-resource display record.
#define CSRF_COOKIE_NAME
CSRF cookie name used by the Pharos framework.
static char * resolve_invite_accept_page_params_json_runtime(const AppRuntime *app, const NativeDynamicRouteMatch *match, const char *request_path)
Resolves invite-accept page parameters from the current app state.
static int runtime_page_params_add_string_runtime(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *key, const char *value)
Builds native template params for the password-reset request page.
static long active_user_id_from_cookie_runtime(const char *state_path, const char *cookie_header)
static char * password_reset_complete_page_params_json_dup_runtime(const char *request_path)
Builds native template params for the password-reset completion page.
static int render_declared_page_response_with_headers_runtime(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const char *page_id, const char *request_path, const char *body_params_json, const char *extra_headers)
static int text_append_runtime_local(char **buffer, size_t *length, size_t *capacity, const char *text)
static char * csrf_token_from_cookie(const char *cookie_header)
Extracts the CSRF token from the request cookie header.
static yyjson_val * find_user_by_username_runtime_local(yyjson_val *root, const char *username)
static char * invite_accept_target_html_dup_runtime(const char *target_business_name)
Builds the optional invite target HTML fragment.
static char * password_reset_preview_html_dup_runtime(const char *preview_path)
Builds the optional password-reset preview HTML block.
static int request_has_session_cookie(const char *cookie_header)
static unsigned char * request_body_bytes_dup_runtime(const HttpRequest *request, size_t *out_size)
static int handle_page_route(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const NativeDynamicRouteMatch *match, const char *request_path, const char *query_string, const char *csrf_token)
Handles a matched page route using dynamic template rendering.
static int technician_booked_for_slot_runtime_local(yyjson_val *root, long technician_id, const char *date_value, const char *time_value, long duration_minutes)
#define SESSION_COOKIE_NAME
Session cookie name used by the Pharos framework.
static char * profile_subscriptions_block_dup_runtime(yyjson_val *root, long user_id)
Builds the profile subscription block for one user.
static int append_generated_admin_surface_rows_runtime(const GeneratedAdminSurfaceDescriptor *descriptor, yyjson_val *root, long business_id, const char *business_slug, char **row_html, size_t *row_html_length, size_t *row_html_capacity)
Appends row HTML for one generated-admin manage surface.
static int valid_day_of_week_runtime(const char *day_text)
Resolves the final page title from a declarative page spec and params.
static const GeneratedAdminSurfaceDescriptor * generated_admin_surface_descriptor_runtime(const char *surface_kind)
Returns the generated-admin descriptor for one declared manage surface.
static int handle_action_route(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const NativeDynamicRouteMatch *match)
Handles a matched action route.
int native_dynamic_runtime_handle_request(const AppRuntime *app, const HttpRequest *request, NativeConnection *connection)
Serves one dynamic-app request entirely in-process.
char * diagnostics_render_page_params_json_dup_runtime(const char *state_path)
static char * request_path_dup_runtime(const HttpRequest *request)
static int handle_password_reset_complete_action_runtime(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const NativeDynamicRouteMatch *match)
Handles password-reset completion action processing.
static yyjson_val * state_array_runtime_local(yyjson_val *root, const char *field_name)
static long next_array_id_runtime_local(yyjson_val *rows)
static int append_rate_limit_event_runtime_local(const AppRuntime *app, const char *policy_key, const char *bucket_key, const char *outcome)
static void sha256_final_runtime(Sha256Runtime *ctx, uint8_t hash[SHA256_DIGEST_SIZE])
static int is_business_availability_api_schema_path_runtime(const char *request_path)
Tests whether a request path is the exact business availability schema API path.
static int write_generated_surface_schema_response(NativeConnection *connection, const AppRuntime *app, const char *state_path, const char *business_slug, const char *surface_id)
Writes one generated-surface schema object from the declarative fixture.
In-process dynamic-app request handler for the Pharos native runtime.
Declares template rendering functions for the in-process dynamic runtime.
char * business_dashboard_render_page_params_json_dup_runtime(const char *state_path, long user_id, const char *app_root)
Builds rendered business dashboard page params JSON from runtime state.
char * profile_render_page_params_json_dup_runtime(const char *state_path, long user_id, const char *app_root, const char *payment_preference_options_html)
Builds rendered profile page params JSON from runtime state.
char * render_file_template_dup_runtime(const char *template_file, yyjson_val *params_root)
Reads a template file and renders it with JSON parameter substitution.
char * error_alert_html_dup_runtime(const char *message)
Renders an error alert.
char * join_base_relative_path_dup_runtime(const char *base_path, const char *relative_path)
Joins a base path and a relative path into one string.
char * business_detail_admin_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *app_root)
Builds rendered business admin page params JSON from runtime state.
char * appointments_render_page_params_json_dup_runtime(const char *state_path, long user_id, const char *app_root)
Builds rendered appointments page params JSON from runtime state.
char * explore_render_page_params_json_dup_runtime(const char *state_path, const char *app_root)
Builds rendered appointments page params JSON from runtime state.
char * html_escaped_dup_runtime(const char *text)
Escapes one plain-text string for safe HTML insertion.
char * declared_page_relative_path_dup_runtime(const char *app_root, const char *page_id)
Resolves one UCAL declared page id to its app-relative page-spec path.
char * business_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug)
Builds rendered business-edit page params JSON from runtime state.
char * service_book_form_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long service_id, const char *app_root, const char *error_message)
Builds rendered service-book-form page params JSON from runtime state.
char * hidden_next_input_html_dup_runtime(const char *next_path)
Renders a hidden next-path <input> HTML, or an empty string.
char * render_page_html_dup_runtime(const char *app_root, const char *base_path, const char *layout_id, const char *title, const char *body, const char *current_lang, const char *current_request_target, int has_user, int managed_business_count)
Builds the nav context and renders a full layout page.
yyjson_val * load_root_from_text_runtime(const char *text, yyjson_doc **doc_out)
Parses JSON text into a yyjson_val root.
int write_text_file(const char *path, const char *content)
Writes a text buffer to a file path.
Definition native_fs.c:66
Declares native filesystem helper routines.
unsigned char * read_file_bytes_dup_runtime(const char *path, size_t *out_size)
Reads the full content of a binary file into a heap-allocated buffer.
void native_send_text_response(NativeConnection *connection, int status, const char *status_text, const char *body)
Sends a plain-text HTTP response with explicit content length.
int native_connection_printf(NativeConnection *connection, const char *format,...)
Writes formatted bytes to a native connection.
Declares native HTTP response writing helpers.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
char * dup_range(const char *start, size_t len)
Duplicates a byte range into a newly allocated NUL-terminated string.
int streq(const char *a, const char *b)
Tests whether two strings are exactly equal.
char * read_file_text(const char *path)
Reads an entire file into a newly allocated text buffer.
int path_exists(const char *path)
Tests whether a filesystem path currently exists.
Declares shared low-level support helpers used across the Pharos native runtime.
yyjson_doc * native_json_doc_load_file(const char *path)
Parses a JSON file into an owned yyjson document.
long native_json_obj_get_long_default(yyjson_val *obj, const char *key, long default_value, int *found_out)
Reads a numeric object member or returns a fallback value.
void native_json_doc_free(yyjson_doc *doc)
Releases a document returned by the native yyjson helpers.
char * native_json_obj_get_string_dup(yyjson_val *obj, const char *key)
Duplicates a string-valued key from a JSON object.
yyjson_val * native_json_obj_get_array(yyjson_val *obj, const char *key)
Looks up an array-valued key from a JSON object value.
yyjson_val * native_json_obj_get(yyjson_val *obj, const char *key)
Looks up a key from a JSON object value.
Declares shared helper wrappers around vendored yyjson parsing and serialization APIs.
Loaded runtime metadata for one Pharos app artifact.
Parsed HTTP request fields extracted from a native connection.
Accepted client connection and its resolved transport metadata.
Carries request-local relational bridge state for one native request.
Describes one matched route from the route fixture.
void yyjson_mut_doc_free(yyjson_mut_doc *doc)
Definition yyjson.c:2653
yyjson_mut_doc * yyjson_mut_doc_new(const yyjson_alc *alc)
Definition yyjson.c:2663
yyjson_mut_doc * yyjson_doc_mut_copy(const yyjson_doc *doc, const yyjson_alc *alc)
Definition yyjson.c:2678
yyjson_mut_val * yyjson_val_mut_copy(yyjson_mut_doc *m_doc, const yyjson_val *i_vals)
Definition yyjson.c:2714
yyjson_api_inline bool yyjson_is_str(const yyjson_val *val)
Definition yyjson.h:5390
yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val)
Definition yyjson.h:5398
#define yyjson_mut_arr_foreach(arr, idx, max, val)
Definition yyjson.h:2987
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr_remove(yyjson_mut_val *arr, size_t idx)
Definition yyjson.h:6687
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj_get(const yyjson_mut_val *obj, const char *key)
Definition yyjson.h:6970
yyjson_api_inline int64_t yyjson_get_sint(const yyjson_val *val)
Definition yyjson.h:5453
yyjson_api_inline yyjson_val * yyjson_arr_get(const yyjson_val *arr, size_t idx)
Definition yyjson.h:5603
yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, int64_t val)
Definition yyjson.h:7425
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
Definition yyjson.h:5323
yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val)
Definition yyjson.h:5394
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj(yyjson_mut_doc *doc)
Definition yyjson.h:7086
yyjson_api_inline yyjson_mut_val * yyjson_mut_null(yyjson_mut_doc *doc)
Definition yyjson.h:6224
yyjson_api_inline bool yyjson_is_int(const yyjson_val *val)
Definition yyjson.h:5378
yyjson_api_inline bool yyjson_is_real(const yyjson_val *val)
Definition yyjson.h:5382
yyjson_api_inline char * yyjson_mut_write(const yyjson_mut_doc *doc, yyjson_write_flag flg, size_t *len)
Definition yyjson.h:1604
yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, yyjson_mut_val *root)
Definition yyjson.h:5919
yyjson_api_inline yyjson_mut_val * yyjson_mut_int(yyjson_mut_doc *doc, int64_t num)
Definition yyjson.h:6251
yyjson_api_inline yyjson_mut_val * yyjson_mut_strcpy(yyjson_mut_doc *doc, const char *str)
Definition yyjson.h:6282
yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, const char *str)
Definition yyjson.h:5477
yyjson_api_inline bool yyjson_mut_is_obj(const yyjson_mut_val *val)
Definition yyjson.h:5978
yyjson_api_inline yyjson_mut_val * yyjson_mut_doc_get_root(yyjson_mut_doc *doc)
Definition yyjson.h:5915
yyjson_api_inline bool yyjson_is_uint(const yyjson_val *val)
Definition yyjson.h:5370
yyjson_api_inline yyjson_val * yyjson_arr_iter_next(yyjson_arr_iter *iter)
Definition yyjson.h:5672
yyjson_api_inline bool yyjson_is_null(const yyjson_val *val)
Definition yyjson.h:5354
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr_get(const yyjson_mut_val *arr, size_t idx)
Definition yyjson.h:6318
yyjson_api_inline size_t yyjson_get_len(const yyjson_val *val)
Definition yyjson.h:5473
yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, bool val)
Definition yyjson.h:7404
yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *val)
Definition yyjson.h:7479
const char size_t len
Definition yyjson.h:8364
yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr)
Definition yyjson.h:5599
yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, yyjson_mut_val *key, yyjson_mut_val *val)
Definition yyjson.h:7259
yyjson_api_inline yyjson_mut_val * yyjson_mut_sint(yyjson_mut_doc *doc, int64_t num)
Definition yyjson.h:6246
yyjson_api_inline char * yyjson_write(const yyjson_doc *doc, yyjson_write_flag flg, size_t *len)
Definition yyjson.h:1471
static const yyjson_write_flag YYJSON_WRITE_NOFLAG
Definition yyjson.h:1245
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
Definition yyjson.h:5469
yyjson_api_inline bool yyjson_mut_is_arr(const yyjson_mut_val *val)
Definition yyjson.h:5974
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr(yyjson_mut_doc *doc)
Definition yyjson.h:6410
yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, yyjson_mut_val *val)
Definition yyjson.h:6621
yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter)
Definition yyjson.h:5650
yyjson_api_inline bool yyjson_get_bool(const yyjson_val *val)
Definition yyjson.h:5445