48#define DYNAMIC_HOST_PROFILE "dynamic-app"
51#define MAX_QUERY_LEN 4096
54#define CSRF_COOKIE_NAME "pharos_csrf"
57#define SESSION_COOKIE_NAME "pharos_session"
60#define CSRF_TOKEN_HEX_LEN 48
63#define CSRF_COOKIE_MAX_AGE "7200"
109 const char *business_slug,
111 size_t *row_html_length,
112 size_t *row_html_capacity
116 char **form_fields_html,
117 size_t *form_fields_length,
118 size_t *form_fields_capacity,
119 const char *day_options_html
128 const char *filter_controls_html
149 const char *resource_collection_name,
157 char **subscriptions_block,
158 size_t *subscriptions_length,
159 size_t *subscriptions_capacity
183 const char *request_path,
184 const char *query_string,
190 const char *request_path
225 const char *request_path,
226 const char *body_params_json
241 const char *status_text,
242 const char *error_kind,
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"
253 if (written < 0 || (
size_t)written >=
sizeof(body)) {
260 "Content-Type: application/json\r\n"
261 "Content-Length: %d\r\n"
262 "Connection: close\r\n"
281 static const char hex[] =
"0123456789abcdef";
283 unsigned char raw[48];
284 if (byte_count >
sizeof(raw)) {
285 byte_count =
sizeof(raw);
288 int fd = open(
"/dev/urandom", O_RDONLY);
291 while (got < byte_count) {
292 ssize_t n = read(fd, raw + got, byte_count - got);
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];
303 buf[byte_count * 2] =
'\0';
328 if (existing != NULL && existing[0] !=
'\0') {
343 int has_session = session_token != NULL && session_token[0] !=
'\0';
350 const char *cursor = headers;
351 if (headers == NULL || header_name == NULL || header_name[0] ==
'\0') {
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')) {
364 while (value_end > value_start && (value_end[-1] ==
' ' || value_end[-1] ==
'\t' || value_end[-1] ==
'\r')) {
367 return dup_range(value_start, (
size_t)(value_end - value_start));
369 cursor = line_end != NULL ? line_end + 1 : cursor + line_len;
376 if (token != NULL && token[0] !=
'\0') {
386 int valid = cookie_token != NULL && request_token != NULL && cookie_token[0] !=
'\0' && request_token[0] !=
'\0' &&
streq(cookie_token, request_token);
393 static const struct {
397 {
"pay_on_site",
"Pay on site"},
398 {
"credit_card",
"Credit card"}
401 size_t capacity = 256;
405 html = (
char *)malloc(capacity);
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(
416 "<option value=\"%s\"%s>%s</option>",
417 options[index].value,
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);
433 capacity = new_capacity;
437 "<option value=\"%s\"%s>%s</option>",
438 options[index].value,
442 if (written < 0 || (
size_t)written >= capacity - length) {
447 length += (size_t)written;
454 if (request == NULL) {
457 if (request->
body != NULL) {
458 return strdup(request->
body);
462 if (body_text != NULL) {
470 if (out_size != NULL) {
473 if (request == NULL) {
479 if (request->
body != NULL) {
480 unsigned char *copy = (
unsigned char *)malloc(request->
body_len + 1);
486 if (out_size != NULL) {
495 if (field_name == NULL || field_name[0] ==
'\0') {
502 if (body_bytes != NULL) {
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);
527 const char *base_path = app != NULL ? app->
base_path : NULL;
528 if (full_path == NULL) {
531 if (base_path == NULL || base_path[0] ==
'\0' ||
streq(base_path,
"/")) {
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') {
543 if (*suffix ==
'/') {
544 trimmed = strdup(suffix);
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\"}]}"
563 char *seed_json = NULL;
564 if (doc_out != NULL) {
567 if (state_path == NULL || state_path[0] ==
'\0') {
574 if (doc_out != NULL) {
581 if (seed_json == NULL ||
write_text_file(state_path, seed_json) != 0) {
593 if (doc_out != NULL) {
611 char *title_html = NULL;
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>",
622 done ?
"Reopen" :
"Done",
633 return strdup(
"<li>No tasks yet.</li>");
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);
657 free(task_items_html);
667 if (connection == NULL) {
680 body_len = (int)strlen(body);
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"
699 if (buffer == NULL || length == NULL || capacity == NULL || text == NULL) {
702 text_len = strlen(text);
703 if (*buffer == NULL) {
704 *capacity = text_len + 64;
705 *buffer = (
char *)malloc(*capacity);
706 if (*buffer == NULL) {
713 if (*length + text_len + 1 > *capacity) {
714 *capacity = (*length + text_len + 1) * 2;
715 grown = (
char *)realloc(*buffer, *capacity);
721 memcpy(*buffer + *length, text, text_len);
723 (*buffer)[*length] =
'\0';
733 if (buffer == NULL || length == NULL || capacity == NULL || format == NULL) {
736 va_start(args, format);
737 va_copy(args_copy, args);
738 needed = vsnprintf(NULL, 0, format, args_copy);
744 chunk = (
char *)malloc((
size_t)needed + 1);
749 vsnprintf(chunk, (
size_t)needed + 1, format, args);
757 static const char *days[] = {
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"};
762 for (index = 0; index <
sizeof(days) /
sizeof(days[0]); index++) {
764 snprintf(option,
sizeof(option),
"<option value=\"%s\"%s>%s</option>",
766 (selected_day != NULL &&
streq(selected_day, days[index])) ?
" selected" :
"",
773 return html != NULL ? html : strdup(
"");
788 "Generated Service Admin",
789 "Framework-owned generated admin surface for business services.",
791 "<option value=\"sort_order_asc\" selected>Display order</option><option value=\"name_asc\">Name</option><option value=\"duration_desc\">Longest duration</option>",
793 "<th>Display order</th><th>Name</th><th>Cost</th><th>Duration</th>",
796 "<tr><td colspan=\"5\">No services found.</td></tr>",
801 "business_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>",
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>"
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>",
823 "<th>Name</th><th>Email</th><th>Status</th>",
826 "<tr><td colspan=\"4\">No technicians found.</td></tr>",
831 if (surface_kind == NULL || surface_kind[0] ==
'\0') {
834 for (index = 0; index <
sizeof(descriptors) /
sizeof(descriptors[0]); index++) {
835 if (
streq(surface_kind, descriptors[index].surface_kind)) {
836 return &descriptors[index];
858 const char *business_slug,
860 size_t *row_html_length,
861 size_t *row_html_capacity
866 if (descriptor == NULL || root == NULL || business_slug == NULL || row_html == NULL || row_html_length == NULL || row_html_capacity == NULL) {
872 long row_business_id = 0;
877 if (row_business_id != business_id) {
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>",
892 name != NULL ? name :
"",
893 cost != NULL ? cost :
"",
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 :
"",
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",
953 if (*row_html == NULL) {
971 char **form_fields_html,
972 size_t *form_fields_length,
973 size_t *form_fields_capacity,
974 const char *day_options_html
976 if (descriptor == NULL || form_fields_html == NULL || form_fields_length == NULL || form_fields_capacity == NULL) {
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>"
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 :
""
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>"
1041 const char *filter_controls_html
1043 if (doc == NULL || page == NULL || descriptor == NULL) {
1069 const char *resource_collection_name,
1076 if (root == NULL || resource_collection_name == NULL || resource_collection_name[0] ==
'\0' || resource_id <= 0 || display == NULL) {
1079 memset(display, 0,
sizeof(*display));
1090 if (row_id != resource_id) {
1106 if (display == NULL) {
1128 char **subscriptions_block,
1129 size_t *subscriptions_length,
1130 size_t *subscriptions_capacity
1135 if (root == NULL || user_id <= 0 || subscriptions_block == NULL || subscriptions_length == NULL || subscriptions_capacity == NULL) {
1143 long row_user_id = 0;
1144 long business_id = 0;
1150 if (row_user_id != user_id) {
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>",
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;
1190 &subscriptions_block,
1191 &subscriptions_length,
1192 &subscriptions_capacity)) {
1193 free(subscriptions_block);
1196 if (subscriptions_block == NULL) {
1197 return strdup(
"<p class=\"muted\">You are not subscribed to any businesses yet.</p>");
1203 free(subscriptions_block);
1206 free(subscriptions_block);
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) {
1232 if (descriptor == NULL) {
1238 if (business == NULL) {
1246 if (doc == NULL || page == NULL || error_html == NULL) {
1255 if (day_options_html == NULL) {
1256 goto generated_admin_manage_cleanup;
1266 &row_html_capacity)) {
1267 goto generated_admin_manage_cleanup;
1272 &form_fields_length,
1273 &form_fields_capacity,
1274 day_options_html)) {
1275 goto generated_admin_manage_cleanup;
1279 goto generated_admin_manage_cleanup;
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);
1301generated_admin_manage_cleanup:
1304 free(form_fields_html);
1305 free(filter_controls_html);
1306 free(day_options_html);
1327 char *subscriptions_block = NULL;
1328 char *full_name = NULL;
1330 char *phone_number = NULL;
1331 char *address = NULL;
1334 char *result = NULL;
1336 if (state_path == NULL || user_id <= 0) {
1342 if (root == NULL || user == NULL) {
1348 if (subscriptions_block == NULL) {
1363 if (doc == NULL || page == NULL
1368 || !
yyjson_mut_obj_add_strcpy(doc, page,
"payment_preference_options_html", payment_preference_options_html != NULL ? payment_preference_options_html :
"")
1370 || !
yyjson_mut_obj_add_strcpy(doc, page,
"subscriptions_block", subscriptions_block != NULL ? subscriptions_block :
"")) {
1375 free(subscriptions_block);
1386 free(subscriptions_block);
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) {
1414 if (business == NULL) {
1432 if (day == NULL && start == NULL && end == 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);
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);
1459 free(day); free(start); free(end); free(day_options_html); free(error_html);
1475 static const char *prefix =
"/api/businesses/";
1476 static const char *suffix =
"/services/";
1477 const char *slug_start;
1478 const char *suffix_start;
1484 if (request_path == NULL) {
1487 prefix_len = strlen(prefix);
1488 suffix_len = strlen(suffix);
1489 path_len = strlen(request_path);
1490 if (path_len <= prefix_len + suffix_len) {
1493 if (strncmp(request_path, prefix, prefix_len) != 0) {
1496 suffix_start = request_path + (path_len - suffix_len);
1497 if (strcmp(suffix_start, suffix) != 0) {
1500 slug_start = request_path + prefix_len;
1501 slug_len = (size_t)(suffix_start - slug_start);
1502 if (slug_len == 0) {
1505 return memchr(slug_start,
'/', slug_len) == NULL ? 1 : 0;
1517 static const char *prefix =
"/api/businesses/";
1518 static const char *suffix =
"/availability/";
1519 const char *slug_start;
1520 const char *suffix_start;
1526 if (request_path == NULL) {
1529 prefix_len = strlen(prefix);
1530 suffix_len = strlen(suffix);
1531 path_len = strlen(request_path);
1532 if (path_len <= prefix_len + suffix_len) {
1535 if (strncmp(request_path, prefix, prefix_len) != 0) {
1538 suffix_start = request_path + (path_len - suffix_len);
1539 if (strcmp(suffix_start, suffix) != 0) {
1542 slug_start = request_path + prefix_len;
1543 slug_len = (size_t)(suffix_start - slug_start);
1544 if (slug_len == 0) {
1547 return memchr(slug_start,
'/', slug_len) == NULL ? 1 : 0;
1556 static const char *prefix =
"/api/businesses/";
1557 static const char *suffix =
"/services/schema/";
1558 const char *slug_start;
1559 const char *suffix_start;
1565 if (request_path == NULL) {
1568 prefix_len = strlen(prefix);
1569 suffix_len = strlen(suffix);
1570 path_len = strlen(request_path);
1571 if (path_len <= prefix_len + suffix_len) {
1574 if (strncmp(request_path, prefix, prefix_len) != 0) {
1577 suffix_start = request_path + (path_len - suffix_len);
1578 if (strcmp(suffix_start, suffix) != 0) {
1581 slug_start = request_path + prefix_len;
1582 slug_len = (size_t)(suffix_start - slug_start);
1583 if (slug_len == 0) {
1586 return memchr(slug_start,
'/', slug_len) == NULL ? 1 : 0;
1595 static const char *prefix =
"/api/businesses/";
1596 static const char *suffix =
"/availability/schema/";
1597 const char *slug_start;
1598 const char *suffix_start;
1604 if (request_path == NULL) {
1607 prefix_len = strlen(prefix);
1608 suffix_len = strlen(suffix);
1609 path_len = strlen(request_path);
1610 if (path_len <= prefix_len + suffix_len) {
1613 if (strncmp(request_path, prefix, prefix_len) != 0) {
1616 suffix_start = request_path + (path_len - suffix_len);
1617 if (strcmp(suffix_start, suffix) != 0) {
1620 slug_start = request_path + prefix_len;
1621 slug_len = (size_t)(suffix_start - slug_start);
1622 if (slug_len == 0) {
1625 return memchr(slug_start,
'/', slug_len) == NULL ? 1 : 0;
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")
1650 if (minutes_out != NULL) {
1653 if (time_text == NULL || strlen(time_text) != 5 || time_text[2] !=
':') {
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') {
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) {
1665 if (minutes_out != NULL) {
1666 *minutes_out = hour * 60 + minute;
1673 char *title_mode = NULL;
1674 if (page_spec_root != NULL) {
1678 if ((title == NULL || title[0] ==
'\0') && title_mode != NULL && title_mode[0] !=
'\0' && body_params_root != NULL) {
1683 if (title == NULL) {
1691 const char *state_path,
1692 const char *business_slug
1704 long business_id = 0;
1707 if (connection == NULL || state_path == NULL || business_slug == NULL) {
1713 if (business == NULL) {
1714 write_json_error(connection, 404,
"Not Found",
"not_found",
"Business not found.");
1723 if (out_doc == NULL || out_root == NULL || items == NULL) {
1737 if (entry != NULL) {
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"
1770 const char *state_path,
1771 const char *business_slug
1783 long business_id = 0;
1786 if (connection == NULL || state_path == NULL || business_slug == NULL) {
1792 if (business == NULL) {
1793 write_json_error(connection, 404,
"Not Found",
"not_found",
"Business not found.");
1802 if (out_doc == NULL || out_root == NULL || items == NULL) {
1816 if (entry != NULL) {
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"
1891 const char *state_path,
1892 const char *business_slug,
1893 const char *surface_id
1898 char *fixture_path = NULL;
1908 if (connection == NULL || app == NULL || state_path == NULL || business_slug == NULL || surface_id == NULL) {
1914 if (business == NULL) {
1915 write_json_error(connection, 404,
"Not Found",
"not_found",
"Business not found.");
1932 if (surface == NULL) {
1936 write_json_error(connection, 404,
"Not Found",
"not_found",
"Generated surface schema not found.");
1941 if (out_doc == NULL || out_surface == NULL) {
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"
1981 char *params_json = NULL;
1988 if (doc == NULL || root == NULL
1989 || error_html == NULL || next_input == NULL
2010 char *params_json = NULL;
2014 const char *account_type_options_html =
2015 "<option value=\"business_customer\" selected>Customer</option>"
2016 "<option value=\"business_owner\">Business owner</option>";
2020 if (doc == NULL || root == NULL
2021 || error_html == NULL || next_input == NULL
2060 if (doc == NULL || root == NULL || key == NULL || key[0] ==
'\0') {
2081 char *escaped_value = NULL;
2083 if (doc == NULL || root == NULL || key == NULL || key[0] ==
'\0') {
2087 if (escaped_value == NULL) {
2091 free(escaped_value);
2103 if (doc == NULL || root == NULL) {
2118 char *preview_html = NULL;
2119 if (preview_path == NULL || preview_path[0] ==
'\0') {
2122 needed = strlen(preview_path) * 2 + 128;
2123 preview_html = (
char *)malloc(needed);
2124 if (preview_html == NULL) {
2130 "<p>Local runtime preview link:</p><p><a href=\"%s\"><code>%s</code></a></p>",
2134 return preview_html;
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') {
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>")
2156 free(escaped_target_business_name);
2160 free(escaped_target_business_name);
2168 char *params_json = NULL;
2169 if (doc == NULL || root == NULL || error_html == NULL
2184 char *params_json = NULL;
2186 if (doc == NULL || root == NULL || preview_html == NULL
2208 char *params_json = NULL;
2209 if (doc == NULL || root == NULL || error_html == NULL
2233 char *params_json = NULL;
2234 if (doc == NULL || root == NULL
2261 char *params_json = NULL;
2262 if (doc == NULL || root == NULL || error_html == NULL || target_html == NULL
2289 char *params_json = NULL;
2290 if (doc == NULL || root == 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;
2331 if (state_path == NULL || business_slug == NULL || business_slug[0] ==
'\0' || service_id <= 0) {
2339 if (business == NULL || service == NULL) {
2350 if (service_duration <= 0) service_duration = 45;
2351 if (service_sort_order <= 0) service_sort_order = service_id;
2361 if (service_count <= 0) service_count = 1;
2362 for (
long index = 1; index <= service_count; index++) {
2364 "<option value=\"%ld\"%s>%ld</option>",
2366 index == service_sort_order ?
" selected" :
"",
2368 goto service_edit_cleanup;
2374 long option_index = 0;
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 :
"")) {
2386 goto service_edit_cleanup;
2392 if (option_index == 0) {
2394 "<input type=\"hidden\" name=\"booking_option_count\" value=\"0\">")) {
2395 goto service_edit_cleanup;
2398 char *wrapped_html = NULL;
2399 size_t wrapped_len = 0;
2400 size_t wrapped_cap = 0;
2402 "<input type=\"hidden\" name=\"booking_option_count\" value=\"%ld\"><div class=\"stack\">%s</div>",
2404 option_rows_html != NULL ? option_rows_html :
"")) {
2406 goto service_edit_cleanup;
2408 free(option_rows_html);
2409 option_rows_html = wrapped_html;
2410 option_rows_len = wrapped_len;
2411 option_rows_cap = wrapped_cap;
2419 long technician_id = 0;
2431 long assigned_id = 0;
2439 "<label style=\"display:flex;gap:0.5rem;align-items:center;\"><input type=\"checkbox\" name=\"technician_%ld\" value=\"%ld\"%s> %s</label>",
2442 checked ?
" checked" :
"",
2443 name != NULL ? name :
"")) {
2445 goto service_edit_cleanup;
2447 technician_emitted = 1;
2451 if (!technician_emitted) {
2453 "<p>No technicians available for this business yet.</p>")) {
2454 goto service_edit_cleanup;
2460 if (doc == NULL || page == NULL) {
2461 goto service_edit_cleanup;
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");
2475 snprintf(visual_html,
sizeof(visual_html),
"<div class=\"card\">No image uploaded.</div>");
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;
2492service_edit_cleanup:
2494 free(service_description);
2496 free(service_image);
2497 free(sort_options_html);
2498 free(option_rows_html);
2499 free(technician_boxes_html);
2509 unsigned char diff = 0;
2510 if (left == NULL || right == NULL) {
2513 left_len = strlen(left);
2514 right_len = strlen(right);
2515 if (left_len != right_len) {
2518 for (index = 0; index < left_len; index++) {
2519 diff |= (
unsigned char)(left[index] ^ right[index]);
2524#define SHA256_BLOCK_SIZE 64
2525#define SHA256_DIGEST_SIZE 32
2535 return (value >> bits) | (value << (32U - bits));
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
2549 uint32_t a, b, c, d, e, f, g, h;
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]);
2559 for (index = 16; index < 64; index++) {
2562 m[index] = m[index - 16] + s0 + m[index - 7] + s1;
2566 for (index = 0; index < 64; index++) {
2568 uint32_t ch = (e & f) ^ ((~e) & g);
2569 t1 = h + s1 + ch + k[index] + m[index];
2572 uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
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;
2603 for (index = 0; index <
len; index++) {
2614 uint32_t index = ctx->
datalen;
2616 ctx->
data[index++] = 0x80;
2617 while (index < 56) ctx->
data[index++] = 0x00;
2619 ctx->
data[index++] = 0x80;
2620 while (index < 64) ctx->
data[index++] = 0x00;
2622 memset(ctx->
data, 0, 56);
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);
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);
2647 static const char hex_chars[] =
"0123456789abcdef";
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];
2653 hex_out[
len * 2] =
'\0';
2663 memset(key_block, 0,
sizeof(key_block));
2668 }
else if (key_len > 0) {
2669 memcpy(key_block, key, key_len);
2672 ipad[index] = (uint8_t)(key_block[index] ^ 0x36U);
2673 opad[index] = (uint8_t)(key_block[index] ^ 0x5cU);
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) {
2691 while (produced < out_len) {
2692 uint8_t salt_block[512];
2697 if (salt_len + 4 >
sizeof(salt_block)) {
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);
2706 memcpy(t, u,
sizeof(t));
2707 for (iter = 1; iter < iterations; iter++) {
2710 for (index = 0; index <
sizeof(t); index++) {
2711 t[index] ^= u[index];
2714 copy_len = (out_len - produced) <
sizeof(t) ? (out_len - produced) :
sizeof(t);
2715 memcpy(out + produced, t, copy_len);
2716 produced += copy_len;
2723 if (stored_hash == NULL || password_value == NULL) {
2726 if (strncmp(stored_hash,
"pharos-pbkdf2-sha256$", strlen(
"pharos-pbkdf2-sha256$")) == 0) {
2727 char *copy = strdup(stored_hash);
2728 char *saveptr = NULL;
2730 char *iterations_text;
2735 uint32_t iterations;
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))) {
2754 if (strncmp(stored_hash,
"dev$", 4) == 0) {
2755 char *copy = strdup(stored_hash);
2756 char *saveptr = NULL;
2758 char *legacy_username;
2759 char *legacy_password;
2764 prefix = strtok_r(copy,
"$", &saveptr);
2765 legacy_username = strtok_r(NULL,
"$", &saveptr);
2766 legacy_password = saveptr;
2768 if (legacy_password != NULL) {
2769 if (username_value != NULL && username_value[0] !=
'\0' && legacy_username != NULL && !
streq(legacy_username, username_value)) {
2785 if (value == NULL) {
2800 char *sha_hex = NULL;
2803 if (token_value == NULL || token_value[0] ==
'\0') {
2807 if (sha_hex == NULL) {
2810 needed = strlen(
"pharos-sha256$") + strlen(sha_hex) + 1;
2811 out = (
char *)malloc(needed);
2813 snprintf(out, needed,
"pharos-sha256$%s", sha_hex);
2820 const uint32_t iterations = 310000;
2826 if (password_value == NULL || password_value[0] ==
'\0') {
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))) {
2834 needed = strlen(
"pharos-pbkdf2-sha256$") + 10 + 1 + strlen(salt_text) + 1 + strlen(derived_hex) + 1;
2835 out = (
char *)malloc(needed);
2837 snprintf(out, needed,
"pharos-pbkdf2-sha256$%u$%s$%s", iterations, salt_text, derived_hex);
2843 time_t now = time(NULL);
2846 if (gmtime_s(&tm_value, &now) != 0)
return 0;
2848 if (gmtime_r(&now, &tm_value) == NULL)
return 0;
2850 return strftime(out, 21,
"%Y-%m-%dT%H:%M:%SZ", &tm_value) == 20;
2854 time_t now = time(NULL) + (time_t)delta_seconds;
2857 if (gmtime_s(&tm_value, &now) != 0)
return 0;
2859 if (gmtime_r(&now, &tm_value) == NULL)
return 0;
2861 return strftime(out, 21,
"%Y-%m-%dT%H:%M:%SZ", &tm_value) == 20;
2866 return value != NULL &&
yyjson_is_arr(value) ? value : NULL;
2939 if (out_value != NULL) {
2942 if (value == NULL) {
2946 if (out_value != NULL) {
2955 long actual_value = 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) {
3003 if (year_out != NULL) *year_out = year;
3004 if (month_out != NULL) *month_out = month;
3005 if (day_out != NULL) *day_out = day;
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;
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;
3023 weekday = (adjusted_year + adjusted_year / 4 - adjusted_year / 100 + adjusted_year / 400 + offsets[month - 1] + day) % 7;
3024 if (weekday < 0 || weekday > 6) {
3027 return names[weekday];
3039 const char *day_name = NULL;
3044 slot_end = slot_start + (int)duration_minutes;
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;
3059 matches = row_day != NULL && day_name != NULL &&
streq(row_day, day_name)
3062 && slot_start >= start_minutes && slot_end <= end_minutes;
3084 char *existing_date = NULL;
3085 char *existing_time = NULL;
3086 char *existing_status = NULL;
3087 long service_id = 0;
3089 int existing_start = 0;
3090 int existing_duration = 45;
3098 if (existing_status != NULL &&
streq(existing_status,
"cancelled")) {
3099 free(existing_date);
3100 free(existing_time);
3101 free(existing_status);
3106 if (service != NULL) {
3109 matches = existing_date != NULL &&
streq(existing_date, date_value != NULL ? date_value :
"")
3112 free(existing_date);
3113 free(existing_time);
3114 free(existing_status);
3134 int duration_minutes = 45;
3135 const char *day_name = NULL;
3136 if (service != NULL) {
3139 if (duration_minutes <= 0) {
3140 duration_minutes = 45;
3146 slot_end = slot_start + duration_minutes;
3149 long technician_id = 0;
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;
3169 available = row_day != NULL && day_name != NULL &&
streq(row_day, day_name)
3172 && slot_start >= start_minutes && slot_end <= end_minutes;
3177 return technician_id;
3193 if (existing_id >= next_id) {
3194 next_id = existing_id + 1;
3238 if (memberships == NULL || user_id <= 0 || tenant_id <= 0 || !
yyjson_arr_iter_init(memberships, &iter)) {
3242 const char *membership_status = NULL;
3251 if (membership_status == NULL || membership_status[0] ==
'\0' ||
streq(membership_status,
"active")) {
3266 if (role_hint != NULL && role_hint[0] !=
'\0') {
3267 if (
streq(role_hint,
"owner") ||
streq(role_hint,
"business_owner")) {
3268 return "business_owner";
3270 if (
streq(role_hint,
"manager") ||
streq(role_hint,
"business_manager")) {
3271 return "business_manager";
3273 if (
streq(role_hint,
"technician") ||
streq(role_hint,
"business_technician")) {
3274 return "business_technician";
3276 if (
streq(role_hint,
"customer") ||
streq(role_hint,
"business_customer")) {
3277 return "business_customer";
3280 if (account_type != NULL && account_type[0] !=
'\0') {
3281 if (
streq(account_type,
"business_owner")) {
3282 return "business_owner";
3284 if (
streq(account_type,
"business_manager")) {
3285 return "business_manager";
3287 if (
streq(account_type,
"business_technician")) {
3288 return "business_technician";
3291 return "business_customer";
3305 char *backend = NULL;
3306 char *export_json = NULL;
3307 if (app == NULL || app->
state_path == NULL || state_json_text == NULL) {
3316 if (backend != NULL) {
3323 if (export_json != NULL) {
3343 char created_at[21];
3353 events = mut_root != NULL ?
yyjson_mut_obj_get(mut_root,
"rate_limit_events") : NULL;
3368 if (event_obj == NULL
3395 char *session_token = NULL;
3396 char *session_token_hash = NULL;
3403 if (session_token == NULL || session_token[0] ==
'\0') {
3404 free(session_token);
3408 free(session_token);
3409 if (session_token_hash == NULL) {
3418 char *expires_at = NULL;
3419 char *revoked_at = NULL;
3425 if ((revoked_at == NULL || revoked_at[0] ==
'\0') && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
3435 free(session_token_hash);
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;
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) {
3459 snprintf(header, needed,
"Set-Cookie: %s=%s; Path=%s%s; Max-Age=%s; HttpOnly; SameSite=%s%s",
3462 path_suffix != NULL ? path_suffix : base_path,
3463 path_suffix != NULL ?
"" :
"/",
3466 secure ?
"; Secure" :
"");
3472 if (account_type != NULL && (
streq(account_type,
"business_owner") ||
streq(account_type,
"business_manager") ||
streq(account_type,
"business_technician"))) {
3473 return "/businesses/";
3484 char *session_token = NULL;
3485 char *session_token_hash = NULL;
3486 char revoked_at[21];
3494 if (session_token == NULL || session_token[0] ==
'\0') {
3495 free(session_token);
3499 free(session_token);
3500 if (session_token_hash == NULL) {
3515 free(session_token_hash);
3524 free(session_token_hash);
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];
3554 if (session_token_out != NULL) *session_token_out = NULL;
3555 if (session_cookie_header_out != NULL) *session_cookie_header_out = NULL;
3560 if (session_token == NULL) {
3565 if (session_token_hash == NULL) {
3566 free(session_token);
3574 free(session_token_hash);
3575 free(session_token);
3586 if (existing_id >= session_id) session_id = existing_id + 1;
3592 free(session_token_hash);
3593 free(session_token);
3600 if (session_obj == NULL
3612 free(session_token_hash);
3613 free(session_token);
3621 free(session_token_hash);
3622 free(session_token);
3628 free(session_token_hash);
3629 if (session_cookie_header_out != NULL) {
3632 if (session_token_out != NULL) {
3633 *session_token_out = session_token;
3635 free(session_token);
3659 char input_tag[256];
3662 if (html == NULL || csrf_token == NULL) {
3663 return html != NULL ? strdup(html) : NULL;
3666 input_len = snprintf(input_tag,
sizeof(input_tag),
3667 "<input type=\"hidden\" name=\"csrf_token\" value=\"%s\" />",
3669 if (input_len < 0 || (
size_t)input_len >=
sizeof(input_tag)) {
3670 return strdup(html);
3673 result_cap = strlen(html) + (size_t)input_len + 64;
3674 result = (
char *)malloc(result_cap);
3675 if (result == NULL) {
3676 return strdup(html);
3681 while (*scan !=
'\0') {
3682 const char *form_start = NULL;
3683 const char *form_open_end = NULL;
3684 const char *lower_check;
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) {
3693 return strdup(html);
3696 result_cap = new_cap;
3698 result[result_len++] = *scan++;
3700 if (*scan ==
'\0') {
3705 lower_check = scan + 1;
3706 while (*lower_check ==
' ') {
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 !=
'>') {
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')) {
3726 if (equals < form_open_end && *equals ==
'=') {
3727 const char *value = equals + 1;
3728 while (value < form_open_end && (*value ==
' ' || *value ==
'\t' || *value ==
'\n')) {
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] ==
'>')) {
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) {
3754 return strdup(html);
3757 result_cap = new_cap;
3759 memcpy(result + result_len, form_start, tag_len);
3760 result_len += tag_len;
3762 memcpy(result + result_len, input_tag, (
size_t)input_len);
3763 result_len += (size_t)input_len;
3765 scan = form_open_end + 1;
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) {
3775 return strdup(html);
3778 result_cap = new_cap;
3780 result[result_len++] = *scan++;
3782 result[result_len] =
'\0';
3798 const char *status_text,
3800 const char *csrf_token,
3801 const char *extra_headers
3809 body_len = (int)strlen(html);
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"
3824 csrf_token != NULL ? csrf_token :
"",
3826 extra_headers != NULL ? extra_headers :
"",
3834 const char *status_text,
3836 const char *csrf_token
3845 const char *page_id,
3846 const char *request_path,
3847 const char *body_params_json,
3848 const char *extra_headers
3851 char *page_spec_rel = NULL;
3852 char *page_spec_path = NULL;
3853 char *page_spec_text = NULL;
3856 char *template_file = NULL;
3857 char *layout_id = NULL;
3858 char *body_template_path = NULL;
3861 char *body_html = NULL;
3862 char *full_html = NULL;
3863 char *injected_html = NULL;
3865 char *csrf_token = NULL;
3868 if (connection == NULL || app == NULL || app->
app_root == NULL || page_id == NULL || body_params_json == NULL) {
3873 page_spec_text = page_spec_path != NULL ?
read_file_text(page_spec_path) : NULL;
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;
3884 if (body_html == NULL) {
3885 goto render_declared_cleanup;
3894 request_path != NULL ? request_path : (request != NULL ? request->
target :
"/"),
3898 if (full_html == NULL) {
3899 goto render_declared_cleanup;
3906render_declared_cleanup:
3907 free(page_spec_rel);
3908 free(page_spec_path);
3909 free(page_spec_text);
3911 free(template_file);
3913 free(body_template_path);
3917 free(injected_html);
3927 const char *page_id,
3928 const char *request_path,
3929 const char *body_params_json
3945 if (target == NULL || dest_size == 0) {
3949 qmark = strchr(target,
'?');
3950 if (qmark != NULL && *(qmark + 1) !=
'\0') {
3952 snprintf(dest, dest_size,
"%s", qmark);
3969 const char *request_path
3971 char *invite_token = NULL;
3972 char *token_hash = NULL;
3979 char *body_params_json = NULL;
3981 if (app == NULL || match == NULL || request_path == NULL) {
3993 char *expires_at = NULL;
3999 && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
4002 long business_id = 0;
4004 char *target_business_name = NULL;
4005 if (tenant_id > 0) {
4018 if (business_id > 0) {
4023 invite_kind != NULL ? invite_kind :
"business_invite",
4024 target_business_name != NULL ? target_business_name :
"",
4026 invite_email != NULL ? invite_email :
"",
4031 free(target_business_name);
4041 return body_params_json;
4060 const char *request_path,
4061 const char *query_string,
4064 char *body_params_json = NULL;
4066 if (app == NULL || match == NULL || match->
page_id == NULL) {
4067 return strdup(
"{\"error_html\":\"\",\"next_input\":\"\"}");
4070 if (
streq(match->
page_id,
"profile") && active_user_id > 0) {
4080 payment_preference_options_html != NULL ? payment_preference_options_html :
""
4082 free(selected_payment_preference);
4083 free(payment_preference_options_html);
4085 return body_params_json;
4096 if (
streq(match->
page_id,
"password_reset_complete")) {
4105 if (
streq(match->
page_id,
"appointments") && active_user_id > 0) {
4108 if (
streq(match->
page_id,
"business_dashboard") && active_user_id > 0) {
4115 business_slug != NULL ? business_slug :
"",
4119 free(business_slug);
4120 return body_params_json;
4129 business_slug != NULL ? business_slug :
"",
4135 free(business_slug);
4136 free(selected_year);
4137 free(selected_month);
4138 free(selected_date);
4139 return body_params_json;
4148 business_slug != NULL ? business_slug :
"",
4151 free(business_slug);
4152 return body_params_json;
4157 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
4160 business_slug != NULL ? business_slug :
"",
4165 free(business_slug);
4166 free(service_id_text);
4167 return body_params_json;
4173 business_slug != NULL ? business_slug :
""
4175 free(business_slug);
4176 return body_params_json;
4181 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
4184 business_slug != NULL ? business_slug :
"",
4187 free(business_slug);
4188 free(service_id_text);
4189 return body_params_json;
4193 const char *surface_kind =
"services";
4195 surface_kind =
"availability";
4197 surface_kind =
"technicians";
4201 business_slug != NULL ? business_slug :
"",
4205 free(business_slug);
4206 return body_params_json;
4211 if (
streq(match->
page_id,
"business_availability_edit")) {
4214 long availability_id = availability_id_text != NULL ? strtol(availability_id_text, NULL, 10) : 0;
4217 business_slug != NULL ? business_slug :
"",
4221 free(business_slug);
4222 free(availability_id_text);
4223 return body_params_json;
4227 return strdup(
"{\"error_html\":\"\",\"next_input\":\"\"}");
4245 char *preview_path = NULL;
4252 char created_at[21];
4253 char expires_at[21];
4275 char *token_hash = NULL;
4279 password_resets = mut_root != NULL ?
yyjson_mut_obj_get(mut_root,
"password_resets") : NULL;
4283 if (password_resets != NULL) {
4290 if (mut_doc != NULL && mut_root != NULL && password_resets != NULL && token_hash != NULL) {
4294 if (reset_obj != NULL
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);
4326 int status = body_params_json != NULL
4329 free(body_params_json);
4365 char *token_hash = NULL;
4366 char *password_hash = NULL;
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) {
4373 free(password_confirm);
4374 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_password_reset",
"Invalid password reset submission.");
4383 password_resets = mut_root != NULL ?
yyjson_mut_obj_get(mut_root,
"password_resets") : NULL;
4386 if (root == NULL || mut_doc == NULL || mut_root == NULL || token_hash == NULL || !
now_iso_utc_runtime(now_iso)
4391 free(password_confirm);
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;
4412 char *expires_at = NULL;
4419 && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
4420 found_reset_index = reset_index;
4429 if (source_users != NULL && found_reset_index != (
size_t)-1) {
4432 size_t user_index = 0;
4436 found_user_index = user_index;
4443 if (found_reset_index != (
size_t)-1) {
4446 if (found_user_index != (
size_t)-1) {
4451 if (reset_row == NULL || user_row == NULL) {
4455 free(password_confirm);
4459 write_json_error(connection, 404,
"Not Found",
"password_reset_not_found",
"Password reset token not found.");
4464 if (password_hash == NULL
4470 free(password_confirm);
4472 free(password_hash);
4478 if (sessions != NULL) {
4482 char *render_path = request_path != NULL ? strdup(request_path) : strdup(
"/accounts/password-reset/");
4489 free(password_confirm);
4491 free(password_hash);
4494 if (write_code != 0) {
4500 char *body_params_json = strdup(
"{}");
4501 int status = body_params_json != NULL && render_path != NULL
4504 free(body_params_json);
4539 long business_id = 0;
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;
4553 free(business_slug);
4557 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_invite_request",
"Invalid invite submission.");
4568 free(business_slug);
4573 write_json_error(connection, 403,
"Forbidden",
"invite_forbidden",
"Invite creation requires business membership.");
4582 free(business_slug);
4595 free(business_slug);
4608 if (token_hash != NULL) {
4612 const char *invite_kind =
"business_invite";
4614 if (invite_obj != NULL
4617 &&
yyjson_mut_obj_add_strcpy(mut_doc, invite_obj,
"account_type", account_type != NULL && account_type[0] !=
'\0' ? account_type :
"business_customer")
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);
4643 if (return_path != NULL) {
4644 snprintf(return_path, return_needed,
"/businesses/%s/", business_slug);
4651 free(business_slug);
4658 if (write_code != 0 || preview_path == NULL || return_path == NULL || render_path == NULL || invite_email_copy == NULL) {
4662 free(invite_email_copy);
4668 int status = body_params_json != NULL
4671 free(body_params_json);
4675 free(invite_email_copy);
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;
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;
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)) {
4735 free(password_confirm);
4736 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_invite_accept",
"Invalid invite acceptance submission.");
4745 if (root == NULL || token_hash == NULL || username_taken) {
4752 free(password_confirm);
4755 if (username_taken) {
4756 write_json_error(connection, 422,
"Unprocessable Entity",
"username_taken",
"Username already exists.");
4759 write_json_error(connection, 404,
"Not Found",
"invite_not_found",
"Invite token not found.");
4771 size_t scan_index = 0;
4779 char *expires_at = NULL;
4786 && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
4787 invite_index = scan_index;
4802 if (invite_index == (
size_t)-1) {
4809 free(password_confirm);
4812 write_json_error(connection, 404,
"Not Found",
"invite_not_found",
"Invite token not found.");
4821 memberships = mut_root != NULL ?
yyjson_mut_obj_get(mut_root,
"tenant_memberships") : NULL;
4829 free(password_confirm);
4831 free(password_hash);
4846 free(password_confirm);
4848 free(password_hash);
4857 if (invite_row == NULL) {
4864 free(password_confirm);
4866 free(password_hash);
4875 int ok = user_obj != NULL
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")
4891 if (ok && tenant_id > 0) {
4892 ok = membership_obj != NULL
4910 free(password_confirm);
4912 free(password_hash);
4924 if (write_code != 0) {
4931 free(password_confirm);
4933 free(password_hash);
4947 if (tenant_id > 0) {
4951 long business_id = 0;
4960 if (business_id > 0) {
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;
4969 }
else if (user_after != NULL) {
4975 if (
issue_browser_session_runtime(app, request, next_user_id, &session_token, &session_cookie_header) != 0 || session_cookie_header == NULL) {
4982 free(password_confirm);
4984 free(password_hash);
4985 free(session_token);
4986 free(session_cookie_header);
4994 size_t header_needed = strlen(session_cookie_header) + 3;
4995 char *extra_headers = (
char *)malloc(header_needed);
4998 if (extra_headers != NULL) {
4999 snprintf(extra_headers, header_needed,
"%s\r\n", session_cookie_header);
5001 if (body_params_json != NULL && extra_headers != NULL) {
5004 free(extra_headers);
5005 free(body_params_json);
5012 free(password_confirm);
5014 free(password_hash);
5015 free(session_token);
5016 free(session_cookie_header);
5033 const char *route_id,
5034 const char *csrf_token
5039 len = snprintf(html,
sizeof(html),
5041 "<html lang=\"en\">\n"
5043 " <meta charset=\"utf-8\">\n"
5044 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
5045 " <title>%s</title>\n"
5050 " <p>Served by the Pharos native in-process dynamic runtime.</p>\n"
5051 " <p>Route: <code>%s</code></p>\n"
5055 app_id != NULL ? app_id :
"Pharos App",
5056 app_id != NULL ? app_id :
"Pharos App",
5057 route_id != NULL ? route_id :
"/"
5060 if (
len < 0 || (
size_t)
len >=
sizeof(html)) {
5082 const char *request_path,
5083 const char *query_string,
5084 const char *csrf_token
5086 char *page_spec_rel = NULL;
5087 char *page_spec_path = NULL;
5088 char *page_spec_text = NULL;
5091 char *template_file = NULL;
5093 char *layout_id = NULL;
5094 char *body_template_path = NULL;
5097 char *body_params_json = NULL;
5098 char *body_html = NULL;
5099 char *full_html = NULL;
5100 char *injected_html = NULL;
5103 if (app == NULL || connection == NULL || match == NULL) {
5104 if (connection != NULL) {
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"
5134 if (page_spec_rel != NULL) {
5136 if (page_spec_path != NULL &&
path_exists(page_spec_path)) {
5138 if (page_spec_text != NULL) {
5140 if (page_spec_root != NULL) {
5149 if (template_file != NULL && layout_id != NULL) {
5151 if (body_template_path != NULL &&
path_exists(body_template_path)) {
5161 body_params_json != NULL ? body_params_json :
"{\"error_html\":\"\",\"next_input\":\"\"}",
5163 if (body_params_root != NULL) {
5171 if (body_html != NULL) {
5179 request != NULL ? request->
target :
"/",
5180 active_user_id > 0 ? 1 : 0,
5184 if (full_html != NULL) {
5187 injected_html != NULL ? injected_html : full_html,
5197 free(body_params_json);
5200 free(injected_html);
5201 free(template_file);
5204 free(body_template_path);
5205 free(page_spec_text);
5206 free(page_spec_path);
5207 free(page_spec_rel);
5210 if (status == 200) {
5217 if (page_file != NULL) {
5218 char *html_file = NULL;
5219 size_t page_file_len = strlen(page_file);
5221 if (page_file_len > 5 && strcmp(page_file + page_file_len - 5,
".html") == 0) {
5222 html_file = strdup(page_file);
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);
5230 if (html_file != NULL &&
path_exists(html_file)) {
5258 if (connection == NULL || app == NULL || request == NULL || match == NULL) {
5259 if (connection != NULL) {
5266 write_json_error(connection, 405,
"Method Not Allowed",
"action_requires_post",
"Action routes require POST.");
5273 free(csrf_form_body);
5295 if (title == NULL || title[0] ==
'\0') {
5302 if (state_root == NULL || items == NULL || !
yyjson_is_arr(items)) {
5312 if (mut_doc == NULL || mut_root == NULL || mut_items == NULL
5372 "HTTP/1.1 303 See Other\r\n"
5374 "Cache-Control: no-store\r\n"
5375 "Content-Length: 0\r\n"
5376 "Connection: close\r\n"
5386 long task_id = task_id_text != NULL ? strtol(task_id_text, NULL, 10) : 0;
5405 if (state_root == NULL || items == NULL || !
yyjson_is_arr(items)) {
5415 if (mut_doc == NULL || mut_root == NULL || mut_items == NULL
5441 if (row_id == task_id) {
5460 write_json_error(connection, 404,
"Not Found",
"task_not_found",
"Task not found.");
5482 "HTTP/1.1 303 See Other\r\n"
5484 "Cache-Control: no-store\r\n"
5485 "Content-Length: 0\r\n"
5486 "Connection: close\r\n"
5517 char *stored_hash = NULL;
5518 char *session_cookie_header = NULL;
5519 char *session_token = NULL;
5521 const char *target_path =
"/explore/";
5528 if (stored_hash != NULL &&
verify_password_hash_runtime(stored_hash, password != NULL ? password :
"", username != NULL ? username :
"")) {
5536 "HTTP/1.1 302 Found\r\n"
5537 "Location: %s%s\r\n"
5539 "Cache-Control: no-store\r\n"
5540 "Content-Length: 0\r\n"
5541 "Connection: close\r\n"
5545 session_cookie_header
5553 char *error_params_json = NULL;
5556 if (error_params_json != NULL) {
5558 free(error_params_json);
5569 free(session_cookie_header);
5570 free(session_token);
5591 if (active_user_id <= 0) {
5596 free(payment_preference);
5597 write_json_error(connection, 403,
"Forbidden",
"session_required",
"Profile updates require an active session.");
5601 if (payment_preference != NULL && payment_preference[0] !=
'\0'
5602 && !
streq(payment_preference,
"pay_on_site")
5603 && !
streq(payment_preference,
"credit_card")) {
5608 free(payment_preference);
5609 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_payment_preference",
"Unsupported payment preference.");
5617 if (mut_doc == NULL || mut_root == NULL || users == NULL || !
yyjson_mut_is_arr(users)) {
5622 free(payment_preference);
5640 free(payment_preference);
5655 free(payment_preference);
5658 write_json_error(connection, 404,
"Not Found",
"user_not_found",
"Profile user not found.");
5670 free(payment_preference);
5683 free(payment_preference);
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"
5704 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 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;
5728 if (active_user_id <= 0) {
5730 free(business_slug);
5731 free(service_id_text);
5738 write_json_error(connection, 403,
"Forbidden",
"session_required",
"Appointments require an active session.");
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') {
5744 free(business_slug);
5745 free(service_id_text);
5752 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_appointment_request",
"Missing appointment booking fields.");
5756 hour_value = atoi(time_hour);
5757 minute_value = atoi(time_minute);
5758 if (minute_value < 0 || minute_value > 59) {
5761 if (time_period != NULL && time_period[0] !=
'\0') {
5762 if (hour_value < 1 || hour_value > 12 || minute_value < 0) {
5764 free(business_slug);
5765 free(service_id_text);
5772 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_appointment_time",
"Invalid appointment time.");
5775 normalized_hour = hour_value % 12;
5776 if (
streq(time_period,
"PM") ||
streq(time_period,
"pm")) {
5777 normalized_hour += 12;
5780 if (hour_value < 0 || hour_value > 23 || minute_value < 0) {
5782 free(business_slug);
5783 free(service_id_text);
5790 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_appointment_time",
"Invalid appointment time.");
5793 normalized_hour = hour_value;
5795 snprintf(time_value,
sizeof(time_value),
"%02d:%02d", normalized_hour, minute_value);
5802 if (business == NULL || service == NULL) {
5804 free(business_slug);
5805 free(service_id_text);
5813 write_json_error(connection, 404,
"Not Found",
"not_found",
"Business or service not found.");
5818 if (duration_minutes <= 0) {
5819 duration_minutes = 45;
5823 free(business_slug);
5824 free(service_id_text);
5832 native_send_text_response(connection, 422,
"Unprocessable Entity",
"Requested time is outside business availability.\n");
5837 if (technician_id <= 0) {
5839 free(business_slug);
5840 free(service_id_text);
5854 appointments = mut_root != NULL ?
yyjson_mut_obj_get(mut_root,
"appointments") : NULL;
5857 free(business_slug);
5858 free(service_id_text);
5874 free(business_slug);
5875 free(service_id_text);
5891 if (appointment_obj == NULL
5903 free(business_slug);
5904 free(service_id_text);
5922 free(business_slug);
5923 free(service_id_text);
5939 free(business_slug);
5940 free(service_id_text);
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"
5966 long appointment_id = appointment_id_text != NULL ? strtol(appointment_id_text, NULL, 10) : 0;
5977 size_t remove_index = 0;
5980 if (active_user_id <= 0) {
5982 free(appointment_id_text);
5983 write_json_error(connection, 403,
"Forbidden",
"session_required",
"Appointment deletion requires an active session.");
5986 if (appointment_id <= 0) {
5988 free(appointment_id_text);
5989 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_appointment_id",
"Invalid appointment id.");
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)
6002 free(appointment_id_text);
6027 free(appointment_id_text);
6030 write_json_error(connection, 404,
"Not Found",
"appointment_not_found",
"Appointment not found.");
6039 free(appointment_id_text);
6049 free(appointment_id_text);
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"
6069 long service_id = service_id_text != NULL ? strtol(service_id_text, NULL, 10) : 0;
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;
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.");
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;
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);
6117 size_t service_index = 0;
6118 for (service_index = 0; service_index < service_count; service_index++) {
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.");
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);
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);
6152 for (
long option_index = 1; option_index <= booking_option_count; option_index++) {
6155 char *option_duration_text = NULL;
6156 char *option_cost = NULL;
6157 long option_duration = 0;
6159 snprintf(key,
sizeof(key),
"booking_option_label_%ld", option_index);
6161 snprintf(key,
sizeof(key),
"booking_option_duration_%ld", option_index);
6163 snprintf(key,
sizeof(key),
"booking_option_cost_%ld", option_index);
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) {
6168 if (option_obj != NULL
6176 free(option_duration_text);
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);
6188 if (source_technicians != NULL &&
yyjson_is_arr(source_technicians)) {
6190 size_t tech_index = 0;
6191 for (tech_index = 0; tech_index < tech_count; tech_index++) {
6193 char field_name[64];
6194 char *field_value = NULL;
6195 long technician_id = 0;
6200 snprintf(field_name,
sizeof(field_name),
"technician_%ld", technician_id);
6202 if (field_value != NULL && field_value[0] !=
'\0') {
6211 if (remove_image != NULL && remove_image[0] !=
'\0') {
6212 if (existing_image != NULL && existing_image[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);
6224 if (uploaded_media_url != NULL && uploaded_media_url[0] !=
'\0') {
6225 if (existing_image != NULL && existing_image[0] !=
'\0') {
6230 free(uploaded_media_url);
6232 free(existing_image);
6235 char *redirect_slug = business_slug != NULL ? strdup(business_slug) : NULL;
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);
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"
6257 free(redirect_slug);
6265 if (cost == NULL || cost[0] ==
'\0') {
6273 write_json_error(connection, 501,
"Not Implemented",
"not_implemented",
"Native service creation is not implemented yet.");
6282 int start_minutes = 0;
6283 int end_minutes = 0;
6287 if (!day_valid || !start_valid || !end_valid || end_minutes <= start_minutes) {
6292 native_send_text_response(connection, 422,
"Unprocessable Entity",
"Validation failed: availability_day_valid availability_end_after_start\n");
6299 write_json_error(connection, 501,
"Not Implemented",
"not_implemented",
"Native availability creation is not implemented yet.");
6309 long score = score_text != NULL ? strtol(score_text, NULL, 10) : 0;
6318 long business_id = 0;
6321 if (active_user_id <= 0) {
6323 free(business_slug);
6326 write_json_error(connection, 403,
"Forbidden",
"session_required",
"Submitting a rating requires an active session.");
6329 if (business_slug == NULL || business_slug[0] ==
'\0') {
6331 free(business_slug);
6334 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_business_slug",
"Missing business slug.");
6337 if (score < 1 || score > 5) {
6339 free(business_slug);
6342 write_json_error(connection, 422,
"Unprocessable Entity",
"invalid_rating_score",
"Rating score must be between 1 and 5.");
6355 if (business == NULL || business_id <= 0 || mut_doc == NULL || mut_root == NULL || !
yyjson_mut_is_obj(mut_root)) {
6357 free(business_slug);
6362 write_json_error(connection, 404,
"Not Found",
"business_not_found",
"Business not found.");
6369 free(business_slug);
6379 if (source_ratings != NULL &&
yyjson_is_arr(source_ratings)) {
6381 size_t rating_index = 0;
6382 for (rating_index = 0; rating_index < rating_count; rating_index++) {
6393 if (rating_row != NULL) {
6397 free(business_slug);
6409 if (rating_row == NULL
6417 free(business_slug);
6432 free(business_slug);
6437 native_send_text_response(connection, 500,
"Internal Server Error", updated ?
"rating update persist failed\n" :
"rating create persist failed\n");
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"
6452 business_slug != NULL ? business_slug :
""
6455 free(business_slug);
6468 "HTTP/1.1 302 Found\r\n"
6469 "Location: %s/accounts/login/\r\n"
6471 "Cache-Control: no-store\r\n"
6472 "Content-Length: 0\r\n"
6473 "Connection: close\r\n"
6476 clear_cookie_header != NULL ? clear_cookie_header :
""
6478 free(clear_cookie_header);
6482 write_json_error(connection, 501,
"Not Implemented",
"not_implemented",
"This action is not implemented in the native runtime yet.");
6499 char *route_fixture_path = NULL;
6500 char *request_path = NULL;
6507 if (app == NULL || request == NULL || connection == NULL) {
6508 if (connection != NULL) {
6514 target = request->
target != NULL ? request->
target :
"/";
6516 fprintf(stderr,
"dynamic_handler_enter: target=%s app_id=%s artifact_dir=%s\n",
6524 if (request_path == NULL) {
6533 if (raw_query[0] !=
'\0') {
6534 query = strdup(raw_query);
6552 if (
streq(request_path,
"/api/state")) {
6560 if (state_text != NULL) {
6561 int state_len = (int)strlen(state_text);
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"
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"
6603 int start_minutes = 0;
6604 int end_minutes = 0;
6608 if (!day_valid || !start_valid || !end_valid || end_minutes <= start_minutes) {
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\"]}"
6629 write_json_error(connection, 501,
"Not Implemented",
"not_implemented",
"Native availability API creation is not implemented yet.");
6638 if (business_slug != NULL) {
6640 free(business_slug);
6646 if (business_slug != NULL) {
6648 free(business_slug);
6654 if (business_slug != NULL) {
6656 free(business_slug);
6662 if (business_slug != NULL) {
6664 free(business_slug);
6672 if (route_fixture_path == NULL) {
6676 if (dispatch_file != NULL) {
6678 free(dispatch_file);
6682 write_json_error(connection, 404,
"Not Found",
"not_found",
"No route fixture available and no dispatch match.");
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",
6704 write_json_error(connection, 500,
"Internal Server Error",
"dispatch_error",
"route fixture load failed");
6709 if (match_result > 0 || match == NULL) {
6713 if (dispatch_file != NULL) {
6715 free(dispatch_file);
6719 write_json_error(connection, 404,
"Not Found",
"not_found",
"No matching route for this path.");
6731 status =
handle_page_route(connection, app, request, match, request_path, query, csrf_token);
6742 free(route_fixture_path);
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.
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.
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.
const char * filter_title
const char * surface_kind
const char * resource_collection_name
const char * surface_title
const char * create_button_label
const char * sort_options_html
const char * surface_path_segment
const char * create_title
const char * surface_intro
const char * records_title
const char * default_filter_controls_html
const char * empty_state_row_html
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)
yyjson_mut_doc * yyjson_mut_doc_new(const yyjson_alc *alc)
yyjson_mut_doc * yyjson_doc_mut_copy(const yyjson_doc *doc, const yyjson_alc *alc)
yyjson_mut_val * yyjson_val_mut_copy(yyjson_mut_doc *m_doc, const yyjson_val *i_vals)
yyjson_api_inline bool yyjson_is_str(const yyjson_val *val)
yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val)
#define yyjson_mut_arr_foreach(arr, idx, max, val)
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr_remove(yyjson_mut_val *arr, size_t idx)
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj_get(const yyjson_mut_val *obj, const char *key)
yyjson_api_inline int64_t yyjson_get_sint(const yyjson_val *val)
yyjson_api_inline yyjson_val * yyjson_arr_get(const yyjson_val *arr, size_t idx)
yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, int64_t val)
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val)
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj(yyjson_mut_doc *doc)
yyjson_api_inline yyjson_mut_val * yyjson_mut_null(yyjson_mut_doc *doc)
yyjson_api_inline bool yyjson_is_int(const yyjson_val *val)
yyjson_api_inline bool yyjson_is_real(const yyjson_val *val)
yyjson_api_inline char * yyjson_mut_write(const yyjson_mut_doc *doc, yyjson_write_flag flg, size_t *len)
yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, yyjson_mut_val *root)
yyjson_api_inline yyjson_mut_val * yyjson_mut_int(yyjson_mut_doc *doc, int64_t num)
yyjson_api_inline yyjson_mut_val * yyjson_mut_strcpy(yyjson_mut_doc *doc, const char *str)
yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, const char *str)
yyjson_api_inline bool yyjson_mut_is_obj(const yyjson_mut_val *val)
yyjson_api_inline yyjson_mut_val * yyjson_mut_doc_get_root(yyjson_mut_doc *doc)
yyjson_api_inline bool yyjson_is_uint(const yyjson_val *val)
yyjson_api_inline yyjson_val * yyjson_arr_iter_next(yyjson_arr_iter *iter)
yyjson_api_inline bool yyjson_is_null(const yyjson_val *val)
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr_get(const yyjson_mut_val *arr, size_t idx)
yyjson_api_inline size_t yyjson_get_len(const yyjson_val *val)
yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, bool val)
yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *val)
yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr)
yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, yyjson_mut_val *key, yyjson_mut_val *val)
yyjson_api_inline yyjson_mut_val * yyjson_mut_sint(yyjson_mut_doc *doc, int64_t num)
yyjson_api_inline char * yyjson_write(const yyjson_doc *doc, yyjson_write_flag flg, size_t *len)
static const yyjson_write_flag YYJSON_WRITE_NOFLAG
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
yyjson_api_inline bool yyjson_mut_is_arr(const yyjson_mut_val *val)
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr(yyjson_mut_doc *doc)
yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, yyjson_mut_val *val)
yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter)
yyjson_api_inline bool yyjson_get_bool(const yyjson_val *val)