From 33e8bed1780d19ead8ca2fb908c44c8c8594eeb2 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 22 Aug 2025 18:24:04 +0700 Subject: [PATCH 01/30] added db_insert_user --- src/db_insert_user.c | 56 ++++++++++++++++++++++++++++++++++++++++++++ src/sql.h | 25 +++++++++++++------- src/user.h | 7 ++++-- 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/db_insert_user.c diff --git a/src/db_insert_user.c b/src/db_insert_user.c new file mode 100644 index 0000000..409fb1e --- /dev/null +++ b/src/db_insert_user.c @@ -0,0 +1,56 @@ +#include "database.h" +#include "sql.h" +#include "user.h" + +User *db_insert_user(const char *username, const char *email, + const char *password, const char *group, + bool *is_superuser) { + pthread_mutex_lock(&db_mutex); + if (!db) { + fprintf(stderr, "Databse not initialized\n"); + return nullptr; + } + if (!username || !email || !password) { + fprintf(stderr, "username, email, password connot be empty\n"); + return nullptr; + } + const char *sql = nullptr; + sqlite3_stmt *stmt; + int rc; + if (!group || !is_superuser) { + sql = INSERT_USER_USERNAME_EMAIL_PASSWORD_SQL; + } else if (!group) { + sql = INSERT_USER_USERNAME_EMAIL_PASSWORD_GROUP_SQL; + } else { + sql = INSERT_USER_FULL_SQL; + } + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); + if (rc != SQLITE_OK) { + fprintf(stderr, "Cannot prepare statement: %s\n", sqlite3_errmsg(db)); + return nullptr; + } + + sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, email, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, password, -1, SQLITE_STATIC); + + if (group) { + sqlite3_bind_text(stmt, 4, group, -1, SQLITE_STATIC); + } + if (is_superuser) { + sqlite3_bind_int(stmt, 5, is_superuser); + } + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + fprintf(stderr, "Execution failed: %s\n", sqlite3_errmsg(db)); + sqlite3_finalize(stmt); + return nullptr; + } + + int inserted_id = sqlite3_last_insert_rowid(db); + + sqlite3_finalize(stmt); + printf("User inserted successfully. ID: %d\n", inserted_id); + pthread_mutex_unlock(&db_mutex); +} \ No newline at end of file diff --git a/src/sql.h b/src/sql.h index 4c364e6..88ee2fb 100644 --- a/src/sql.h +++ b/src/sql.h @@ -1,13 +1,13 @@ #pragma once -#define CREATE_TABLE_USERS \ - "create table if not exists users(" \ - "id integer primary key autoincrement," \ - "username text unique," \ - "email char(50) unique," \ - "password char(150) not null," \ - "user_group char(50) default 'users'," \ - "is_superuser boolean default false" \ +#define CREATE_TABLE_USERS \ + "create table if not exists users(" \ + "id integer primary key autoincrement," \ + "username text unique," \ + "email char(50) unique," \ + "password char(150) not null," \ + "user_group char(50) default 'users'," \ + "is_superuser boolean default false" \ ");" #define INSERT_USER_SQL \ @@ -16,4 +16,11 @@ #define SELECT_ALL_USERS_SQL "select * from users" #define SELECT_USER_BY_USERNAME_SQL "select * from users where username = ?" - +#define INSERT_USER_USERNAME_EMAIL_PASSWORD_SQL \ + "insert into users (username, email, password) values (?, ?, ?)" +#define INSERT_USER_USERNAME_EMAIL_PASSWORD_GROUP_SQL \ + "insert into users (username, email, password, user_group) values (?, ?, " \ + "?, ?)" +#define INSERT_USER_FULL_SQL \ + "insert into users (username, email, password, user_group, is_superuser) " \ + "values (?, ?, ?, ?, ?)" diff --git a/src/user.h b/src/user.h index 2e94960..d030b45 100644 --- a/src/user.h +++ b/src/user.h @@ -13,8 +13,8 @@ typedef struct User { bool is_superuser; } User; -User *new_user(int id, const char *username, const char *password, const char *email, - const char *group, bool is_superuser); +User *new_user(int id, const char *username, const char *password, + const char *email, const char *group, bool is_superuser); void free_user(User *user); @@ -55,3 +55,6 @@ void verify_token(struct mg_connection *c, struct mg_http_message *hm); void db_get_users_from_file(const char *filename); char *db_get_all_users(); User *db_get_user_by_username(const char *username); +User *db_insert_user(const char *username, const char *email, + const char *password, const char *group, + bool *is_superuser); From 9c3a8d796a19a9b76535bdc0e18e3029e459af0d Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 23 Aug 2025 00:30:45 +0700 Subject: [PATCH 02/30] added update user change user and change password --- meson.build | 3 ++ src/add_user.c | 41 ++++++++++++++++++++++++++- src/change_password.c | 31 ++++++++++++++++++++ src/change_user.c | 52 +++++++++++++++++++++++++++++++++- src/db_delete_user.c | 44 +++++++++++++++++++++++++++++ src/db_get_users_from_file.c | 33 ++++------------------ src/db_insert_user.c | 13 +++++++-- src/db_update_user.c | 55 ++++++++++++++++++++++++++++++++++++ src/delete_user.c | 6 +++- src/main.c | 3 +- src/register_user.c | 8 ++++-- src/sql.h | 5 ++++ src/user.c | 23 +++++++++++---- src/user.h | 6 +++- 14 files changed, 278 insertions(+), 45 deletions(-) create mode 100644 src/db_delete_user.c create mode 100644 src/db_update_user.c diff --git a/meson.build b/meson.build index 79ed8d9..85c3ed0 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,9 @@ executable( 'src/db_get_user_by_username.c', 'src/db_get_users_from_file.c', 'src/file.c', + 'src/db_insert_user.c', + 'src/db_update_user.c', + 'src/db_delete_user.c', include_directories: include_dirs, dependencies: deps, ) diff --git a/src/add_user.c b/src/add_user.c index ed68752..9741e2d 100644 --- a/src/add_user.c +++ b/src/add_user.c @@ -10,8 +10,47 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { MG_ESC("invalid token")); return; } + cJSON *jsonbody = cJSON_Parse(hm->body.buf); + cJSON *username = cJSON_GetObjectItem(jsonbody, "username"); + cJSON *password = cJSON_GetObjectItem(jsonbody, "password"); + cJSON *email = cJSON_GetObjectItem(jsonbody, "email"); + cJSON *group = cJSON_GetObjectItem(jsonbody, "group"); + cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); + if (username == nullptr || password == nullptr || email == nullptr || + is_superuser == nullptr || group == nullptr) { + mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("must have , , , , ")); + return; + } + cJSON *payload = jwt_get_payload(requestdata->token); + bool jwt_is_superuser = + cJSON_GetObjectItem(payload, "is_superuser")->valueint; + if (!jwt_is_superuser) { + mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("not authorized")); + cJSON_Delete(payload); + return; + } + const bool user_is_superuser = is_superuser->valueint ? true : false; + User *new_db_user = db_insert_user( + username->valuestring, email->valuestring, password->valuestring, + group->valuestring, + &user_is_superuser); + if (!new_db_user) { + mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("failed to create user")); + cJSON_Delete(payload); + return; + } char responsedata[2048] = {0}; - snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); + snprintf(responsedata, sizeof(responsedata), + "{\"username\":\"%s\",\"email\":\"%s\",\"group\":\"%s\",\"is_" + "superuser\":%s}", + username->valuestring, email->valuestring, group->valuestring, + is_superuser->valueint ? "true" : "false"); + mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + cJSON_Delete(payload); } diff --git a/src/change_password.c b/src/change_password.c index 9fbd0af..8e1aa1b 100644 --- a/src/change_password.c +++ b/src/change_password.c @@ -10,8 +10,39 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { MG_ESC("invalid token")); return; } + cJSON *jsonbody = cJSON_Parse(hm->body.buf); + cJSON *old_password = cJSON_GetObjectItem(jsonbody, "old_password"); + cJSON *new_password = cJSON_GetObjectItem(jsonbody, "new_password"); + if (old_password == nullptr || new_password == nullptr) { + mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("must have , ")); + return; + } + cJSON *payload = jwt_get_payload(requestdata->token); + char *username = cJSON_GetObjectItem(payload, "username")->valuestring; + User *old_user_data = db_get_user_by_username(username); + char *password = old_user_data->password; + + if (strcmp(password, old_password->valuestring)) { + mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid old password")); + return; + } + const bool old_is_superuser = old_user_data->is_superuser; + User *new_user_data = db_update_user( + old_user_data->id, old_user_data->username, old_user_data->email, + new_password->valuestring, old_user_data->group, &old_is_superuser); + if (!new_user_data) { + mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("failed to update user")); + return; + } + char responsedata[64] = {0}; snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + // free(username); + free_user(old_user_data); + cJSON_Delete(payload); } diff --git a/src/change_user.c b/src/change_user.c index df91ded..f64f8d9 100644 --- a/src/change_user.c +++ b/src/change_user.c @@ -10,8 +10,58 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { MG_ESC("invalid token")); return; } + cJSON *jsonbody = cJSON_Parse(hm->body.buf); + cJSON *username = cJSON_GetObjectItem(jsonbody, "username"); + cJSON *email = cJSON_GetObjectItem(jsonbody, "email"); + cJSON *group = cJSON_GetObjectItem(jsonbody, "group"); + cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); + User *old_user_data = db_get_user_by_username(username->valuestring); + if (!old_user_data) { + mg_http_reply(c, 404, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("user not found")); + return; + } + char *password = old_user_data->password; + if (!password) { + mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("password is required")); + return; + } + if (username == nullptr || email == nullptr || + is_superuser == nullptr || group == nullptr) { + mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("must have , , , ")); + free_user(old_user_data); + free(password); + return; + } + cJSON *payload = jwt_get_payload(requestdata->token); + int id = cJSON_GetObjectItem(payload, "id")->valueint; + const bool user_is_superuser = is_superuser->valueint ? true : false; + User *updated_user_data = db_update_user( + id, username->valuestring, email->valuestring, password, + group->valuestring, &user_is_superuser); + + if (!updated_user_data) { + mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + MG_ESC("failed to update user")); + cJSON_Delete(payload); + free_user(old_user_data); + free(password); + return; + } + char responsedata[2048] = {0}; - snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); + snprintf(responsedata, sizeof(responsedata), + "{\"username\":\"%s\",\"email\":\"%s\",\"group\":\"%s\",\"is_" + "superuser\":%s}", + username->valuestring, email->valuestring, group->valuestring, + is_superuser->valueint ? "true" : "false"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + free_user(updated_user_data); + free_user(old_user_data); + cJSON_Delete(payload); + free(password); } diff --git a/src/db_delete_user.c b/src/db_delete_user.c new file mode 100644 index 0000000..35513af --- /dev/null +++ b/src/db_delete_user.c @@ -0,0 +1,44 @@ +#include "database.h" +#include "sql.h" +#include "user.h" + +bool db_delete_user(const int id) { + pthread_mutex_lock(&db_mutex); + if (!db) { + fprintf(stderr, "Database not initialized\n"); + pthread_mutex_unlock(&db_mutex); + return false; + } + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_prepare_v2(db, DELETE_USER_SQL, -1, &stmt, 0); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db)); + pthread_mutex_unlock(&db_mutex); + return false; + } + + sqlite3_bind_int(stmt, 1, id); + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + fprintf(stderr, "Execution failed: %s\n", sqlite3_errmsg(db)); + sqlite3_finalize(stmt); + pthread_mutex_unlock(&db_mutex); + return false; + } + + int changes = sqlite3_changes(db); + sqlite3_finalize(stmt); + + if (changes > 0) { + printf("User deleted successfully. ID: %d\n", id); + } else { + printf("No user found with ID: %d\n", id); + return false; + } + + pthread_mutex_unlock(&db_mutex); + return true; +} diff --git a/src/db_get_users_from_file.c b/src/db_get_users_from_file.c index a435976..c7956e0 100644 --- a/src/db_get_users_from_file.c +++ b/src/db_get_users_from_file.c @@ -1,14 +1,11 @@ #include #include -#include "sql.h" -#include "user.h" #include "file.h" -#include "database.h" +#include "user.h" void db_get_users_from_file(const char *path) { char *users_string = read_file(path); cJSON *json_array = cJSON_Parse(users_string); - pthread_mutex_lock(&db_mutex); for (int i = 0; i < cJSON_GetArraySize(json_array); i++) { cJSON *json_object = cJSON_GetArrayItem(json_array, i); const char *username = @@ -19,32 +16,14 @@ void db_get_users_from_file(const char *path) { const char *group = cJSON_GetObjectItem(json_object, "group")->valuestring; const bool is_superuser = cJSON_GetObjectItem(json_object, "is_superuser")->valueint; - - sqlite3_stmt *stmt; - int rc = sqlite3_prepare_v2(db, INSERT_USER_SQL, -1, &stmt, nullptr); - if (rc != SQLITE_OK) { - fprintf(stderr, "Cannot prepare statement: %s\n", sqlite3_errmsg(db)); - sqlite3_close(db); - exit(1); + User *new_db_user = + db_insert_user(username, email, password, group, &is_superuser); + if (!new_db_user) { + fprintf(stderr, "Cannot insert user: %s\n", username); } - - sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, email, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, password, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, group, -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 5, is_superuser); - - rc = sqlite3_step(stmt); - if (rc != SQLITE_DONE) { - fprintf(stderr, "Cannot insert data: %s\n", sqlite3_errmsg(db)); - } else { - printf("Record inserted successfully. Row ID: %lld\n", - sqlite3_last_insert_rowid(db)); - } - sqlite3_finalize(stmt); + free_user(new_db_user); } cJSON_Delete(json_array); free(users_string); - pthread_mutex_unlock(&db_mutex); } diff --git a/src/db_insert_user.c b/src/db_insert_user.c index 409fb1e..d293129 100644 --- a/src/db_insert_user.c +++ b/src/db_insert_user.c @@ -4,14 +4,16 @@ User *db_insert_user(const char *username, const char *email, const char *password, const char *group, - bool *is_superuser) { + const bool *is_superuser) { pthread_mutex_lock(&db_mutex); if (!db) { fprintf(stderr, "Databse not initialized\n"); + pthread_mutex_unlock(&db_mutex); return nullptr; } if (!username || !email || !password) { fprintf(stderr, "username, email, password connot be empty\n"); + pthread_mutex_unlock(&db_mutex); return nullptr; } const char *sql = nullptr; @@ -27,6 +29,7 @@ User *db_insert_user(const char *username, const char *email, rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); if (rc != SQLITE_OK) { fprintf(stderr, "Cannot prepare statement: %s\n", sqlite3_errmsg(db)); + pthread_mutex_unlock(&db_mutex); return nullptr; } @@ -38,13 +41,14 @@ User *db_insert_user(const char *username, const char *email, sqlite3_bind_text(stmt, 4, group, -1, SQLITE_STATIC); } if (is_superuser) { - sqlite3_bind_int(stmt, 5, is_superuser); + sqlite3_bind_int(stmt, 5, *is_superuser); } rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { fprintf(stderr, "Execution failed: %s\n", sqlite3_errmsg(db)); sqlite3_finalize(stmt); + pthread_mutex_unlock(&db_mutex); return nullptr; } @@ -53,4 +57,7 @@ User *db_insert_user(const char *username, const char *email, sqlite3_finalize(stmt); printf("User inserted successfully. ID: %d\n", inserted_id); pthread_mutex_unlock(&db_mutex); -} \ No newline at end of file + User *new_db_user = + new_user(inserted_id, username, email, password, group, is_superuser); + return new_db_user; +} diff --git a/src/db_update_user.c b/src/db_update_user.c new file mode 100644 index 0000000..9215d22 --- /dev/null +++ b/src/db_update_user.c @@ -0,0 +1,55 @@ +#include "database.h" +#include "sql.h" +#include "user.h" + +User *db_update_user(const int userid, const char *username, const char *email, + const char *password, const char *group, + const bool *is_superuser) { + pthread_mutex_lock(&db_mutex); + if (!db) { + fprintf(stderr, "Databse not initialized\n"); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + if (!username || !email || !password || !group || !is_superuser) { + fprintf(stderr, + "username, email, password, group, is_superuser connot be empty\n"); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_prepare_v2(db, UPDATE_USER_FULL_SQL, -1, &stmt, 0); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db)); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, email, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, password, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, group, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 5, *is_superuser); + sqlite3_bind_int(stmt, 6, userid); + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + fprintf(stderr, "Execution failed: %s\n", sqlite3_errmsg(db)); + sqlite3_finalize(stmt); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + + int changes = sqlite3_changes(db); + sqlite3_finalize(stmt); + + if (changes > 0) { + printf("User updated successfully. ID: %d\n", userid); + } else { + printf("No user found with ID: %d\n", userid); + return nullptr; + } + pthread_mutex_unlock(&db_mutex); + return new_user(userid, username, email, password, group, is_superuser); +} diff --git a/src/delete_user.c b/src/delete_user.c index b35ee49..afd6e8c 100644 --- a/src/delete_user.c +++ b/src/delete_user.c @@ -10,8 +10,12 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { MG_ESC("invalid token")); return; } + cJSON *payload = jwt_get_payload(requestdata->token); + int id = cJSON_GetObjectItem(payload, "id")->valueint; + bool user_deleted = db_delete_user(id); char responsedata[2048] = {0}; - snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); + snprintf(responsedata, sizeof(responsedata), "{\"success\": %s}", + user_deleted ? "true" : "false"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); } diff --git a/src/main.c b/src/main.c index 0ed524b..5a21502 100644 --- a/src/main.c +++ b/src/main.c @@ -2,7 +2,6 @@ #include #include "database.h" #include "route.h" -#include "sql.h" #include "user.h" static void root_handler(struct mg_connection *c, struct mg_http_message *hm) { @@ -18,7 +17,7 @@ static const struct route routes[] = { {"POST", "/api/users/user", get_user}, {"POST", "/api/users/delete", delete_user}, {"POST", "/api/users/password", change_password}, - {"POST", "/api/users/change", change_password}, + {"POST", "/api/users/change", change_user}, {"POST", "/api/users/add", add_user}, {"POST", "/api/register", register_user}, {nullptr, nullptr, nullptr}}; diff --git a/src/register_user.c b/src/register_user.c index 6eadf46..0476f3e 100644 --- a/src/register_user.c +++ b/src/register_user.c @@ -25,9 +25,10 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { if (is_superuser == nullptr) { is_superuser = cJSON_CreateBool(false); } - const User *new_user_data = - new_user(0, username->valuestring, password->valuestring, email->valuestring, - group->valuestring, is_superuser->valueint); + const bool is_superuser_value = is_superuser->valueint ? true : false; + User *new_user_data = db_insert_user( + username->valuestring, email->valuestring, password->valuestring, + group->valuestring, (const bool*)is_superuser_value); if (new_user_data == nullptr) { mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); @@ -41,4 +42,5 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { new_user_data->group, new_user_data->is_superuser); mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + free_user(new_user_data); } diff --git a/src/sql.h b/src/sql.h index 88ee2fb..c77721c 100644 --- a/src/sql.h +++ b/src/sql.h @@ -24,3 +24,8 @@ #define INSERT_USER_FULL_SQL \ "insert into users (username, email, password, user_group, is_superuser) " \ "values (?, ?, ?, ?, ?)" +#define UPDATE_USER_FULL_SQL \ + "update users set username = ?, email = ?, password = ?, user_group = ?, " \ + "is_superuser = ? where id = ?" +#define DELETE_USER_SQL "delete from users where id = ?" +#define UPDATE_USER_PASSWORD_SQL "update users set password = ? where id = ?" diff --git a/src/user.c b/src/user.c index a6bd83e..1bc96b8 100644 --- a/src/user.c +++ b/src/user.c @@ -24,8 +24,8 @@ void free_request_with_token(request_with_token *data) { free(data); } -User *new_user(int id, const char *username, const char *password, const char *email, - const char *group, bool is_superuser) { +User *new_user(int id, const char *username, const char *password, + const char *email, const char *group, bool is_superuser) { User *user = malloc(sizeof(User)); if (user == NULL) { return NULL; @@ -40,9 +40,20 @@ User *new_user(int id, const char *username, const char *password, const char *e } void free_user(User *user) { - free(user->username); - free(user->password); - free(user->email); - free(user->group); + if (!user) { + return; + } + if (user->username != nullptr) { + free(user->username); + } + if (user->password != nullptr) { + free(user->password); + } + if (user->email != nullptr) { + free(user->email); + } + if (user->group != nullptr) { + free(user->group); + } free(user); } diff --git a/src/user.h b/src/user.h index d030b45..7cf3428 100644 --- a/src/user.h +++ b/src/user.h @@ -57,4 +57,8 @@ char *db_get_all_users(); User *db_get_user_by_username(const char *username); User *db_insert_user(const char *username, const char *email, const char *password, const char *group, - bool *is_superuser); + const bool *is_superuser); +User *db_update_user(const int userid, const char *username, const char *email, + const char *password, const char *group, + const bool *is_superuser); +bool db_delete_user(const int id); From 7d04b0078f94f446da67567950f4864702e3722b Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 23 Aug 2025 13:05:40 +0000 Subject: [PATCH 03/30] fix POST requests to DELETE and PUT and fix double-free password in change_user.c --- .gitignore | 3 ++- src/add_user.c | 8 ++++---- src/change_password.c | 8 ++++---- src/change_user.c | 12 ++++++------ src/delete_user.c | 2 +- src/get_all_users.c | 6 +++--- src/get_token.c | 12 ++++++------ src/get_user.c | 4 ++-- src/main.c | 4 ++-- src/register_user.c | 6 +++--- src/verify_token.c | 6 +++--- web/index.html | 21 +++++++++++++++++++++ 12 files changed, 57 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 5328952..af6ce43 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ build builddir fmongoose -users.db \ No newline at end of file +users.db +*.swp diff --git a/src/add_user.c b/src/add_user.c index 9741e2d..b089abd 100644 --- a/src/add_user.c +++ b/src/add_user.c @@ -6,7 +6,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -18,7 +18,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); if (username == nullptr || password == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); return; @@ -27,7 +27,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { bool jwt_is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!jwt_is_superuser) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("not authorized")); cJSON_Delete(payload); return; @@ -38,7 +38,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!new_db_user) { - mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); cJSON_Delete(payload); return; diff --git a/src/change_password.c b/src/change_password.c index 8e1aa1b..2eb3c8b 100644 --- a/src/change_password.c +++ b/src/change_password.c @@ -6,7 +6,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -14,7 +14,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { cJSON *old_password = cJSON_GetObjectItem(jsonbody, "old_password"); cJSON *new_password = cJSON_GetObjectItem(jsonbody, "new_password"); if (old_password == nullptr || new_password == nullptr) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , ")); return; } @@ -24,7 +24,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char *password = old_user_data->password; if (strcmp(password, old_password->valuestring)) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid old password")); return; } @@ -33,7 +33,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { old_user_data->id, old_user_data->username, old_user_data->email, new_password->valuestring, old_user_data->group, &old_is_superuser); if (!new_user_data) { - mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user")); return; } diff --git a/src/change_user.c b/src/change_user.c index f64f8d9..e9a2574 100644 --- a/src/change_user.c +++ b/src/change_user.c @@ -6,7 +6,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -17,19 +17,19 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); User *old_user_data = db_get_user_by_username(username->valuestring); if (!old_user_data) { - mg_http_reply(c, 404, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 404, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user not found")); return; } char *password = old_user_data->password; if (!password) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("password is required")); return; } if (username == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , ")); free_user(old_user_data); @@ -44,7 +44,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!updated_user_data) { - mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user")); cJSON_Delete(payload); free_user(old_user_data); @@ -63,5 +63,5 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { free_user(updated_user_data); free_user(old_user_data); cJSON_Delete(payload); - free(password); + //free(password); } diff --git a/src/delete_user.c b/src/delete_user.c index afd6e8c..9ad4db6 100644 --- a/src/delete_user.c +++ b/src/delete_user.c @@ -6,7 +6,7 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } diff --git a/src/get_all_users.c b/src/get_all_users.c index e8e1308..8163d7d 100644 --- a/src/get_all_users.c +++ b/src/get_all_users.c @@ -5,18 +5,18 @@ void get_all_users(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *data = check_body(hm); if (data->error != NULL) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); return; } if (!jwt_verifyJWT(data->token, JWT_DEFAULT_SECRET)) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } cJSON *payload = jwt_get_payload(data->token); bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); return; } diff --git a/src/get_token.c b/src/get_token.c index 5522a10..46c4109 100644 --- a/src/get_token.c +++ b/src/get_token.c @@ -3,7 +3,7 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have username and password")); return; } @@ -13,21 +13,21 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { if (username == NULL || password == NULL) { const char *error_ptr = cJSON_GetErrorPtr(); if (error_ptr != NULL) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), MG_ESC(error_ptr)); + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(error_ptr)); return; } - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have username and password")); return; } User *db_user = db_get_user_by_username(username->valuestring); if (!db_user) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("couldn't find user")); return; } if (strcmp(db_user->password, password->valuestring)) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("wrong password")); return; } @@ -41,7 +41,7 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { db_user->id, db_user->username, db_user->email, db_user->group, db_user->is_superuser ? "true" : "false"); char *token = jwt_createJWT(header, payload, JWT_DEFAULT_SECRET); - mg_http_reply(c, 200, "", "{%m:%m}", MG_ESC("token"), MG_ESC(token)); + mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("token"), MG_ESC(token)); free_user(db_user); free(token); } diff --git a/src/get_user.c b/src/get_user.c index 0a26199..3449b7e 100644 --- a/src/get_user.c +++ b/src/get_user.c @@ -6,7 +6,7 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); if (!jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET)) { free_request_with_token(requestdata); - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -18,7 +18,7 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { free_request_with_token(requestdata); cJSON_Delete(payload); free(username); - mg_http_reply(c, 404, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 404, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user not found")); return; } diff --git a/src/main.c b/src/main.c index 5a21502..99362e7 100644 --- a/src/main.c +++ b/src/main.c @@ -15,9 +15,9 @@ static const struct route routes[] = { {"POST", "/api/token/verify", verify_token}, {"POST", "/api/users", get_all_users}, {"POST", "/api/users/user", get_user}, - {"POST", "/api/users/delete", delete_user}, + {"DELETE", "/api/users", delete_user}, {"POST", "/api/users/password", change_password}, - {"POST", "/api/users/change", change_user}, + {"PUT", "/api/users", change_user}, {"POST", "/api/users/add", add_user}, {"POST", "/api/register", register_user}, {nullptr, nullptr, nullptr}}; diff --git a/src/register_user.c b/src/register_user.c index 0476f3e..426108f 100644 --- a/src/register_user.c +++ b/src/register_user.c @@ -2,7 +2,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); return; @@ -14,7 +14,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { cJSON *group = cJSON_GetObjectItem(jsonbody, "group"); cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); if (username == nullptr || password == nullptr || email == nullptr) { - mg_http_reply(c, 400, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); return; @@ -30,7 +30,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { username->valuestring, email->valuestring, password->valuestring, group->valuestring, (const bool*)is_superuser_value); if (new_user_data == nullptr) { - mg_http_reply(c, 500, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); return; } diff --git a/src/verify_token.c b/src/verify_token.c index 4c605e6..e9a5014 100644 --- a/src/verify_token.c +++ b/src/verify_token.c @@ -5,14 +5,14 @@ void verify_token(struct mg_connection *c, struct mg_http_message *hm) { const request_with_token *data = check_body(hm); if (data->error != nullptr) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); return; } const bool valid = jwt_verifyJWT(data->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } - mg_http_reply(c, 200, "", "{%m:%m}", MG_ESC("valid"), MG_ESC("true")); + mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("valid"), MG_ESC("true")); } diff --git a/web/index.html b/web/index.html index 647e2a1..7c1b18f 100644 --- a/web/index.html +++ b/web/index.html @@ -5,7 +5,28 @@ fmongoose +

it's work

+ + From e2b06fbc5d138277a948d20dc21698234c0f42e4 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sun, 24 Aug 2025 14:38:58 +0700 Subject: [PATCH 04/30] updated project name and added actions.yaml --- .gitea/workflows/actions.yaml | 45 +++++++++++++++++++++++++++++++++++ .gitignore | 2 +- CMakeLists.txt | 6 ++--- Makefile | 2 +- meson.build | 4 ++-- web/index.html | 11 +++++---- 6 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 .gitea/workflows/actions.yaml diff --git a/.gitea/workflows/actions.yaml b/.gitea/workflows/actions.yaml new file mode 100644 index 0000000..ab162ef --- /dev/null +++ b/.gitea/workflows/actions.yaml @@ -0,0 +1,45 @@ +name: Prepare Build and Testing executable binary +run-name: ${{ gitea.actor }} +on: [push] + +jobs: + Prepare-Build-Testing-With-Make: + runs-on: alpine-sdk + steps: + - name: prepare-container + run: | + echo "192.168.3.113 gitea.home.com" >> /etc/hosts + - name: Check out repository code + uses: actions/checkout@v3 + - name: Building binary with make + run: make -j$(nproc) + + Prepare-Build-Testing-With-CMake: + runs-on: alpine-sdk + steps: + - name: prepare-container + run: | + echo "192.168.3.113 gitea.home.com" >> /etc/hosts && \ + apk add cmake + - name: Check out repository code + uses: actions/checkout@v3 + - name: Building binary with CMake + run: | + mkdir build && \ + cd build && \ + cmake .. && \ + make -j$(nproc) + + Prepare-Build-Testing-With-Meson: + runs-on: alpine-sdk + steps: + - name: prepare-container + run: | + echo "192.168.3.113 gitea.home.com" >> /etc/hosts && \ + apk add meson ninja cmake + - name: Check out repository code + uses: actions/checkout@v3 + - name: Building binary with meson + run: | + meson setup build && \ + ninja -C build diff --git a/.gitignore b/.gitignore index af6ce43..8f6ff03 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ .idea build builddir -fmongoose +auth_service users.db *.swp diff --git a/CMakeLists.txt b/CMakeLists.txt index e1914cd..115c0f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required (VERSION 3.6) -project(fmongoose LANGUAGES C) +project(auth_service LANGUAGES C) set(CMAKE_C_STANDARD 23) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -11,8 +11,8 @@ FILE(GLOB_RECURSE SOURCES src/*.c) find_package(SQLite3 REQUIRED) add_executable( - fmongoose + auth_service ${SOURCES} ) -target_link_libraries(fmongoose ssl crypto sqlite3 pthread) +target_link_libraries(auth_service ssl crypto sqlite3 pthread) diff --git a/Makefile b/Makefile index f267588..2f0da38 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ LIBS = -lpthread -lcrypto -lsqlite3 INCLUDES = -I./src/lib -I./src SRCS = $(wildcard src/*.c src/lib/*.c) OBJS = $(SRCS:.c=.o) -TARGET = fmongoose +TARGET = auth_service BUILD_DIR=./build CROSS_COMPILE = diff --git a/meson.build b/meson.build index 85c3ed0..f111476 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,8 @@ -project('fmongoose', 'c', default_options: ['c_std=gnu2x']) +project('auth_service', 'c', default_options: ['c_std=gnu2x']) include_dirs = include_directories('src', 'src/lib') deps = [dependency('openssl'), dependency('sqlite3')] executable( - 'fmongoose', + 'auth_service', 'src/main.c', 'src/user.c', 'src/add_user.c', diff --git a/web/index.html b/web/index.html index 7c1b18f..abd3506 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - fmongoose + auth_service

it's work

- + +
+
+

Add User

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+

Change User Data

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+

Are you sure?

+
+ + +
+
+
+ +
+
+

Change Password

+
+ + +
+
+ + +
+
+
+ +
+
+

+
+
+ +
+ + + + + + + + + + + + + + +
idusernameemailgroupis_superuserdeletechange passwordchange user data
+
+ + + + + + + + + + + + + diff --git a/web/js/add_user.js b/web/js/add_user.js new file mode 100644 index 0000000..c43e3a4 --- /dev/null +++ b/web/js/add_user.js @@ -0,0 +1,46 @@ +const adduserbutton = document.querySelector('#adduserbutton') +const adduserform = document.querySelector('.adduserform') +const canceladduserbutton = document.querySelector('#canceladduser') +const adduserformbody = document.querySelector('.adduserformbody') +const usernameinput = document.querySelector('#adduserformusername') +const passwordinput = document.querySelector('#adduserformpassword') +const groupinput = document.querySelector('#adduserformgroup') +const is_superuserinput = document.querySelector('#adduserformissuperuser') +const emailinput = document.querySelector('#adduserformemail') + +adduserbutton.addEventListener('click', () => { + adduserform.classList.add('active') +}) + +canceladduserbutton.addEventListener('click', (e) => { + e.preventDefault() + adduserform.classList.remove('active') +}) + +adduserformbody.addEventListener('submit', async (e) => { + e.preventDefault() + const hashedpassword = await hashPassword(passwordinput.value) + const new_user = { + username: usernameinput.value, + email: emailinput.value, + password: hashedpassword, + group: groupinput.value, + is_superuser: is_superuserinput.checked + } + const status = await fetch(routes.adduser(), { + method: 'POST', + body: `{"token":"${localStorage.getItem('token')}","username":"${new_user.username}","email":"${new_user.email}","password":"${new_user.password}","group":"${new_user.group}","is_superuser":"${new_user.is_superuser}"}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + if (!status.error) { + adduserform.classList.remove('active') + await get_all_users() + notification("Пользователь успешно добавлен", "success") + usernameinput.value = "" + passwordinput.value = "" + groupinput.value = "" + } else { + notification(status.error, "error") + } +}) diff --git a/web/js/bootstrap.min.js b/web/js/bootstrap.min.js new file mode 100644 index 0000000..d4f1193 --- /dev/null +++ b/web/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.5 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&P(t,e,i,r.callable,r.delegationSelector)}function M(t){return t=t.replace(E,""),$[t]||t}const j={on(t,e,i,s){N(t,e,i,s,!1)},one(t,e,i,s){N(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=D(e,i,s),a=r!==e,l=O(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))x(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(C,"");a&&!e.includes(n)||P(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;P(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f();let n=null,o=!0,r=!0,a=!1;e!==M(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=F(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function F(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function z(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function H(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${H(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${H(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=z(t.dataset[s])}return e},getDataAttribute:(t,e)=>z(t.getAttribute(`data-bs-${H(e)}`))};class q{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=l(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...l(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[s,n]of Object.entries(e)){const e=t[s],o=l(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${o}" but expected type "${n}".`)}var i}}class W extends q{constructor(t,e){super(),(t=c(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){y(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(c(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.5"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const R=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>r(t))).join(","):null},K={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!d(t)&&h(t)))},getSelectorFromElement(t){const e=R(t);return e&&K.findOne(e)?e:null},getElementFromSelector(t){const e=R(t);return e?K.findOne(e):null},getMultipleElementsFromSelector(t){const e=R(t);return e?K.find(e):[]}},V=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=K.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))},Q=".bs.alert",X=`close${Q}`,Y=`closed${Q}`;class U extends W{static get NAME(){return"alert"}close(){if(j.trigger(this._element,X).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,Y),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=U.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}V(U,"close"),b(U);const G='[data-bs-toggle="button"]';class J extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=J.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}j.on(document,"click.bs.button.data-api",G,(t=>{t.preventDefault();const e=t.target.closest(G);J.getOrCreateInstance(e).toggle()})),b(J);const Z=".bs.swipe",tt=`touchstart${Z}`,et=`touchmove${Z}`,it=`touchend${Z}`,st=`pointerdown${Z}`,nt=`pointerup${Z}`,ot={endCallback:null,leftCallback:null,rightCallback:null},rt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class at extends q{constructor(t,e){super(),this._element=t,t&&at.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"swipe"}dispose(){j.off(this._element,Z)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),v(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&v(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(j.on(this._element,st,(t=>this._start(t))),j.on(this._element,nt,(t=>this._end(t))),this._element.classList.add("pointer-event")):(j.on(this._element,tt,(t=>this._start(t))),j.on(this._element,et,(t=>this._move(t))),j.on(this._element,it,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const lt=".bs.carousel",ct=".data-api",ht="ArrowLeft",dt="ArrowRight",ut="next",_t="prev",gt="left",ft="right",mt=`slide${lt}`,pt=`slid${lt}`,bt=`keydown${lt}`,vt=`mouseenter${lt}`,yt=`mouseleave${lt}`,wt=`dragstart${lt}`,At=`load${lt}${ct}`,Et=`click${lt}${ct}`,Ct="carousel",Tt="active",kt=".active",$t=".carousel-item",St=kt+$t,Lt={[ht]:ft,[dt]:gt},Ot={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},It={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Dt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=K.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Ct&&this.cycle()}static get Default(){return Ot}static get DefaultType(){return It}static get NAME(){return"carousel"}next(){this._slide(ut)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(_t)}pause(){this._isSliding&&a(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?j.one(this._element,pt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,pt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?ut:_t;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&j.on(this._element,bt,(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,vt,(()=>this.pause())),j.on(this._element,yt,(()=>this._maybeEnableCycle()))),this._config.touch&&at.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of K.find(".carousel-item img",this._element))j.on(t,wt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(gt)),rightCallback:()=>this._slide(this._directionToOrder(ft)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new at(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Lt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=K.findOne(kt,this._indicatorsElement);e.classList.remove(Tt),e.removeAttribute("aria-current");const i=K.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Tt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===ut,n=e||w(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>j.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(mt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),g(n),i.classList.add(l),n.classList.add(l),this._queueCallback((()=>{n.classList.remove(l,c),n.classList.add(Tt),i.classList.remove(Tt,c,l),this._isSliding=!1,r(pt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return K.findOne(St,this._element)}_getItems(){return K.find($t,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===gt?_t:ut:t===gt?ut:_t}_orderToDirection(t){return p()?t===_t?gt:ft:t===_t?ft:gt}static jQueryInterface(t){return this.each((function(){const e=Dt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}j.on(document,Et,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=K.getElementFromSelector(this);if(!e||!e.classList.contains(Ct))return;t.preventDefault();const i=Dt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),j.on(window,At,(()=>{const t=K.find('[data-bs-ride="carousel"]');for(const e of t)Dt.getOrCreateInstance(e)})),b(Dt);const Nt=".bs.collapse",Pt=`show${Nt}`,xt=`shown${Nt}`,Mt=`hide${Nt}`,jt=`hidden${Nt}`,Ft=`click${Nt}.data-api`,zt="show",Ht="collapse",Bt="collapsing",qt=`:scope .${Ht} .${Ht}`,Wt='[data-bs-toggle="collapse"]',Rt={parent:null,toggle:!0},Kt={parent:"(null|element)",toggle:"boolean"};class Vt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=K.find(Wt);for(const t of i){const e=K.getSelectorFromElement(t),i=K.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Rt}static get DefaultType(){return Kt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Vt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(j.trigger(this._element,Pt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ht),this._element.classList.add(Bt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht,zt),this._element.style[e]="",j.trigger(this._element,xt)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,Mt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,g(this._element),this._element.classList.add(Bt),this._element.classList.remove(Ht,zt);for(const t of this._triggerArray){const e=K.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht),j.trigger(this._element,jt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(zt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=c(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Wt);for(const e of t){const t=K.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=K.find(qt,this._config.parent);return K.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Vt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,Ft,Wt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of K.getMultipleElementsFromSelector(this))Vt.getOrCreateInstance(t,{toggle:!1}).toggle()})),b(Vt);const Qt="dropdown",Xt=".bs.dropdown",Yt=".data-api",Ut="ArrowUp",Gt="ArrowDown",Jt=`hide${Xt}`,Zt=`hidden${Xt}`,te=`show${Xt}`,ee=`shown${Xt}`,ie=`click${Xt}${Yt}`,se=`keydown${Xt}${Yt}`,ne=`keyup${Xt}${Yt}`,oe="show",re='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ae=`${re}.${oe}`,le=".dropdown-menu",ce=p()?"top-end":"top-start",he=p()?"top-start":"top-end",de=p()?"bottom-end":"bottom-start",ue=p()?"bottom-start":"bottom-end",_e=p()?"left-start":"right-start",ge=p()?"right-start":"left-start",fe={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},me={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class pe extends W{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=K.next(this._element,le)[0]||K.prev(this._element,le)[0]||K.findOne(le,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return fe}static get DefaultType(){return me}static get NAME(){return Qt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!j.trigger(this._element,te,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(oe),this._element.classList.add(oe),j.trigger(this._element,ee,t)}}hide(){if(d(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!j.trigger(this._element,Jt,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._popper&&this._popper.destroy(),this._menu.classList.remove(oe),this._element.classList.remove(oe),this._element.setAttribute("aria-expanded","false"),B.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,Zt,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Qt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:l(this._config.reference)?t=c(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(oe)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return _e;if(t.classList.contains("dropstart"))return ge;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?he:ce:e?ue:de}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...v(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=K.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>h(t)));i.length&&w(i,e,t===Gt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=pe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=K.find(ae);for(const i of e){const e=pe.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Ut,Gt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(re)?this:K.prev(this,re)[0]||K.next(this,re)[0]||K.findOne(re,t.delegateTarget.parentNode),o=pe.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}j.on(document,se,re,pe.dataApiKeydownHandler),j.on(document,se,le,pe.dataApiKeydownHandler),j.on(document,ie,pe.clearMenus),j.on(document,ne,pe.clearMenus),j.on(document,ie,re,(function(t){t.preventDefault(),pe.getOrCreateInstance(this).toggle()})),b(pe);const be="backdrop",ve="show",ye=`mousedown.bs.${be}`,we={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ae={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ee extends q{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return we}static get DefaultType(){return Ae}static get NAME(){return be}show(t){if(!this._config.isVisible)return void v(t);this._append();const e=this._getElement();this._config.isAnimated&&g(e),e.classList.add(ve),this._emulateAnimation((()=>{v(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(ve),this._emulateAnimation((()=>{this.dispose(),v(t)}))):v(t)}dispose(){this._isAppended&&(j.off(this._element,ye),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=c(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),j.on(t,ye,(()=>{v(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const Ce=".bs.focustrap",Te=`focusin${Ce}`,ke=`keydown.tab${Ce}`,$e="backward",Se={autofocus:!0,trapElement:null},Le={autofocus:"boolean",trapElement:"element"};class Oe extends q{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Se}static get DefaultType(){return Le}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),j.off(document,Ce),j.on(document,Te,(t=>this._handleFocusin(t))),j.on(document,ke,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,Ce))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=K.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===$e?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?$e:"forward")}}const Ie=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",De=".sticky-top",Ne="padding-right",Pe="margin-right";class xe{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Ne,(e=>e+t)),this._setElementAttributes(Ie,Ne,(e=>e+t)),this._setElementAttributes(De,Pe,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ne),this._resetElementAttributes(Ie,Ne),this._resetElementAttributes(De,Pe)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(l(t))e(t);else for(const i of K.find(t,this._element))e(i)}}const Me=".bs.modal",je=`hide${Me}`,Fe=`hidePrevented${Me}`,ze=`hidden${Me}`,He=`show${Me}`,Be=`shown${Me}`,qe=`resize${Me}`,We=`click.dismiss${Me}`,Re=`mousedown.dismiss${Me}`,Ke=`keydown.dismiss${Me}`,Ve=`click${Me}.data-api`,Qe="modal-open",Xe="show",Ye="modal-static",Ue={backdrop:!0,focus:!0,keyboard:!0},Ge={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Je extends W{constructor(t,e){super(t,e),this._dialog=K.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new xe,this._addEventListeners()}static get Default(){return Ue}static get DefaultType(){return Ge}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,He,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Qe),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(j.trigger(this._element,je).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Xe),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){j.off(window,Me),j.off(this._dialog,Me),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ee({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=K.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),g(this._element),this._element.classList.add(Xe),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,Be,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){j.on(this._element,Ke,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),j.on(window,qe,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),j.on(this._element,Re,(t=>{j.one(this._element,We,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Qe),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,ze)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,Fe).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Ye)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Ye),this._queueCallback((()=>{this._element.classList.remove(Ye),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,Ve,'[data-bs-toggle="modal"]',(function(t){const e=K.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,He,(t=>{t.defaultPrevented||j.one(e,ze,(()=>{h(this)&&this.focus()}))}));const i=K.findOne(".modal.show");i&&Je.getInstance(i).hide(),Je.getOrCreateInstance(e).toggle(this)})),V(Je),b(Je);const Ze=".bs.offcanvas",ti=".data-api",ei=`load${Ze}${ti}`,ii="show",si="showing",ni="hiding",oi=".offcanvas.show",ri=`show${Ze}`,ai=`shown${Ze}`,li=`hide${Ze}`,ci=`hidePrevented${Ze}`,hi=`hidden${Ze}`,di=`resize${Ze}`,ui=`click${Ze}${ti}`,_i=`keydown.dismiss${Ze}`,gi={backdrop:!0,keyboard:!0,scroll:!1},fi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class mi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return gi}static get DefaultType(){return fi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,ri,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new xe).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(si),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(ii),this._element.classList.remove(si),j.trigger(this._element,ai,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,li).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ni),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(ii,ni),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new xe).reset(),j.trigger(this._element,hi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ee({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():j.trigger(this._element,ci)}:null})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_addEventListeners(){j.on(this._element,_i,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():j.trigger(this._element,ci))}))}static jQueryInterface(t){return this.each((function(){const e=mi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,ui,'[data-bs-toggle="offcanvas"]',(function(t){const e=K.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;j.one(e,hi,(()=>{h(this)&&this.focus()}));const i=K.findOne(oi);i&&i!==e&&mi.getInstance(i).hide(),mi.getOrCreateInstance(e).toggle(this)})),j.on(window,ei,(()=>{for(const t of K.find(oi))mi.getOrCreateInstance(t).show()})),j.on(window,di,(()=>{for(const t of K.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&mi.getOrCreateInstance(t).hide()})),V(mi),b(mi);const pi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},bi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),vi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,yi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!bi.has(i)||Boolean(vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},wi={allowList:pi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Ai={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ei={entry:"(string|element|function|null)",selector:"(string|element)"};class Ci extends q{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return wi}static get DefaultType(){return Ai}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ei)}_setContent(t,e,i){const s=K.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?l(e)?this._putElementInTemplate(c(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)yi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return v(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ti=new Set(["sanitize","allowList","sanitizeFn"]),ki="fade",$i="show",Si=".tooltip-inner",Li=".modal",Oi="hide.bs.modal",Ii="hover",Di="focus",Ni={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},Pi={allowList:pi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},xi={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Mi extends W{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Pi}static get DefaultType(){return xi}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(Li),Oi,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.eventName("show")),e=(u(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),j.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._queueCallback((()=>{j.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!j.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._activeTrigger.click=!1,this._activeTrigger[Di]=!1,this._activeTrigger[Ii]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ki,$i),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ki),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ci({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[Si]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ki)}_isShown(){return this.tip&&this.tip.classList.contains($i)}_createPopper(t){const e=v(this._config.placement,[this,t,this._element]),s=Ni[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return v(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...v(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)j.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===Ii?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Ii?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");j.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Di:Ii]=!0,e._enter()})),j.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Di:Ii]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(Li),Oi,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ti.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:c(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=Mi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Mi);const ji=".popover-header",Fi=".popover-body",zi={...Mi.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Hi={...Mi.DefaultType,content:"(null|string|element|function)"};class Bi extends Mi{static get Default(){return zi}static get DefaultType(){return Hi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ji]:this._getTitle(),[Fi]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=Bi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Bi);const qi=".bs.scrollspy",Wi=`activate${qi}`,Ri=`click${qi}`,Ki=`load${qi}.data-api`,Vi="active",Qi="[href]",Xi=".nav-link",Yi=`${Xi}, .nav-item > ${Xi}, .list-group-item`,Ui={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Gi={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ji extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Ui}static get DefaultType(){return Gi}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=c(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(j.off(this._config.target,Ri),j.on(this._config.target,Ri,Qi,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=K.find(Qi,this._config.target);for(const e of t){if(!e.hash||d(e))continue;const t=K.findOne(decodeURI(e.hash),this._element);h(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Vi),this._activateParents(t),j.trigger(this._element,Wi,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))K.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Vi);else for(const e of K.parents(t,".nav, .list-group"))for(const t of K.prev(e,Yi))t.classList.add(Vi)}_clearActiveClass(t){t.classList.remove(Vi);const e=K.find(`${Qi}.${Vi}`,t);for(const t of e)t.classList.remove(Vi)}static jQueryInterface(t){return this.each((function(){const e=Ji.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,Ki,(()=>{for(const t of K.find('[data-bs-spy="scroll"]'))Ji.getOrCreateInstance(t)})),b(Ji);const Zi=".bs.tab",ts=`hide${Zi}`,es=`hidden${Zi}`,is=`show${Zi}`,ss=`shown${Zi}`,ns=`click${Zi}`,os=`keydown${Zi}`,rs=`load${Zi}`,as="ArrowLeft",ls="ArrowRight",cs="ArrowUp",hs="ArrowDown",ds="Home",us="End",_s="active",gs="fade",fs="show",ms=".dropdown-toggle",ps=`:not(${ms})`,bs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',vs=`.nav-link${ps}, .list-group-item${ps}, [role="tab"]${ps}, ${bs}`,ys=`.${_s}[data-bs-toggle="tab"], .${_s}[data-bs-toggle="pill"], .${_s}[data-bs-toggle="list"]`;class ws extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),j.on(this._element,os,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?j.trigger(e,ts,{relatedTarget:t}):null;j.trigger(t,is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(_s),this._activate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),j.trigger(t,ss,{relatedTarget:e})):t.classList.add(fs)}),t,t.classList.contains(gs)))}_deactivate(t,e){t&&(t.classList.remove(_s),t.blur(),this._deactivate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),j.trigger(t,es,{relatedTarget:e})):t.classList.remove(fs)}),t,t.classList.contains(gs)))}_keydown(t){if(![as,ls,cs,hs,ds,us].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!d(t)));let i;if([ds,us].includes(t.key))i=e[t.key===ds?0:e.length-1];else{const s=[ls,hs].includes(t.key);i=w(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),ws.getOrCreateInstance(i).show())}_getChildren(){return K.find(vs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=K.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=K.findOne(t,i);n&&n.classList.toggle(s,e)};s(ms,_s),s(".dropdown-menu",fs),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(_s)}_getInnerElement(t){return t.matches(vs)?t:K.findOne(vs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=ws.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,ns,bs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||ws.getOrCreateInstance(this).show()})),j.on(window,rs,(()=>{for(const t of K.find(ys))ws.getOrCreateInstance(t)})),b(ws);const As=".bs.toast",Es=`mouseover${As}`,Cs=`mouseout${As}`,Ts=`focusin${As}`,ks=`focusout${As}`,$s=`hide${As}`,Ss=`hidden${As}`,Ls=`show${As}`,Os=`shown${As}`,Is="hide",Ds="show",Ns="showing",Ps={animation:"boolean",autohide:"boolean",delay:"number"},xs={animation:!0,autohide:!0,delay:5e3};class Ms extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return xs}static get DefaultType(){return Ps}static get NAME(){return"toast"}show(){j.trigger(this._element,Ls).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Is),g(this._element),this._element.classList.add(Ds,Ns),this._queueCallback((()=>{this._element.classList.remove(Ns),j.trigger(this._element,Os),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(j.trigger(this._element,$s).defaultPrevented||(this._element.classList.add(Ns),this._queueCallback((()=>{this._element.classList.add(Is),this._element.classList.remove(Ns,Ds),j.trigger(this._element,Ss)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ds),super.dispose()}isShown(){return this._element.classList.contains(Ds)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,Es,(t=>this._onInteraction(t,!0))),j.on(this._element,Cs,(t=>this._onInteraction(t,!1))),j.on(this._element,Ts,(t=>this._onInteraction(t,!0))),j.on(this._element,ks,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Ms.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return V(Ms),b(Ms),{Alert:U,Button:J,Carousel:Dt,Collapse:Vt,Dropdown:pe,Modal:Je,Offcanvas:mi,Popover:Bi,ScrollSpy:Ji,Tab:ws,Toast:Ms,Tooltip:Mi}})); +//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/web/js/change_password.js b/web/js/change_password.js new file mode 100644 index 0000000..e742b56 --- /dev/null +++ b/web/js/change_password.js @@ -0,0 +1,34 @@ +const changepasswordform = document.querySelector('.changepasswordform') +const cancelchangepassword = document.querySelector('#cancelchangepassword') +const changepasswordformbody = document.querySelector('.changepasswordformbody') +const changepasswordnewpassword = document.querySelector('#changepasswordnewpassword') + +let changepassuserid = ""; + +const change_password = (userid) => { + changepasswordform.classList.add('active') + changepassuserid = userid +} + +cancelchangepassword.addEventListener('click', e => { + e.preventDefault() + changepasswordform.classList.remove('active') +}) + +changepasswordformbody.addEventListener('submit', async e => { + e.preventDefault() + const hashedpassword = await hashPassword(changepasswordnewpassword.value) + const status = await fetch(routes.change_password_url(), { + method: 'POST', + body: `{"token":"${localStorage.getItem('token')}","userid":${changepassuserid},"new_password":"${hashedpassword}"}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + if (!status.error) { + notification("Пароль успешно изменен", "success") + changepasswordform.classList.remove('active') + changepasswordnewpassword.value = "" + } else { + notification(status.error, "error") + } +}) diff --git a/web/js/change_user.js b/web/js/change_user.js new file mode 100644 index 0000000..4248a0e --- /dev/null +++ b/web/js/change_user.js @@ -0,0 +1,56 @@ +const changeuserform = document.querySelector('.changeuserform') +const changeuserformbody = document.querySelector('.changeuserformbody') +const changeuserformusername = document.querySelector('#changeuserformusername') +const changeuserformemail = document.querySelector('#changeuserformemail') +const changeuserformgroup = document.querySelector('#changeuserformgroup') +const changeuserformissuperuser = document.querySelector('#changeuserformissuperuser') +const cancelchangeuser = document.querySelector("#cancelchangeuser") + +let changedatauserid = ""; +let userdata = ""; + +const change_user_data = async (userid) => { + changeuserform.classList.add('active') + changedatauserid = userid + const all_users = await fetch(routes.all_users(), { + method: 'POST', + body: `{"token":"${localStorage.getItem('token')}"}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + userdata = all_users.find(usr => usr.id === userid) + changeuserformusername.value = userdata.username + changeuserformgroup.value = userdata.group + changeuserformissuperuser.checked = userdata.is_superuser === "1" + changeuserformemail.value = userdata.email +} + +cancelchangeuser.addEventListener('click', (e) => { + e.preventDefault() + changeuserform.classList.remove('active') +}) + +changeuserformbody.addEventListener('submit', async (e) => { + e.preventDefault() + let request = { + token: localStorage.getItem('token'), + userid: changedatauserid, + username: changeuserformusername.value, + email: changeuserformemail.value, + is_superuser: changeuserformissuperuser.checked, + group: changeuserformgroup.value + }; + const status = await fetch(routes.change_user(), { + method: 'PUT', + body: JSON.stringify(request) + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + if (!status.error) { + notification("Данные пользователя успешно изменены", "success") + changeuserform.classList.remove('active') + await get_all_users() + } else { + notification(status.error, "error") + } +}) diff --git a/web/js/del_user.js b/web/js/del_user.js new file mode 100644 index 0000000..2e46195 --- /dev/null +++ b/web/js/del_user.js @@ -0,0 +1,41 @@ +const userstablebody = document.querySelector('#alluserstablebody') +const confirmform = document.querySelector('.confirmform') +const confirmformcancel = document.querySelector('#no') +const confirmformok = document.querySelector('#yes') + +let useridtodelete = ""; + +userstablebody.addEventListener('click', e => { + if (e.target.classList.contains('deleteuser')) { + useridtodelete = e.target.id + confirmform.classList.add("active") + } + if (e.target.classList.contains('changepassword')) { + change_password(e.target.id) + } + if (e.target.classList.contains('changeuser')) { + change_user_data(parseInt(e.target.id)) + } +}) + +confirmformcancel.addEventListener('click', () => { + useridtodelete = "" + confirmform.classList.remove('active') +}) + +confirmformok.addEventListener('click', async () => { + const status = await fetch(routes.deluser(), { + method: 'DELETE', + body: `{"token":"${localStorage.getItem('token')}","userid":${useridtodelete}}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + if (status.success) { + confirmform.classList.remove('active') + await get_all_users() + notification("Пользователь успешно удален", "success") + } else { + notification("Can't delete user", "error") + confirmform.classList.remove('active') + } +}) diff --git a/web/js/get_all_users.js b/web/js/get_all_users.js new file mode 100644 index 0000000..087c6f7 --- /dev/null +++ b/web/js/get_all_users.js @@ -0,0 +1,24 @@ +const get_all_users = async () => { + const response = await fetch(routes.all_users(), { + method: "POST", + body: `{"token":"${localStorage.getItem("token")}"}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + const alluserstablebody = document.querySelector('#alluserstablebody') + alluserstablebody.innerHTML = "" + for (const user of response) { + alluserstablebody.innerHTML += ` + +

${user.id}

+ ${user.username} + ${user.email} + ${user.group} + ${user.is_superuser} + + + + + ` + } +} diff --git a/web/js/login.js b/web/js/login.js new file mode 100644 index 0000000..a2af6da --- /dev/null +++ b/web/js/login.js @@ -0,0 +1,83 @@ +const loginform = document.querySelector('#loginform') +const loginformbody = document.querySelector('#loginformbody') +const loginforminputusername = document.querySelector('#loginforminputusername') +const loginforminputpassword = document.querySelector('#loginforminputpassword') +const exitbutton = document.querySelector('#exitbutton') +const usernameheader = document.querySelector('.usernameheader') + +if (localStorage.getItem("token") === null) { + loginform.classList.add("active") +} + +loginformbody.addEventListener('submit', (e) => { + e.preventDefault() + hashPassword(loginforminputpassword.value).then(hashedpassword => { + login(loginforminputusername.value, hashedpassword) + }) +}) + +const login = async (username, password) => { + const response = await fetch(routes.login(), { + method: 'POST', + headers: { + Accept: 'application/json', + // 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }, + // mode: 'no-cors', + body: `{"username":"${username}","password":"${password}"}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + if (error.message === "Unexpected end of JSON input") { + notification(`Ошибка на сервере: Создайте файл users.json и запишите в него первого пользователя`, "error") + } else { + notification(`Ошибка на сервере: ${error}`, "error") + } + }) + if (response.error) { + loginforminputusername.classList.add('is-invalid') + loginforminputpassword.classList.add('is-invalid') + notification("Неправильный логин или пароль", "error") + } + if (response.token) { + notification("Успешно", "success") + loginform.classList.remove("active") + localStorage.setItem("token", response.token) + await get_user_data() + loginforminputusername.value = "" + loginforminputpassword.value = "" + loginforminputusername.classList.remove('is-invalid') + loginforminputpassword.classList.remove('is-invalid') + } +} + +const get_user_data = async () => { + const response = await fetch(routes.user(), { + method: "POST", + body: `{"token":"${localStorage.getItem("token")}"}` + }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + if (response.is_superuser) { + await get_all_users() + usernameheader.innerHTML = response.username + } else { + document.querySelector('#alluserstablebody').innerHTML = "" + loginform.classList.add("active") + notification("Вы не можете войти потому что не являетесь администратором", "error") + localStorage.clear() + } + if (response.id) { + localStorage.setItem("userdata", JSON.stringify(response)) + } +} + +message.addEventListener('click', () => { + message.classList.remove("active") +}) + +exitbutton.addEventListener('click', () => { + localStorage.removeItem('token') + localStorage.removeItem('userdata') + loginform.classList.add('active') + notification('Вы вышли', "success") +}) diff --git a/web/js/message.js b/web/js/message.js new file mode 100644 index 0000000..8014faa --- /dev/null +++ b/web/js/message.js @@ -0,0 +1,18 @@ +const message = document.querySelector('.message') +const messagetext = document.querySelector('.messagetext') + +const notification = (text, type) => { + if (type === "error") { + message.classList.remove('success') + message.classList.add('error') + } else { + message.classList.remove('error') + message.classList.add('success') + } + messagetext.innerHTML = text + message.classList.add('active') + setTimeout(() => { + message.classList.remove('active') + }, 2000) +} + diff --git a/web/js/routes.js b/web/js/routes.js new file mode 100644 index 0000000..14d2dc9 --- /dev/null +++ b/web/js/routes.js @@ -0,0 +1,13 @@ +const host = window.location.origin +const prefix = 'api' + +const routes = { + login: () => [host, prefix, 'token'].join('/'), + user: () => [host, prefix, 'users', 'user'].join('/'), + all_users: () => [host, prefix, 'users'].join('/'), + access: () => [host, prefix, 'token', 'verify'].join('/'), + adduser: () => [host, prefix, 'users', 'add'].join('/'), + deluser: () => [host, prefix, 'users'].join('/'), + change_password_url: () => [host, prefix, 'users', 'password'].join('/'), + change_user: () => [host, prefix, 'users'].join('/') +} \ No newline at end of file diff --git a/web/js/security.js b/web/js/security.js new file mode 100644 index 0000000..a4a5b23 --- /dev/null +++ b/web/js/security.js @@ -0,0 +1,8 @@ +const hashPassword = async (password) => { + const encoder = new TextEncoder() + const data = encoder.encode(password) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray.map(b=>('00' + b.toString(16)).slice(-2)).join('') + return hashHex +} \ No newline at end of file diff --git a/web/js/theme.js b/web/js/theme.js new file mode 100644 index 0000000..c7718bd --- /dev/null +++ b/web/js/theme.js @@ -0,0 +1,11 @@ +const themecheckbox = document.querySelector('#themecheckbox') + +themecheckbox.addEventListener('click', (e) => { + if (themecheckbox.checked) { + document.documentElement.setAttribute('data-bs-theme', 'dark') + localStorage.setItem('bs-theme', 'dark') + } else { + document.documentElement.setAttribute('data-bs-theme', 'light') + localStorage.setItem('bs-theme', 'light') + } +}) diff --git a/web/js/window.js b/web/js/window.js new file mode 100644 index 0000000..2c87dc2 --- /dev/null +++ b/web/js/window.js @@ -0,0 +1,27 @@ +document.addEventListener("DOMContentLoaded", () => { + var theme = localStorage.getItem('bs-theme') + if (theme) { + document.documentElement.setAttribute('data-bs-theme', theme) + if (theme === 'dark') { + themecheckbox.checked = true + } + } + if (localStorage.getItem('token')) { + fetch(routes.access(), { + method: 'POST', + body: `{"token": "${localStorage.getItem('token')}"}` + }).then(data => data.json()).then(jsondata => { + if (jsondata.valid) { + document.querySelector('.loginform').classList.remove('active') + get_all_users() + } else { + document.querySelector('.loginform').classList.add('active') + } + }).catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error") + }) + usernameheader.innerHTML = JSON.parse(localStorage.getItem('userdata')).username + } else { + document.querySelector('.loginform').classList.add('active') + } +}) From 5840fded288f756066276bd184a9f26b2c8a5421 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Wed, 27 Aug 2025 17:02:38 +0700 Subject: [PATCH 23/30] update meson build --- meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/meson.build b/meson.build index f111476..9c71551 100644 --- a/meson.build +++ b/meson.build @@ -26,6 +26,7 @@ executable( 'src/db_insert_user.c', 'src/db_update_user.c', 'src/db_delete_user.c', + 'src/db_get_user_by_id.c', include_directories: include_dirs, dependencies: deps, ) From 28655e7b463c147d12af1c5a3ef99a658ba00415 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Thu, 28 Aug 2025 18:02:18 +0700 Subject: [PATCH 24/30] added cors headers and colors log --- src/add_user.c | 10 +++--- src/change_password.c | 20 +++++------ src/change_user.c | 16 ++++----- src/cors.c | 5 +++ src/cors.h | 5 +++ src/delete_user.c | 12 +++---- src/get_all_users.c | 6 ++-- src/get_token.c | 12 +++---- src/get_user.c | 10 +++--- src/lib/colors.h | 77 +++++++++++++++++++++++++++++++++++++++++++ src/main.c | 6 ++++ src/register_user.c | 31 +++++++++++------ src/verify_token.c | 6 ++-- 13 files changed, 160 insertions(+), 56 deletions(-) create mode 100644 src/cors.c create mode 100644 src/cors.h create mode 100755 src/lib/colors.h diff --git a/src/add_user.c b/src/add_user.c index b089abd..618d27f 100644 --- a/src/add_user.c +++ b/src/add_user.c @@ -6,7 +6,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -18,7 +18,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); if (username == nullptr || password == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); return; @@ -27,7 +27,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { bool jwt_is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!jwt_is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("not authorized")); cJSON_Delete(payload); return; @@ -38,7 +38,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!new_db_user) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); cJSON_Delete(payload); return; @@ -51,6 +51,6 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { is_superuser->valueint ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); cJSON_Delete(payload); } diff --git a/src/change_password.c b/src/change_password.c index c1b5c5f..ddcee84 100644 --- a/src/change_password.c +++ b/src/change_password.c @@ -6,7 +6,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -16,7 +16,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { cJSON *userid = cJSON_GetObjectItem(jsonbody, "userid"); if ((old_password == nullptr || new_password == nullptr) && userid == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , ")); return; @@ -27,7 +27,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char *username = cJSON_GetObjectItem(payload, "username")->valuestring; User *old_user_data = db_get_user_by_username(username); if (!old_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't get old_user_data")); cJSON_Delete(payload); @@ -35,7 +35,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char *password = old_user_data->password; if (strcmp(password, old_password->valuestring)) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid old password")); return; } @@ -44,7 +44,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { old_user_data->id, old_user_data->username, old_user_data->email, new_password->valuestring, old_user_data->group, &old_is_superuser); if (!new_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't update database")); free_user(old_user_data); cJSON_Delete(payload); @@ -54,21 +54,21 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char responsedata[64] = {0}; snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_user(old_user_data); cJSON_Delete(payload); return; } bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); cJSON_Delete(payload); return; } User *old_user_data = db_get_user_by_id(userid->valueint); if (!old_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't get old_user_data")); cJSON_Delete(payload); @@ -79,7 +79,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { old_user_data->id, old_user_data->username, old_user_data->email, new_password->valuestring, old_user_data->group, &old_is_superuser); if (!new_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't create new_user_data from root")); free_user(old_user_data); cJSON_Delete(payload); @@ -88,7 +88,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char responsedata[64] = {0}; snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_user(old_user_data); free_user(new_user_data); cJSON_Delete(payload); diff --git a/src/change_user.c b/src/change_user.c index e4c42b7..f34aeb0 100644 --- a/src/change_user.c +++ b/src/change_user.c @@ -6,7 +6,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -23,19 +23,19 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { old_user_data = db_get_user_by_id(userid->valueint); } if (!old_user_data) { - mg_http_reply(c, 404, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 404, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user not found")); return; } char *password = old_user_data->password; if (!password) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("password is required")); return; } if (username == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , ")); @@ -52,7 +52,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!updated_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user change user request")); cJSON_Delete(payload); free_user(old_user_data); @@ -66,7 +66,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { username->valuestring, email->valuestring, group->valuestring, is_superuser->valueint ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_user(updated_user_data); free_user(old_user_data); cJSON_Delete(payload); @@ -75,7 +75,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { bool req_is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!req_is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); cJSON_Delete(payload); free_user(old_user_data); @@ -99,7 +99,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { username->valuestring, email->valuestring, group->valuestring, is_superuser->valueint ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_user(updated_user_data); free_user(old_user_data); cJSON_Delete(payload); diff --git a/src/cors.c b/src/cors.c new file mode 100644 index 0000000..85dac94 --- /dev/null +++ b/src/cors.c @@ -0,0 +1,5 @@ +#include "cors.h" + +void set_cors_headers(struct mg_connection *c, struct mg_http_message *hm) { + mg_http_reply(c, 200, "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET,POST,OPTIONS,PUT,DELETE\r\nAccess-Control-Allow-Headers: Authorization,X-Custom,Content-Type", ""); +} diff --git a/src/cors.h b/src/cors.h new file mode 100644 index 0000000..a079f73 --- /dev/null +++ b/src/cors.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +void set_cors_headers(struct mg_connection *c, struct mg_http_message *hm); diff --git a/src/delete_user.c b/src/delete_user.c index c3e1cfa..6a39bb7 100644 --- a/src/delete_user.c +++ b/src/delete_user.c @@ -4,19 +4,19 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have token")); return; } request_with_token *requestdata = check_body(hm); if (requestdata->error != NULL) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(requestdata->error)); return; } const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); free_request_with_token(requestdata); return; @@ -31,14 +31,14 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { snprintf(responsedata, sizeof(responsedata), "{\"success\": %s}", user_deleted ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_request_with_token(requestdata); cJSON_Delete(payload); return; } bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); return; } @@ -47,7 +47,7 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { snprintf(responsedata, sizeof(responsedata), "{\"success\": %s}", user_deleted ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_request_with_token(requestdata); cJSON_Delete(payload); } diff --git a/src/get_all_users.c b/src/get_all_users.c index 8163d7d..dd530ca 100644 --- a/src/get_all_users.c +++ b/src/get_all_users.c @@ -5,18 +5,18 @@ void get_all_users(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *data = check_body(hm); if (data->error != NULL) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); return; } if (!jwt_verifyJWT(data->token, JWT_DEFAULT_SECRET)) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } cJSON *payload = jwt_get_payload(data->token); bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); return; } diff --git a/src/get_token.c b/src/get_token.c index 46c4109..07467ba 100644 --- a/src/get_token.c +++ b/src/get_token.c @@ -3,7 +3,7 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have username and password")); return; } @@ -13,21 +13,21 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { if (username == NULL || password == NULL) { const char *error_ptr = cJSON_GetErrorPtr(); if (error_ptr != NULL) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(error_ptr)); + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(error_ptr)); return; } - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have username and password")); return; } User *db_user = db_get_user_by_username(username->valuestring); if (!db_user) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("couldn't find user")); return; } if (strcmp(db_user->password, password->valuestring)) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("wrong password")); return; } @@ -41,7 +41,7 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { db_user->id, db_user->username, db_user->email, db_user->group, db_user->is_superuser ? "true" : "false"); char *token = jwt_createJWT(header, payload, JWT_DEFAULT_SECRET); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("token"), MG_ESC(token)); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("token"), MG_ESC(token)); free_user(db_user); free(token); } diff --git a/src/get_user.c b/src/get_user.c index c970f15..79b79d0 100644 --- a/src/get_user.c +++ b/src/get_user.c @@ -4,18 +4,18 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("must have token")); return; } request_with_token *requestdata = check_body(hm); if (requestdata->error != NULL) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(requestdata->error)); + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(requestdata->error)); return; } if (!jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET)) { free_request_with_token(requestdata); - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -27,7 +27,7 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { free_request_with_token(requestdata); cJSON_Delete(payload); free(username); - mg_http_reply(c, 404, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 404, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("user not found")); return; } @@ -38,7 +38,7 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { user->id, user->username, user->email, user->group, user->is_superuser ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); free_request_with_token(requestdata); free(username); free_user(user); diff --git a/src/lib/colors.h b/src/lib/colors.h new file mode 100755 index 0000000..98570c0 --- /dev/null +++ b/src/lib/colors.h @@ -0,0 +1,77 @@ +#ifndef C_COLORS_H +#define C_COLORS_H + +// reset +#define RESET "\033[0m" + +// regular colrs +#define GREEN "\033[0;32m" // GREEN +#define BLUE "\033[0;34m" // BLUE +#define YELLOW "\033[1;33m" // YELLOW +#define RED "\033[0;31m" // RED +#define BLACK "\033[0;30m" // BLACK +#define PURPLE "\033[0;35m" // PURPLE +#define CYAN "\033[0;36m" // CYAN +#define WHITE "\033[0;37m" // WHITE + +// bold +#define BBLACK "\033[1;30m" // BLACK +#define BRED "\033[1;31m" // RED +#define BGREEN "\033[1;32m" // GREEN +#define BYELLOW "\033[1;33m" // YELLOW +#define BBLUE "\033[1;34m" // BLUE +#define BPURPLE "\033[1;35m" // PURPLE +#define BCYAN "\033[1;36m" // CYAN +#define BWHITE "\033[1;37m" // WHITE + +// Underline +#define UBLACK "\033[4;30m" // BLACK +#define URED "\033[4;31m" // RED +#define UGREEN "\033[4;32m" // GREEN +#define UYELLOW "\033[4;33m" // YELLOW +#define UBLUE "\033[4;34m" // BLUE +#define UPURPLE "\033[4;35m" // PURPLE +#define UCYAN "\033[4;36m" // CYAN +#define UWHITE "\033[4;37m" // WHITE + +// Background +#define ON_BLACK "\033[40m" // BLACK +#define ON_RED "\033[41m" // RED +#define ON_GREEN "\033[42m" // GREEN +#define ON_YELLOW "\033[43m" // YELLOW +#define ON_BLUE "\033[44m" // BLUE +#define ON_PURPLE "\033[45m" // PURPLE +#define ON_CYAN "\033[46m" // CYAN +#define ON_WHITE "\033[47m" // WHITE + +// High Intensity +#define IBLACK "\033[0;90m" // BLACK +#define IRED "\033[0;91m" // RED +#define IGREEN "\033[0;92m" // GREEN +#define IYELLOW "\033[0;93m" // YELLOW +#define IBLUE "\033[0;94m" // BLUE +#define IPURPLE "\033[0;95m" // PURPLE +#define ICYAN "\033[0;96m" // CYAN +#define IWHITE "\033[0;97m" // WHITE + +// Bold High Intensity +#define BIBLACK "\033[1;90m" // BLACK +#define BIRED "\033[1;91m" // RED +#define BIGREEN "\033[1;92m" // GREEN +#define BIYELLOW "\033[1;93m" // YELLOW +#define BIBLUE "\033[1;94m" // BLUE +#define BIPURPLE "\033[1;95m" // PURPLE +#define BICYAN "\033[1;96m" // CYAN +#define BIWHITE "\033[1;97m" // WHITE + +// High Intensity backgrounds +#define ON_IBLACK "\033[0;100m" // BLACK +#define ON_IRED "\033[0;101m" // RED +#define ON_IGREEN "\033[0;102m" // GREEN +#define ON_IYELLOW "\033[0;103m" // YELLOW +#define ON_IBLUE "\033[0;104m" // BLUE +#define ON_IPURPLE "\033[0;105m" // PURPLE +#define ON_ICYAN "\033[0;106m" // CYAN +#define ON_IWHITE "\033[0;107m" // WHITE + +#endif // C_COLORS_H diff --git a/src/main.c b/src/main.c index 7989303..8a5219e 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,7 @@ +#include #include #include +#include "cors.h" #include "database.h" #include "route.h" #include "user.h" @@ -11,6 +13,7 @@ static void root_handler(struct mg_connection *c, struct mg_http_message *hm) { static const struct route routes[] = { {"GET", "/#", root_handler}, + {"OPTIONS", "/#", set_cors_headers}, {"POST", "/api/token", get_token}, {"POST", "/api/token/verify", verify_token}, {"POST", "/api/users", get_all_users}, @@ -30,6 +33,8 @@ void ev_handler(struct mg_connection *c, int ev, void *ev_data) { for (int i = 0; routes[i].path != nullptr; i++) { if (mg_match(hm->uri, mg_str(routes[i].path), nullptr) && mg_match(hm->method, mg_str(routes[i].method), nullptr)) { + mg_log("%s%.*s %s%.*s%s", GREEN, hm->uri.len, hm->uri.buf, BWHITE, + hm->method.len, hm->method.buf, RESET); routes[i].handler(c, hm); route_found = true; break; @@ -48,6 +53,7 @@ int main(void) { db_get_users_from_file("users.json"); struct mg_mgr mgr; + mg_log_set(MG_LL_DEBUG); mg_mgr_init(&mgr); mg_http_listen(&mgr, "http://0.0.0.0:8222", ev_handler, nullptr); while (true) { diff --git a/src/register_user.c b/src/register_user.c index 426108f..0514d88 100644 --- a/src/register_user.c +++ b/src/register_user.c @@ -2,9 +2,12 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), - MG_ESC("must have , , , , ")); + mg_http_reply( + c, 400, + "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + "{%m:%m}", MG_ESC("error"), + MG_ESC("must have , , , , ")); return; } cJSON *jsonbody = cJSON_Parse(hm->body.buf); @@ -14,9 +17,12 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { cJSON *group = cJSON_GetObjectItem(jsonbody, "group"); cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); if (username == nullptr || password == nullptr || email == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), - MG_ESC("must have , , , , ")); + mg_http_reply( + c, 400, + "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + "{%m:%m}", MG_ESC("error"), + MG_ESC("must have , , , , ")); return; } if (group == nullptr) { @@ -28,10 +34,12 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { const bool is_superuser_value = is_superuser->valueint ? true : false; User *new_user_data = db_insert_user( username->valuestring, email->valuestring, password->valuestring, - group->valuestring, (const bool*)is_superuser_value); + group->valuestring, (const bool *) is_superuser_value); if (new_user_data == nullptr) { - mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), - MG_ESC("failed to create user")); + mg_http_reply( + c, 500, + "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); return; } char responsedata[2048] = {0}; @@ -41,6 +49,9 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { new_user_data->id, new_user_data->username, new_user_data->email, new_user_data->group, new_user_data->is_superuser); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", responsedata); + mg_http_reply( + c, 200, + "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + responsedata); free_user(new_user_data); } diff --git a/src/verify_token.c b/src/verify_token.c index 9e6e23f..d4402ad 100644 --- a/src/verify_token.c +++ b/src/verify_token.c @@ -5,14 +5,14 @@ void verify_token(struct mg_connection *c, struct mg_http_message *hm) { const request_with_token *data = check_body(hm); if (data->error != nullptr) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); return; } const bool valid = jwt_verifyJWT(data->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } - mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%m:true}", MG_ESC("valid")); + mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:true}", MG_ESC("valid")); } From 4c7a15672349264360ad49989e95bec679d5283e Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 29 Aug 2025 17:46:58 +0700 Subject: [PATCH 25/30] added config and tls --- Makefile | 2 +- config.json | 9 +++ src/ConfigManager.c | 172 ++++++++++++++++++++++++++++++++++++++++++++ src/ConfigManager.h | 39 ++++++++++ src/config.c | 7 ++ src/config.h | 7 ++ src/database.c | 3 +- src/main.c | 33 ++++++--- src/static.c | 7 ++ src/static.h | 5 ++ 10 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 config.json create mode 100644 src/ConfigManager.c create mode 100644 src/ConfigManager.h create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/static.c create mode 100644 src/static.h diff --git a/Makefile b/Makefile index 13af952..86b2eaa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CC = gcc -CFLAGS = -Wall -g -std=gnu2x +CFLAGS = -Wall -g -std=gnu2x -DMG_TLS=MG_TLS_BUILTIN LDFLAGS = LIBS = -lpthread -lcrypto -lsqlite3 INCLUDES = -I./src/lib -I./src diff --git a/config.json b/config.json new file mode 100644 index 0000000..0e30634 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "database": "users.db", + "address": "localhost:8222", + "default_users_file": "users.json", + "web": "./web", + "tls_enabled": true, + "tls_cert": "/home/tola/certs/localhost/localhost.crt", + "tls_key": "/home/tola/certs/localhost/localhost.key" +} \ No newline at end of file diff --git a/src/ConfigManager.c b/src/ConfigManager.c new file mode 100644 index 0000000..b968fea --- /dev/null +++ b/src/ConfigManager.c @@ -0,0 +1,172 @@ +#include "ConfigManager.h" +#include +#include +#include +#include +#include +#include "file.h" + +typedef struct ConfigManager { + cJSON *config; + pthread_mutex_t config_file_mutex; + char *config_file_name; +} ConfigManager; + +ConfigManager *cm_new_config() { + ConfigManager *newcm = malloc(sizeof(ConfigManager)); + newcm->config_file_name = nullptr; + newcm->config = nullptr; + return newcm; +} + +ConfigManager *cm_new_config_from_file(const char *filename) { + ConfigManager *newcm = malloc(sizeof(ConfigManager)); + newcm->config_file_name = strdup(filename); + pthread_mutex_lock(&(newcm->config_file_mutex)); + char *config_file_content = read_file(newcm->config_file_name); + newcm->config = cJSON_Parse(config_file_content); + pthread_mutex_unlock(&(newcm->config_file_mutex)); + return newcm; +} + +ConfigManager *cm_new_config_from_object(cJSON *config) { + ConfigManager *newcm = malloc(sizeof(ConfigManager)); + newcm->config = config; + newcm->config_file_name = nullptr; + return newcm; +} + +void cm_set_config_from_file(ConfigManager *cm, const char *filename) { + cm->config_file_name = strdup(filename); + pthread_mutex_lock(&(cm->config_file_mutex)); + char *config_file_content = read_file(cm->config_file_name); + cm->config = cJSON_Parse(config_file_content); + pthread_mutex_unlock(&(cm->config_file_mutex)); +} + +void cm_set_config_from_object(ConfigManager *cm, cJSON *config) { + cm->config = config; +} + +void cm_delete_config(ConfigManager *cm) { + if (!cm) { + return; + } + if (cm->config) { + cJSON_Delete(cm->config); + } + if (cm->config_file_name) { + free(cm->config_file_name); + } + free(cm); +} + +void cm_print_config(const ConfigManager *cm) { + printf("Config: %s\n", cJSON_Print(cm->config)); +} + +char *cm_get_config_filename(ConfigManager *cm) { + return strdup(cm->config_file_name); +} + +cJSON *cm_get_config(ConfigManager *cm) { + return cJSON_Duplicate(cm->config, (cJSON_bool) true); +} + +char *cm_get_parameter_as_string(ConfigManager *cm, const char *param_name) { + cJSON *param = cJSON_GetObjectItem(cm->config, param_name); + if (!param) return nullptr; + char *param_value = strdup(param->valuestring); + return param_value; +} + +int cm_get_parameter_as_int(ConfigManager *cm, const char *param_name) { + cJSON *param = cJSON_GetObjectItem(cm->config, param_name); + return param->valueint; +} +double cm_get_parameter_as_double(ConfigManager *cm, const char *param_name) { + cJSON *param = cJSON_GetObjectItem(cm->config, param_name); + return param->valuedouble; +} +float cm_get_parameter_as_float(ConfigManager *cm, const char *param_name) { + cJSON *param = cJSON_GetObjectItem(cm->config, param_name); + return param->valuedouble; +} +bool cm_get_parameter_as_bool(ConfigManager *cm, const char *param_name) { + cJSON *param = cJSON_GetObjectItem(cm->config, param_name); + if (!param) return false; + return param->valueint; +} + +void cm_set_string_parameter(ConfigManager *cm, const char *name, + const char *value) { + cJSON *param = cJSON_GetObjectItem(cm->config, name); + if (!param) { + cJSON_AddStringToObject(cm->config, name, value); + return; + } + cJSON_SetValuestring(param, value); +} +void cm_set_int_parameter(ConfigManager *cm, const char *name, + const int value) { + cJSON *param = cJSON_GetObjectItem(cm->config, name); + if (!param) { + cJSON_AddNumberToObject(cm->config, name, value); + return; + } + cJSON_SetIntValue(param, value); +} +void cm_set_double_parameter(ConfigManager *cm, const char *name, + const double value) { + cJSON *param = cJSON_GetObjectItem(cm->config, name); + if (!param) { + cJSON_AddNumberToObject(cm->config, name, value); + return; + } + cJSON_SetNumberValue(param, value); +} +void cm_set_float_parameter(ConfigManager *cm, const char *name, + const float value) { + cJSON *param = cJSON_GetObjectItem(cm->config, name); + if (!param) { + cJSON_AddNumberToObject(cm->config, name, value); + return; + } + cJSON_SetNumberValue(param, value); +} +void cm_set_bool_parameter(ConfigManager *cm, const char *name, + const bool value) { + cJSON *param = cJSON_GetObjectItem(cm->config, name); + if (!param) { + cJSON_AddBoolToObject(cm->config, name, value); + return; + } + cJSON_SetBoolValue(param, value); +} +void cm_set_null_parameter(ConfigManager *cm, const char *name) { + cJSON *param = cJSON_GetObjectItem(cm->config, name); + if (!param) { + cJSON_AddNullToObject(cm->config, name); + return; + } + cJSON_ReplaceItemInObject(cm->config, name, cJSON_CreateNull()); +} + +void cm_save_config(ConfigManager *cm) { + pthread_mutex_lock(&(cm->config_file_mutex)); + FILE *configfile = fopen(cm->config_file_name, "w+"); + char *configvalue = cJSON_Print(cm->config); + fprintf(configfile, configvalue); + fclose(configfile); + free(configvalue); + pthread_mutex_unlock(&(cm->config_file_mutex)); +} +void cm_save_config_to_file(ConfigManager *cm, const char *filename) { + pthread_mutex_lock(&(cm->config_file_mutex)); + FILE *configfile = fopen(filename, "w+"); + char *configvalue = cJSON_Print(cm->config); + fprintf(configfile, configvalue); + fclose(configfile); + free(configvalue); + pthread_mutex_unlock(&(cm->config_file_mutex)); +} \ No newline at end of file diff --git a/src/ConfigManager.h b/src/ConfigManager.h new file mode 100644 index 0000000..bec4dde --- /dev/null +++ b/src/ConfigManager.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +typedef struct ConfigManager ConfigManager; + +ConfigManager *cm_new_config(); +ConfigManager *cm_new_config_from_file(const char *filename); +ConfigManager *cm_new_config_from_object(cJSON *config); + +void cm_set_config_from_file(ConfigManager *cm, const char *filename); +void cm_set_config_from_object(ConfigManager *cm, cJSON *config); + +void cm_set_string_parameter(ConfigManager *cm, const char *name, + const char *value); +void cm_set_int_parameter(ConfigManager *cm, const char *name, const int value); +void cm_set_double_parameter(ConfigManager *cm, const char *name, + const double value); +void cm_set_float_parameter(ConfigManager *cm, const char *name, + const float value); +void cm_set_bool_parameter(ConfigManager *cm, const char *name, + const bool value); +void cm_set_null_parameter(ConfigManager *cm, const char *name); + +void cm_print_config(const ConfigManager *cm); + +void cm_delete_config(ConfigManager *cm); + +char *cm_get_config_filename(ConfigManager *cm); +cJSON *cm_get_config(ConfigManager *cm); + +char *cm_get_parameter_as_string(ConfigManager *cm, const char *param_name); +int cm_get_parameter_as_int(ConfigManager *cm, const char *param_name); +double cm_get_parameter_as_double(ConfigManager *cm, const char *param_name); +float cm_get_parameter_as_float(ConfigManager *cm, const char *param_name); +bool cm_get_parameter_as_bool(ConfigManager *cm, const char *param_name); + +void cm_save_config(ConfigManager *cm); +void cm_save_config_to_file(ConfigManager *cm, const char *filename); \ No newline at end of file diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..20e196a --- /dev/null +++ b/src/config.c @@ -0,0 +1,7 @@ +#include "config.h" + +ConfigManager *config = nullptr; + +void init_config(const char *filename) { + config = cm_new_config_from_file(filename); +} \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..75d4d78 --- /dev/null +++ b/src/config.h @@ -0,0 +1,7 @@ +#pragma once + +#include "ConfigManager.h" + +extern ConfigManager *config; + +void init_config(const char *filename); \ No newline at end of file diff --git a/src/database.c b/src/database.c index baaf430..17c7aa4 100644 --- a/src/database.c +++ b/src/database.c @@ -3,12 +3,13 @@ #include #include "sql.h" #include +#include "config.h" sqlite3* db = nullptr; pthread_mutex_t db_mutex = PTHREAD_MUTEX_INITIALIZER; void init_db() { - if (sqlite3_open("users.db", &db) != SQLITE_OK) { + if (sqlite3_open(cm_get_parameter_as_string(config, "database"), &db) != SQLITE_OK) { fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); exit(1); diff --git a/src/main.c b/src/main.c index 8a5219e..11498d0 100644 --- a/src/main.c +++ b/src/main.c @@ -1,15 +1,13 @@ #include #include #include +#include "config.h" #include "cors.h" #include "database.h" #include "route.h" +#include "static.h" #include "user.h" - -static void root_handler(struct mg_connection *c, struct mg_http_message *hm) { - struct mg_http_serve_opts opts = {.root_dir = "./web/"}; - mg_http_serve_dir(c, hm, &opts); -} +#include "file.h" static const struct route routes[] = { {"GET", "/#", root_handler}, @@ -26,6 +24,17 @@ static const struct route routes[] = { {nullptr, nullptr, nullptr}}; void ev_handler(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_ERROR) { + // Обработка ошибок соединения + MG_LOG(MG_LL_ERROR, ("Ошибка соединения: %s", (char *) ev_data)); + } + if (ev == MG_EV_ACCEPT && c->is_tls) { + struct mg_tls_opts opts; + memset(&opts, 0, sizeof(opts)); + opts.cert = mg_str(read_file("/home/tola/certs/localhost/cert.pem")); + opts.key = mg_str(read_file("/home/tola/certs/localhost/key.pem")); + mg_tls_init(c, &opts); + } if (ev == MG_EV_HTTP_MSG) { const auto hm = (struct mg_http_message *) ev_data; bool route_found = false; @@ -48,16 +57,24 @@ void ev_handler(struct mg_connection *c, int ev, void *ev_data) { } int main(void) { + init_config("config.json"); init_db(); migrate_db(); - db_get_users_from_file("users.json"); + const char *default_users_filename = + cm_get_parameter_as_string(config, "default_users_file"); + if (default_users_filename) db_get_users_from_file("users.json"); struct mg_mgr mgr; mg_log_set(MG_LL_DEBUG); mg_mgr_init(&mgr); - mg_http_listen(&mgr, "http://0.0.0.0:8222", ev_handler, nullptr); - while (true) { + const char *address = cm_get_parameter_as_string(config, "address"); + // address ? mg_http_listen(&mgr, cm_get_parameter_as_string(config, "address"), + // ev_handler, nullptr) + // : mg_http_listen(&mgr, "http://0.0.0.0:8222", ev_handler, nullptr); + mg_http_listen(&mgr, "https://0.0.0.0:8222", ev_handler, nullptr); + for (;;) { mg_mgr_poll(&mgr, 1000); } + mg_mgr_free(&mgr); return 0; } diff --git a/src/static.c b/src/static.c new file mode 100644 index 0000000..af5bb1c --- /dev/null +++ b/src/static.c @@ -0,0 +1,7 @@ +#include "static.h" +#include "config.h" + +void root_handler(struct mg_connection *c, struct mg_http_message *hm) { + struct mg_http_serve_opts opts = {.root_dir = cm_get_parameter_as_string(config, "web")}; + mg_http_serve_dir(c, hm, &opts); +} \ No newline at end of file diff --git a/src/static.h b/src/static.h new file mode 100644 index 0000000..47fea0d --- /dev/null +++ b/src/static.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +void root_handler(struct mg_connection *c, struct mg_http_message *hm); \ No newline at end of file From e0bd40e47fa6c2a57ac5955e239e537c1517b792 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 29 Aug 2025 18:28:10 +0700 Subject: [PATCH 26/30] added HTTP_HEADERS constant and tls support --- Makefile | 4 ++-- config.json | 1 + src/add_user.c | 10 +++++----- src/change_password.c | 20 ++++++++++---------- src/change_user.c | 16 ++++++++-------- src/delete_user.c | 12 ++++++------ src/get_all_users.c | 6 +++--- src/get_token.c | 12 ++++++------ src/get_user.c | 10 +++++----- src/main.c | 35 +++++++++++++++++++++++------------ src/register_user.c | 8 ++++---- src/user.h | 2 ++ src/verify_token.c | 6 +++--- 13 files changed, 78 insertions(+), 64 deletions(-) diff --git a/Makefile b/Makefile index 86b2eaa..c86cfe9 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ CC = gcc -CFLAGS = -Wall -g -std=gnu2x -DMG_TLS=MG_TLS_BUILTIN +CFLAGS = -Wall -g -std=gnu2x -DMG_TLS=MG_TLS_OPENSSL LDFLAGS = -LIBS = -lpthread -lcrypto -lsqlite3 +LIBS = -lpthread -lcrypto -lssl -lsqlite3 INCLUDES = -I./src/lib -I./src SRCS = $(wildcard src/*.c src/lib/*.c) OBJS = $(SRCS:.c=.o) diff --git a/config.json b/config.json index 0e30634..cb879a2 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,7 @@ { "database": "users.db", "address": "localhost:8222", + "tls_address": "localhost:8223", "default_users_file": "users.json", "web": "./web", "tls_enabled": true, diff --git a/src/add_user.c b/src/add_user.c index 618d27f..80b030b 100644 --- a/src/add_user.c +++ b/src/add_user.c @@ -6,7 +6,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -18,7 +18,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { cJSON *is_superuser = cJSON_GetObjectItem(jsonbody, "is_superuser"); if (username == nullptr || password == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); return; @@ -27,7 +27,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { bool jwt_is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!jwt_is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("not authorized")); cJSON_Delete(payload); return; @@ -38,7 +38,7 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!new_db_user) { - mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); cJSON_Delete(payload); return; @@ -51,6 +51,6 @@ void add_user(struct mg_connection *c, struct mg_http_message *hm) { is_superuser->valueint ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); cJSON_Delete(payload); } diff --git a/src/change_password.c b/src/change_password.c index ddcee84..773fc4b 100644 --- a/src/change_password.c +++ b/src/change_password.c @@ -6,7 +6,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -16,7 +16,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { cJSON *userid = cJSON_GetObjectItem(jsonbody, "userid"); if ((old_password == nullptr || new_password == nullptr) && userid == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , ")); return; @@ -27,7 +27,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char *username = cJSON_GetObjectItem(payload, "username")->valuestring; User *old_user_data = db_get_user_by_username(username); if (!old_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't get old_user_data")); cJSON_Delete(payload); @@ -35,7 +35,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char *password = old_user_data->password; if (strcmp(password, old_password->valuestring)) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid old password")); return; } @@ -44,7 +44,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { old_user_data->id, old_user_data->username, old_user_data->email, new_password->valuestring, old_user_data->group, &old_is_superuser); if (!new_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't update database")); free_user(old_user_data); cJSON_Delete(payload); @@ -54,21 +54,21 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char responsedata[64] = {0}; snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_user(old_user_data); cJSON_Delete(payload); return; } bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); cJSON_Delete(payload); return; } User *old_user_data = db_get_user_by_id(userid->valueint); if (!old_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't get old_user_data")); cJSON_Delete(payload); @@ -79,7 +79,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { old_user_data->id, old_user_data->username, old_user_data->email, new_password->valuestring, old_user_data->group, &old_is_superuser); if (!new_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't create new_user_data from root")); free_user(old_user_data); cJSON_Delete(payload); @@ -88,7 +88,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { char responsedata[64] = {0}; snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_user(old_user_data); free_user(new_user_data); cJSON_Delete(payload); diff --git a/src/change_user.c b/src/change_user.c index f34aeb0..7705407 100644 --- a/src/change_user.c +++ b/src/change_user.c @@ -6,7 +6,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *requestdata = check_body(hm); const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -23,19 +23,19 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { old_user_data = db_get_user_by_id(userid->valueint); } if (!old_user_data) { - mg_http_reply(c, 404, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 404, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("user not found")); return; } char *password = old_user_data->password; if (!password) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("password is required")); return; } if (username == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , ")); @@ -52,7 +52,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!updated_user_data) { - mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user change user request")); cJSON_Delete(payload); free_user(old_user_data); @@ -66,7 +66,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { username->valuestring, email->valuestring, group->valuestring, is_superuser->valueint ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_user(updated_user_data); free_user(old_user_data); cJSON_Delete(payload); @@ -75,7 +75,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { bool req_is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!req_is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); cJSON_Delete(payload); free_user(old_user_data); @@ -99,7 +99,7 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { username->valuestring, email->valuestring, group->valuestring, is_superuser->valueint ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_user(updated_user_data); free_user(old_user_data); cJSON_Delete(payload); diff --git a/src/delete_user.c b/src/delete_user.c index 6a39bb7..1856d15 100644 --- a/src/delete_user.c +++ b/src/delete_user.c @@ -4,19 +4,19 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have token")); return; } request_with_token *requestdata = check_body(hm); if (requestdata->error != NULL) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC(requestdata->error)); return; } const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); free_request_with_token(requestdata); return; @@ -31,14 +31,14 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { snprintf(responsedata, sizeof(responsedata), "{\"success\": %s}", user_deleted ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_request_with_token(requestdata); cJSON_Delete(payload); return; } bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); return; } @@ -47,7 +47,7 @@ void delete_user(struct mg_connection *c, struct mg_http_message *hm) { snprintf(responsedata, sizeof(responsedata), "{\"success\": %s}", user_deleted ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_request_with_token(requestdata); cJSON_Delete(payload); } diff --git a/src/get_all_users.c b/src/get_all_users.c index dd530ca..a21cc74 100644 --- a/src/get_all_users.c +++ b/src/get_all_users.c @@ -5,18 +5,18 @@ void get_all_users(struct mg_connection *c, struct mg_http_message *hm) { request_with_token *data = check_body(hm); if (data->error != NULL) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); return; } if (!jwt_verifyJWT(data->token, JWT_DEFAULT_SECRET)) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } cJSON *payload = jwt_get_payload(data->token); bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("user is not superuser")); return; } diff --git a/src/get_token.c b/src/get_token.c index 07467ba..5e3047f 100644 --- a/src/get_token.c +++ b/src/get_token.c @@ -3,7 +3,7 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have username and password")); return; } @@ -13,21 +13,21 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { if (username == NULL || password == NULL) { const char *error_ptr = cJSON_GetErrorPtr(); if (error_ptr != NULL) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(error_ptr)); + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC(error_ptr)); return; } - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have username and password")); return; } User *db_user = db_get_user_by_username(username->valuestring); if (!db_user) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("couldn't find user")); return; } if (strcmp(db_user->password, password->valuestring)) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("wrong password")); return; } @@ -41,7 +41,7 @@ void get_token(struct mg_connection *c, struct mg_http_message *hm) { db_user->id, db_user->username, db_user->email, db_user->group, db_user->is_superuser ? "true" : "false"); char *token = jwt_createJWT(header, payload, JWT_DEFAULT_SECRET); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("token"), MG_ESC(token)); + mg_http_reply(c, 200, HTTP_HEADERS, "{%m:%m}", MG_ESC("token"), MG_ESC(token)); free_user(db_user); free(token); } diff --git a/src/get_user.c b/src/get_user.c index 79b79d0..3540a7e 100644 --- a/src/get_user.c +++ b/src/get_user.c @@ -4,18 +4,18 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { - mg_http_reply(c, 400, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have token")); return; } request_with_token *requestdata = check_body(hm); if (requestdata->error != NULL) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(requestdata->error)); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC(requestdata->error)); return; } if (!jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET)) { free_request_with_token(requestdata); - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } @@ -27,7 +27,7 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { free_request_with_token(requestdata); cJSON_Delete(payload); free(username); - mg_http_reply(c, 404, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 404, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("user not found")); return; } @@ -38,7 +38,7 @@ void get_user(struct mg_connection *c, struct mg_http_message *hm) { user->id, user->username, user->email, user->group, user->is_superuser ? "true" : "false"); - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", responsedata); + mg_http_reply(c, 200, HTTP_HEADERS, responsedata); free_request_with_token(requestdata); free(username); free_user(user); diff --git a/src/main.c b/src/main.c index 11498d0..4b063dc 100644 --- a/src/main.c +++ b/src/main.c @@ -4,10 +4,10 @@ #include "config.h" #include "cors.h" #include "database.h" +#include "file.h" #include "route.h" #include "static.h" #include "user.h" -#include "file.h" static const struct route routes[] = { {"GET", "/#", root_handler}, @@ -25,15 +25,19 @@ static const struct route routes[] = { void ev_handler(struct mg_connection *c, int ev, void *ev_data) { if (ev == MG_EV_ERROR) { - // Обработка ошибок соединения MG_LOG(MG_LL_ERROR, ("Ошибка соединения: %s", (char *) ev_data)); } - if (ev == MG_EV_ACCEPT && c->is_tls) { - struct mg_tls_opts opts; - memset(&opts, 0, sizeof(opts)); - opts.cert = mg_str(read_file("/home/tola/certs/localhost/cert.pem")); - opts.key = mg_str(read_file("/home/tola/certs/localhost/key.pem")); - mg_tls_init(c, &opts); + const bool tls_enabled = cm_get_parameter_as_bool(config, "tls_enabled"); + if (tls_enabled) { + if (ev == MG_EV_ACCEPT && c->is_tls) { + struct mg_tls_opts opts; + memset(&opts, 0, sizeof(opts)); + opts.cert = + mg_str(read_file(cm_get_parameter_as_string(config, "tls_cert"))); + opts.key = + mg_str(read_file(cm_get_parameter_as_string(config, "tls_key"))); + mg_tls_init(c, &opts); + } } if (ev == MG_EV_HTTP_MSG) { const auto hm = (struct mg_http_message *) ev_data; @@ -68,10 +72,17 @@ int main(void) { mg_log_set(MG_LL_DEBUG); mg_mgr_init(&mgr); const char *address = cm_get_parameter_as_string(config, "address"); - // address ? mg_http_listen(&mgr, cm_get_parameter_as_string(config, "address"), - // ev_handler, nullptr) - // : mg_http_listen(&mgr, "http://0.0.0.0:8222", ev_handler, nullptr); - mg_http_listen(&mgr, "https://0.0.0.0:8222", ev_handler, nullptr); + char http_address[64]; + snprintf(http_address, sizeof(http_address), "http://%s", address); + address ? mg_http_listen(&mgr, http_address, ev_handler, nullptr) + : mg_http_listen(&mgr, "http://0.0.0.0:8222", ev_handler, nullptr); + if (cm_get_parameter_as_bool(config, "tls_enabled")) { + const char *tls_address = cm_get_parameter_as_string(config, "tls_address"); + char https_address[64]; + snprintf(https_address, sizeof(https_address), "https://%s", tls_address); + tls_address ? mg_http_listen(&mgr, https_address, ev_handler, nullptr) + : mg_http_listen(&mgr, "https://0.0.0.0:8223", ev_handler, nullptr); + } for (;;) { mg_mgr_poll(&mgr, 1000); } diff --git a/src/register_user.c b/src/register_user.c index 0514d88..a183e0e 100644 --- a/src/register_user.c +++ b/src/register_user.c @@ -4,7 +4,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { if (hm->body.len == 0) { mg_http_reply( c, 400, - "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); @@ -19,7 +19,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { if (username == nullptr || password == nullptr || email == nullptr) { mg_http_reply( c, 400, - "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , , ")); @@ -38,7 +38,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { if (new_user_data == nullptr) { mg_http_reply( c, 500, - "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to create user")); return; } @@ -51,7 +51,7 @@ void register_user(struct mg_connection *c, struct mg_http_message *hm) { mg_http_reply( c, 200, - "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + HTTP_HEADERS, responsedata); free_user(new_user_data); } diff --git a/src/user.h b/src/user.h index 9695583..6785fe3 100644 --- a/src/user.h +++ b/src/user.h @@ -4,6 +4,8 @@ #include #include +#define HTTP_HEADERS "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n" + typedef struct User { int id; char *username; diff --git a/src/verify_token.c b/src/verify_token.c index d4402ad..d34aa63 100644 --- a/src/verify_token.c +++ b/src/verify_token.c @@ -5,14 +5,14 @@ void verify_token(struct mg_connection *c, struct mg_http_message *hm) { const request_with_token *data = check_body(hm); if (data->error != nullptr) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC(data->error)); return; } const bool valid = jwt_verifyJWT(data->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:%m}", MG_ESC("error"), + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("invalid token")); return; } - mg_http_reply(c, 200, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "{%m:true}", MG_ESC("valid")); + mg_http_reply(c, 200, HTTP_HEADERS, "{%m:true}", MG_ESC("valid")); } From 27ba0ab45e4706c9a87bfd769c413c62d6f38016 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 29 Aug 2025 18:32:24 +0700 Subject: [PATCH 27/30] updated meson.build --- meson.build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meson.build b/meson.build index 9c71551..ce23cdb 100644 --- a/meson.build +++ b/meson.build @@ -27,6 +27,10 @@ executable( 'src/db_update_user.c', 'src/db_delete_user.c', 'src/db_get_user_by_id.c', + 'src/ConfigManager.c', + 'src/config.c', + 'src/static.c', + 'src/cors.c', include_directories: include_dirs, dependencies: deps, ) From 56d670c9b6e8ee48dfec4aa08b983b7c28f658bb Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 30 Aug 2025 12:08:57 +0700 Subject: [PATCH 28/30] added field change and get and updated tests --- meson.build | 2 + src/change_password.c | 79 ++++++++++++++++------------------- src/change_user.c | 36 ++++++++++------ src/db_get_user_by_id.c | 14 +++---- src/db_get_user_by_username.c | 6 +-- src/db_get_user_field_by_id.c | 45 ++++++++++++++++++++ src/db_update_user_field.c | 35 ++++++++++++++++ src/sql.h | 3 +- src/user.h | 6 ++- tests.sh | 68 ++++++++++++++++++++++++++++++ 10 files changed, 225 insertions(+), 69 deletions(-) create mode 100644 src/db_get_user_field_by_id.c create mode 100644 src/db_update_user_field.c diff --git a/meson.build b/meson.build index ce23cdb..4c3f6eb 100644 --- a/meson.build +++ b/meson.build @@ -31,6 +31,8 @@ executable( 'src/config.c', 'src/static.c', 'src/cors.c', + 'src/db_get_user_field_by_id.c', + 'src/db_update_user_field.c', include_directories: include_dirs, dependencies: deps, ) diff --git a/src/change_password.c b/src/change_password.c index 773fc4b..a73d117 100644 --- a/src/change_password.c +++ b/src/change_password.c @@ -1,13 +1,24 @@ +#include "cJSON.h" #include "jwt.h" #include "user.h" #include "utils.h" void change_password(struct mg_connection *c, struct mg_http_message *hm) { + if (hm->body.len == 0) { + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("must have token, old_password, new_password")); + return; + } request_with_token *requestdata = check_body(hm); + if (requestdata->error != NULL) { + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(requestdata->error)); + return; + } const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("invalid token")); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid token")); return; } cJSON *jsonbody = cJSON_Parse(hm->body.buf); @@ -16,37 +27,31 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { cJSON *userid = cJSON_GetObjectItem(jsonbody, "userid"); if ((old_password == nullptr || new_password == nullptr) && userid == nullptr) { - mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , ")); return; } cJSON *payload = jwt_get_payload(requestdata->token); if (!userid) { - char *username = cJSON_GetObjectItem(payload, "username")->valuestring; - User *old_user_data = db_get_user_by_username(username); - if (!old_user_data) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), + cJSON *id = cJSON_GetObjectItem(payload, "id"); + char *password = db_get_user_field_by_id(id->valueint, "password"); + if (!password) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("failed to update user can't get old_user_data")); cJSON_Delete(payload); - } - char *password = old_user_data->password; - - if (strcmp(password, old_password->valuestring)) { - mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("invalid old password")); return; } - const bool old_is_superuser = old_user_data->is_superuser; - User *new_user_data = db_update_user( - old_user_data->id, old_user_data->username, old_user_data->email, - new_password->valuestring, old_user_data->group, &old_is_superuser); - if (!new_user_data) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("failed to update user can't update database")); - free_user(old_user_data); + + if (strcmp(password, old_password->valuestring)) { + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid old password")); + return; + } + if (!db_update_user_field(id->valueint, "password", + new_password->valuestring)) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("failed to update user can't update database")); cJSON_Delete(payload); return; } @@ -55,33 +60,21 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); mg_http_reply(c, 200, HTTP_HEADERS, responsedata); - free_user(old_user_data); cJSON_Delete(payload); return; } bool is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!is_superuser) { - mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("user is not superuser")); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("user is not superuser")); cJSON_Delete(payload); return; } - User *old_user_data = db_get_user_by_id(userid->valueint); - if (!old_user_data) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), - MG_ESC("failed to update user can't get old_user_data")); - cJSON_Delete(payload); - return; - } - const bool old_is_superuser = old_user_data->is_superuser; - User *new_user_data = db_update_user( - old_user_data->id, old_user_data->username, old_user_data->email, - new_password->valuestring, old_user_data->group, &old_is_superuser); - if (!new_user_data) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("failed to update user can't create new_user_data from root")); - free_user(old_user_data); + if (!db_update_user_field(userid->valueint, "password", + new_password->valuestring)) { + mg_http_reply( + c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("failed to update user can't create new_user_data from root")); cJSON_Delete(payload); return; } @@ -89,7 +82,5 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm) { snprintf(responsedata, sizeof(responsedata), "{\"success\": true}"); mg_http_reply(c, 200, HTTP_HEADERS, responsedata); - free_user(old_user_data); - free_user(new_user_data); cJSON_Delete(payload); } diff --git a/src/change_user.c b/src/change_user.c index 7705407..7d260be 100644 --- a/src/change_user.c +++ b/src/change_user.c @@ -3,11 +3,21 @@ #include "utils.h" void change_user(struct mg_connection *c, struct mg_http_message *hm) { + if (hm->body.len == 0) { + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("must have token, new_user_data")); + return; + } request_with_token *requestdata = check_body(hm); + if (requestdata->error != NULL) { + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(requestdata->error)); + return; + } const bool valid = jwt_verifyJWT(requestdata->token, JWT_DEFAULT_SECRET); if (!valid) { - mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("invalid token")); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid token")); return; } cJSON *jsonbody = cJSON_Parse(hm->body.buf); @@ -23,20 +33,19 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { old_user_data = db_get_user_by_id(userid->valueint); } if (!old_user_data) { - mg_http_reply(c, 404, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("user not found")); + mg_http_reply(c, 404, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("user not found")); return; } char *password = old_user_data->password; if (!password) { - mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("password is required")); + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("password is required")); return; } if (username == nullptr || email == nullptr || is_superuser == nullptr || group == nullptr) { - mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), + mg_http_reply(c, 400, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), MG_ESC("must have , , , ")); free_user(old_user_data); @@ -52,8 +61,8 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!updated_user_data) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("failed to update user change user request")); + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("failed to update user change user request")); cJSON_Delete(payload); free_user(old_user_data); return; @@ -75,8 +84,8 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { bool req_is_superuser = cJSON_GetObjectItem(payload, "is_superuser")->valueint; if (!req_is_superuser) { - mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", - MG_ESC("error"), MG_ESC("user is not superuser")); + mg_http_reply(c, 403, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("user is not superuser")); cJSON_Delete(payload); free_user(old_user_data); return; @@ -86,7 +95,8 @@ void change_user(struct mg_connection *c, struct mg_http_message *hm) { group->valuestring, &user_is_superuser); if (!updated_user_data) { mg_http_reply(c, 500, "Content-Type: application/json\r\n", "{%m:%m}", - MG_ESC("error"), MG_ESC("failed to update user can't change userdata")); + MG_ESC("error"), + MG_ESC("failed to update user can't change userdata")); cJSON_Delete(payload); free_user(old_user_data); return; diff --git a/src/db_get_user_by_id.c b/src/db_get_user_by_id.c index dc8d86e..acd759e 100644 --- a/src/db_get_user_by_id.c +++ b/src/db_get_user_by_id.c @@ -1,9 +1,9 @@ -#include "user.h" #include "database.h" #include "sql.h" +#include "user.h" User *db_get_user_by_id(const int id) { - User *user = nullptr; + User *user = nullptr; pthread_mutex_lock(&db_mutex); if (!db) { fprintf(stderr, "Database not initialized\n"); @@ -11,12 +11,11 @@ User *db_get_user_by_id(const int id) { return nullptr; } sqlite3_stmt *stmt; - int rc = - sqlite3_prepare_v2(db, SELECT_USER_BY_ID_SQL, -1, &stmt, nullptr); + int rc = sqlite3_prepare_v2(db, SELECT_USER_BY_ID_SQL, -1, &stmt, nullptr); if (rc != SQLITE_OK) { fprintf(stderr, "Cannot prepare statement: %s\n", sqlite3_errmsg(db)); - sqlite3_close(db); - exit(1); + pthread_mutex_unlock(&db_mutex); + return nullptr; } sqlite3_bind_int(stmt, 1, id); @@ -36,9 +35,10 @@ User *db_get_user_by_id(const int id) { fprintf(stderr, "Error reading data: %s\n", sqlite3_errmsg(db)); free_user(user); sqlite3_finalize(stmt); + pthread_mutex_unlock(&db_mutex); return nullptr; } pthread_mutex_unlock(&db_mutex); return user; -} \ No newline at end of file +} diff --git a/src/db_get_user_by_username.c b/src/db_get_user_by_username.c index 5be5f4c..90a5544 100644 --- a/src/db_get_user_by_username.c +++ b/src/db_get_user_by_username.c @@ -15,8 +15,8 @@ User *db_get_user_by_username(const char *username) { sqlite3_prepare_v2(db, SELECT_USER_BY_USERNAME_SQL, -1, &stmt, nullptr); if (rc != SQLITE_OK) { fprintf(stderr, "Cannot prepare statement: %s\n", sqlite3_errmsg(db)); - sqlite3_close(db); - exit(1); + pthread_mutex_unlock(&db_mutex); + return nullptr; } sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); @@ -41,4 +41,4 @@ User *db_get_user_by_username(const char *username) { pthread_mutex_unlock(&db_mutex); return user; -} \ No newline at end of file +} diff --git a/src/db_get_user_field_by_id.c b/src/db_get_user_field_by_id.c new file mode 100644 index 0000000..7c70125 --- /dev/null +++ b/src/db_get_user_field_by_id.c @@ -0,0 +1,45 @@ +#include +#include "database.h" +#include "sql.h" +#include "user.h" + +char *db_get_user_field_by_id(const int id, const char *field) { + char *password = nullptr; + pthread_mutex_lock(&db_mutex); + if (!db) { + fprintf(stderr, "Database not initialized\n"); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + char sql[256]; + snprintf(sql, sizeof(sql), SELECT_USER_FIELD_SQL, field); + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + fprintf(stderr, "Cannot prepare statement: %s\n", sqlite3_errmsg(db)); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + sqlite3_bind_int(stmt, 1, id); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + const unsigned char *db_password = sqlite3_column_text(stmt, 0); + password = malloc(strlen((const char *) db_password) + 1); + if (password) { + strcpy(password, (const char *) db_password); + } + sqlite3_finalize(stmt); + } else if (rc == SQLITE_DONE) { + fprintf(stderr, "Can't find user"); + sqlite3_finalize(stmt); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } else { + fprintf(stderr, "Error executing query: %s\n", sqlite3_errmsg(db)); + sqlite3_finalize(stmt); + pthread_mutex_unlock(&db_mutex); + return nullptr; + } + pthread_mutex_unlock(&db_mutex); + return password; +} diff --git a/src/db_update_user_field.c b/src/db_update_user_field.c new file mode 100644 index 0000000..15379b5 --- /dev/null +++ b/src/db_update_user_field.c @@ -0,0 +1,35 @@ +#include "database.h" +#include "sql.h" +#include "user.h" + +bool db_update_user_field(const int id, const char *field, const char *value) { + pthread_mutex_lock(&db_mutex); + if (!db || !field || !value) { + fprintf(stderr, "invalid parameters\n"); + pthread_mutex_unlock(&db_mutex); + return false; + } + char sql[256]; + snprintf(sql, sizeof(sql), UPDATE_USER_FIELD_SQL, field); + + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); + if (rc != SQLITE_OK) { + fprintf(stderr, "failed to prepare statement: %s\n", sqlite3_errmsg(db)); + pthread_mutex_unlock(&db_mutex); + return false; + } + sqlite3_bind_text(stmt, 1, value, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, id); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + fprintf(stderr, "execution failed: %s\n", sqlite3_errmsg(db)); + pthread_mutex_unlock(&db_mutex); + return false; + } + pthread_mutex_unlock(&db_mutex); + return sqlite3_changes(db) > 0; +} diff --git a/src/sql.h b/src/sql.h index f183b81..860d87a 100644 --- a/src/sql.h +++ b/src/sql.h @@ -29,4 +29,5 @@ "update users set username = ?, email = ?, password = ?, user_group = ?, " \ "is_superuser = ? where id = ?" #define DELETE_USER_SQL "delete from users where id = ?" -#define UPDATE_USER_PASSWORD_SQL "update users set password = ? where id = ?" +#define UPDATE_USER_FIELD_SQL "update users set %s = ? where id = ?" +#define SELECT_USER_FIELD_SQL "select %s from users where id = ?" diff --git a/src/user.h b/src/user.h index 6785fe3..85fb364 100644 --- a/src/user.h +++ b/src/user.h @@ -4,7 +4,8 @@ #include #include -#define HTTP_HEADERS "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n" +#define HTTP_HEADERS \ + "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n" typedef struct User { int id; @@ -54,6 +55,7 @@ void change_password(struct mg_connection *c, struct mg_http_message *hm); void register_user(struct mg_connection *c, struct mg_http_message *hm); void get_token(struct mg_connection *c, struct mg_http_message *hm); void verify_token(struct mg_connection *c, struct mg_http_message *hm); + void db_get_users_from_file(const char *filename); char *db_get_all_users(); User *db_get_user_by_username(const char *username); @@ -65,3 +67,5 @@ User *db_update_user(const int userid, const char *username, const char *email, const bool *is_superuser); bool db_delete_user(const int id); User *db_get_user_by_id(const int id); +char *db_get_user_field_by_id(const int id, const char *field); +bool db_update_user_field(const int id, const char *field, const char *value); diff --git a/tests.sh b/tests.sh index e7a2317..bc8bcd7 100755 --- a/tests.sh +++ b/tests.sh @@ -436,6 +436,40 @@ else errors=$(($errors+1)) fi +# test POST /api/users/password without post data +printf "==> ${BLUE}Testing${CYAN} POST${BWHITE} /api/users/password without post data${RESET} " +curl \ + --silent \ + -X POST \ + -d "" \ + localhost:8222/api/users/password &> api_users_password_no_data_test.log +sleep 0.1 +res=$(cat api_users_password_no_data_test.log) +if [[ "$res" == *'{"error":"must have token, old_password, new_password"}'* ]]; then + printf "[$GREEN OK $RESET]\n" + success=$(($success+1)) +else + printf "[$RED ERR $RESET]\n==> ${BLUE}Response: $RESET$(echo $res)\n" + errors=$(($errors+1)) +fi + +# test POST /api/users/password with wrong json +printf "==> ${BLUE}Testing${CYAN} POST${BWHITE} /api/users/password with wrong json${RESET} " +curl \ + --silent \ + -X POST \ + -d "ijansd fu9q303ghq9" \ + localhost:8222/api/users/password &> api_users_password_wrong_json_test.log +sleep 0.1 +res=$(cat api_users_password_wrong_json_test.log) +if [[ "$res" == *'error'* ]]; then + printf "[$GREEN OK $RESET]\n" + success=$(($success+1)) +else + printf "[$RED ERR $RESET]\n==> ${BLUE}Response: $RESET$(echo $res)\n" + errors=$(($errors+1)) +fi + # test PUT /api/users printf "==> ${BLUE}Testing${YELLOW} PUT${BWHITE} /api/users${RESET} " reqtoken=$(cat api_token_test.log | sed -e "s/{//g" | sed -e "s/}//g") @@ -459,6 +493,40 @@ else errors=$(($errors+1)) fi +# test PUT /api/users without post data +printf "==> ${BLUE}Testing${YELLOW} PUT${BWHITE} /api/users without post data${RESET} " +curl \ + --silent \ + -X PUT \ + -d "" \ + localhost:8222/api/users &> put_api_users_no_data_test.log +sleep 0.1 +res=$(cat put_api_users_no_data_test.log) +if [[ "$res" == *'{"error":"must have token, new_user_data"}'* ]]; then + printf "[$GREEN OK $RESET]\n" + success=$(($success+1)) +else + printf "[$RED ERR $RESET]\n==> ${BLUE}Response: $RESET$(echo $res)\n" + errors=$(($errors+1)) +fi + +# test PUT /api/users with wrong json +printf "==> ${BLUE}Testing${YELLOW} PUT${BWHITE} /api/users with wrong json${RESET} " +curl \ + --silent \ + -X PUT \ + -d "hiusdf 7qyt0 y qy40" \ + localhost:8222/api/users &> put_api_users_wrong_json_test.log +sleep 0.1 +res=$(cat put_api_users_wrong_json_test.log) +if [[ "$res" == *'error'* ]]; then + printf "[$GREEN OK $RESET]\n" + success=$(($success+1)) +else + printf "[$RED ERR $RESET]\n==> ${BLUE}Response: $RESET$(echo $res)\n" + errors=$(($errors+1)) +fi + # test POST /api/register printf "==> ${BLUE}Testing${CYAN} POST${BWHITE} /api/register${RESET} " curl \ From b0758c5499e65878d0fdeeb7fb7f09b1f2e385c6 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 30 Aug 2025 12:10:40 +0700 Subject: [PATCH 29/30] hot fix warning in fprintf --- src/ConfigManager.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ConfigManager.c b/src/ConfigManager.c index b968fea..bc13f7f 100644 --- a/src/ConfigManager.c +++ b/src/ConfigManager.c @@ -156,7 +156,7 @@ void cm_save_config(ConfigManager *cm) { pthread_mutex_lock(&(cm->config_file_mutex)); FILE *configfile = fopen(cm->config_file_name, "w+"); char *configvalue = cJSON_Print(cm->config); - fprintf(configfile, configvalue); + fprintf(configfile, "%s", configvalue); fclose(configfile); free(configvalue); pthread_mutex_unlock(&(cm->config_file_mutex)); @@ -165,8 +165,8 @@ void cm_save_config_to_file(ConfigManager *cm, const char *filename) { pthread_mutex_lock(&(cm->config_file_mutex)); FILE *configfile = fopen(filename, "w+"); char *configvalue = cJSON_Print(cm->config); - fprintf(configfile, configvalue); + fprintf(configfile, "%s", configvalue); fclose(configfile); free(configvalue); pthread_mutex_unlock(&(cm->config_file_mutex)); -} \ No newline at end of file +} From 4c4f92e43b5008f4bb93c46ca99da03c8d9f0f8d Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 30 Aug 2025 12:24:57 +0700 Subject: [PATCH 30/30] updated web --- web/css/loginform.css | 3 +- web/css/message.css | 32 ++-- web/index.html | 360 +++++++++++++++++++++++++----------------- web/js/login.js | 158 +++++++++--------- web/js/message.js | 46 ++++-- 5 files changed, 357 insertions(+), 242 deletions(-) diff --git a/web/css/loginform.css b/web/css/loginform.css index fdc9bd6..5f60f4b 100644 --- a/web/css/loginform.css +++ b/web/css/loginform.css @@ -28,7 +28,6 @@ background-color: gray; backdrop-filter: blur(5px); border-radius: 10px; - /* box-shadow: -5px -5px 30px 30px gray; */ } .loginformheader { @@ -51,7 +50,7 @@ display: flex; } -.loginforminputbodyflex>button { +.loginforminputbodyflex > button { margin: 10px; } diff --git a/web/css/message.css b/web/css/message.css index d69d710..a97851d 100644 --- a/web/css/message.css +++ b/web/css/message.css @@ -1,19 +1,33 @@ -.message { - width: 100%; +* { + transition: 0.3s; +} + +.notificationsection { + width: max-content; + height: 100vh; + position: fixed; + top: 10px; + right: 0; + pointer-events: none; + z-index: 16; +} + +.notification { + width: max-content; height: 0; overflow: hidden; pointer-events: none; z-index: 10; border-radius: 10px; - position: fixed; top: 0; - left: 0; - transition: 0.3s; + right: 0; cursor: pointer; opacity: 0.8; + margin: 10px; + padding: 0 10px; } -.messagetextbox { +.notificationtextbox { width: 100%; height: 100%; display: grid; @@ -22,19 +36,19 @@ pointer-events: none; } -.messagetext { +.notificationtext { width: 100%; text-align: center; pointer-events: none; } -.message.active.error { +.notification.active.error { pointer-events: all; height: 10%; background-color: rgb(233, 70, 70); } -.message.active.success { +.notification.active.success { pointer-events: all; height: 10%; background-color: rgb(76, 209, 76); diff --git a/web/index.html b/web/index.html index 66525bb..faa0829 100644 --- a/web/index.html +++ b/web/index.html @@ -1,155 +1,231 @@ - + - - - - - - - admin - - - + + + + + + + admin + + + -
-
-

Login

-
- - +
+ +

Login

+
+ + +
+
+ + +
+
+ +
+
-
- - -
-
- -
- -
-
-
-

Add User

-
- - +
+ +

Add User

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
-
-

Change User Data

-
- - +
+ +

Change User Data

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
-
-

Are you sure?

-
- - +
+
+

Are you sure?

+
+ + +
+
-
-
-
-
-

Change Password

-
- - +
+ +

Change Password

+
+ + +
+
+ + +
+
-
- - + +
+ +
+ + + + + + + + + + + + + + +
idusernameemailgroupis_superuserdeletechange passwordchange user data
- -
- -
-
-

-
-
- -
- - - - - - - - - - - - - - -
idusernameemailgroupis_superuserdeletechange passwordchange user data
-
- - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/web/js/login.js b/web/js/login.js index a2af6da..5cc92cd 100644 --- a/web/js/login.js +++ b/web/js/login.js @@ -1,83 +1,93 @@ -const loginform = document.querySelector('#loginform') -const loginformbody = document.querySelector('#loginformbody') -const loginforminputusername = document.querySelector('#loginforminputusername') -const loginforminputpassword = document.querySelector('#loginforminputpassword') -const exitbutton = document.querySelector('#exitbutton') -const usernameheader = document.querySelector('.usernameheader') +const loginform = document.querySelector("#loginform"); +const loginformbody = document.querySelector("#loginformbody"); +const loginforminputusername = document.querySelector( + "#loginforminputusername", +); +const loginforminputpassword = document.querySelector( + "#loginforminputpassword", +); +const exitbutton = document.querySelector("#exitbutton"); +const usernameheader = document.querySelector(".usernameheader"); if (localStorage.getItem("token") === null) { - loginform.classList.add("active") + loginform.classList.add("active"); } -loginformbody.addEventListener('submit', (e) => { - e.preventDefault() - hashPassword(loginforminputpassword.value).then(hashedpassword => { - login(loginforminputusername.value, hashedpassword) - }) -}) +loginformbody.addEventListener("submit", (e) => { + e.preventDefault(); + hashPassword(loginforminputpassword.value).then((hashedpassword) => { + login(loginforminputusername.value, hashedpassword); + }); +}); const login = async (username, password) => { - const response = await fetch(routes.login(), { - method: 'POST', - headers: { - Accept: 'application/json', - // 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - }, - // mode: 'no-cors', - body: `{"username":"${username}","password":"${password}"}` - }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { - if (error.message === "Unexpected end of JSON input") { - notification(`Ошибка на сервере: Создайте файл users.json и запишите в него первого пользователя`, "error") - } else { - notification(`Ошибка на сервере: ${error}`, "error") - } - }) - if (response.error) { - loginforminputusername.classList.add('is-invalid') - loginforminputpassword.classList.add('is-invalid') - notification("Неправильный логин или пароль", "error") - } - if (response.token) { - notification("Успешно", "success") - loginform.classList.remove("active") - localStorage.setItem("token", response.token) - await get_user_data() - loginforminputusername.value = "" - loginforminputpassword.value = "" - loginforminputusername.classList.remove('is-invalid') - loginforminputpassword.classList.remove('is-invalid') - } -} + const response = await fetch(routes.login(), { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: `{"username":"${username}","password":"${password}"}`, + }) + .then((data) => data.json()) + .then((jsondata) => jsondata) + .catch((error) => { + if (error.message === "Unexpected end of JSON input") { + notification( + `Ошибка на сервере: Создайте файл users.json и запишите в него первого пользователя`, + "error", + ); + } else { + notification(`Ошибка на сервере: ${error}`, "error"); + } + }); + if (response.error) { + loginforminputusername.classList.add("is-invalid"); + loginforminputpassword.classList.add("is-invalid"); + notification("Неправильный логин или пароль", "error"); + } + if (response.token) { + notification("Успешно", "success"); + loginform.classList.remove("active"); + localStorage.setItem("token", response.token); + await get_user_data(); + loginforminputusername.value = ""; + loginforminputpassword.value = ""; + loginforminputusername.classList.remove("is-invalid"); + loginforminputpassword.classList.remove("is-invalid"); + } +}; const get_user_data = async () => { - const response = await fetch(routes.user(), { - method: "POST", - body: `{"token":"${localStorage.getItem("token")}"}` - }).then(data => data.json()).then(jsondata => jsondata).catch((error) => { - notification(`Ошибка на сервере: ${error}`, "error") - }) - if (response.is_superuser) { - await get_all_users() - usernameheader.innerHTML = response.username - } else { - document.querySelector('#alluserstablebody').innerHTML = "" - loginform.classList.add("active") - notification("Вы не можете войти потому что не являетесь администратором", "error") - localStorage.clear() - } - if (response.id) { - localStorage.setItem("userdata", JSON.stringify(response)) - } -} + const response = await fetch(routes.user(), { + method: "POST", + body: `{"token":"${localStorage.getItem("token")}"}`, + }) + .then((data) => data.json()) + .then((jsondata) => jsondata) + .catch((error) => { + notification(`Ошибка на сервере: ${error}`, "error"); + }); + if (response.is_superuser) { + await get_all_users(); + usernameheader.innerHTML = response.username; + } else { + document.querySelector("#alluserstablebody").innerHTML = ""; + loginform.classList.add("active"); + notification( + "Вы не можете войти потому что не являетесь администратором", + "error", + ); + localStorage.clear(); + } + if (response.id) { + localStorage.setItem("userdata", JSON.stringify(response)); + } +}; -message.addEventListener('click', () => { - message.classList.remove("active") -}) - -exitbutton.addEventListener('click', () => { - localStorage.removeItem('token') - localStorage.removeItem('userdata') - loginform.classList.add('active') - notification('Вы вышли', "success") -}) +exitbutton.addEventListener("click", () => { + localStorage.removeItem("token"); + localStorage.removeItem("userdata"); + loginform.classList.add("active"); + notification("Вы вышли", "success"); +}); diff --git a/web/js/message.js b/web/js/message.js index 8014faa..e1a9c7b 100644 --- a/web/js/message.js +++ b/web/js/message.js @@ -1,18 +1,34 @@ -const message = document.querySelector('.message') -const messagetext = document.querySelector('.messagetext') +const notificationsection = document.querySelector(".notificationsection"); const notification = (text, type) => { - if (type === "error") { - message.classList.remove('success') - message.classList.add('error') - } else { - message.classList.remove('error') - message.classList.add('success') - } - messagetext.innerHTML = text - message.classList.add('active') + const notificationbox = document.createElement("div"); + const notificationtextbox = document.createElement("div"); + const notificationtext = document.createElement("h3"); + notificationtext.classList.add("notificationtext"); + notificationtextbox.append(notificationtext); + notificationtextbox.classList.add("notificationtextbox"); + notificationtextbox.classList.add("card-header"); + notificationbox.append(notificationtextbox); + notificationbox.classList.add("notification"); + notificationbox.addEventListener("click", () => { + notificationbox.classList.remove("active"); + }); + notificationsection.append(notificationbox); + if (type === "error") { + notificationbox.classList.remove("success"); + notificationbox.classList.add("error"); + } else { + notificationbox.classList.remove("error"); + notificationbox.classList.add("success"); + } + notificationtext.innerHTML = text; + setTimeout(() => { + notificationbox.classList.add("active"); + }, 300); + setTimeout(() => { + notificationbox.classList.remove("active"); setTimeout(() => { - message.classList.remove('active') - }, 2000) -} - + notificationsection.removeChild(notificationbox); + }, 200); + }, 3000); +};