From e11af3ad2abf1abc6524d436097c0a5613330f5e Mon Sep 17 00:00:00 2001 From: QkoSad Date: Sat, 12 Jul 2025 10:50:44 +0300 Subject: [PATCH] fighting weird bug with branches --- Server | 1 - server/CreateComment.cs | 48 +++++++++ server/CreateEducation.cs | 75 ++++++++++++++ server/CreateExperience.cs | 75 ++++++++++++++ server/CreatePost.cs | 44 ++++++++ server/CreateProfile.cs | 80 +++++++++++++++ server/DeleteComment.cs | 19 ++++ server/DeleteEducation.cs | 19 ++++ server/DeleteExperience.cs | 19 ++++ server/DeletePost.cs | 19 ++++ server/DeleteProfile.cs | 19 ++++ server/DeleteRoute.cs | 35 +++++++ server/Login.cs | 118 ++++++++++++++++++++++ server/Program.cs | 199 +++++++++++++++++++++++++++++++++++++ server/Register.cs | 65 ++++++++++++ server/Route.cs | 64 ++++++++++++ server/SecuredRoute.cs | 92 +++++++++++++++++ server/Server.csproj | 17 ++++ server/TODO.md | 20 ++++ server/UpdateComment.cs | 36 +++++++ server/UpdateEducation.cs | 30 ++++++ server/UpdateExperience.cs | 30 ++++++ server/UpdateLikes.cs | 21 ++++ server/UpdatePost.cs | 53 ++++++++++ server/UpdateProfile.cs | 37 +++++++ server/UpdateRoute.cs | 79 +++++++++++++++ server/commands.curl | 145 +++++++++++++++++++++++++++ 27 files changed, 1458 insertions(+), 1 deletion(-) delete mode 160000 Server create mode 100644 server/CreateComment.cs create mode 100644 server/CreateEducation.cs create mode 100644 server/CreateExperience.cs create mode 100644 server/CreatePost.cs create mode 100644 server/CreateProfile.cs create mode 100644 server/DeleteComment.cs create mode 100644 server/DeleteEducation.cs create mode 100644 server/DeleteExperience.cs create mode 100644 server/DeletePost.cs create mode 100644 server/DeleteProfile.cs create mode 100644 server/DeleteRoute.cs create mode 100644 server/Login.cs create mode 100644 server/Program.cs create mode 100644 server/Register.cs create mode 100644 server/Route.cs create mode 100644 server/SecuredRoute.cs create mode 100644 server/Server.csproj create mode 100644 server/TODO.md create mode 100644 server/UpdateComment.cs create mode 100644 server/UpdateEducation.cs create mode 100644 server/UpdateExperience.cs create mode 100644 server/UpdateLikes.cs create mode 100644 server/UpdatePost.cs create mode 100644 server/UpdateProfile.cs create mode 100644 server/UpdateRoute.cs create mode 100644 server/commands.curl diff --git a/Server b/Server deleted file mode 160000 index 4de9704..0000000 --- a/Server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4de9704470fe4bdda312012724bd933569c0fc42 diff --git a/server/CreateComment.cs b/server/CreateComment.cs new file mode 100644 index 0000000..77e4f3a --- /dev/null +++ b/server/CreateComment.cs @@ -0,0 +1,48 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class CreateComment : SecuredRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = ["message", "post"]; + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + bodyParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + + MySqlCommand cmd = new(CreateInsertQuery("comment", bodyParamNames)); + + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + private static void ValidateParams(Dictionary paramsToValidate) + { + { + if (!int.TryParse(paramsToValidate["post"], out int myInt) || myInt < 0) + throw new Exception("Incorect post"); + } + if (paramsToValidate["message"].Length > 1000) + { + throw new Exception("Wrong parameters"); + } + } +} diff --git a/server/CreateEducation.cs b/server/CreateEducation.cs new file mode 100644 index 0000000..d808960 --- /dev/null +++ b/server/CreateEducation.cs @@ -0,0 +1,75 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class CreateEducation : SecuredRoute +{ + private static void ValidateParams(Dictionary paramsToValidate) + { + string format = "yyyy-MM-dd"; + if ( + paramsToValidate["school"].Length > 70 + || string.IsNullOrEmpty(paramsToValidate["school"]) + || paramsToValidate["degree"].Length > 120 + || string.IsNullOrEmpty(paramsToValidate["degree"]) + || paramsToValidate["field"].Length > 100 + || string.IsNullOrEmpty(paramsToValidate["field"]) + || !DateTime.TryParseExact( + paramsToValidate["from_date"], + format, + null, + System.Globalization.DateTimeStyles.None, + out _ + ) + || !DateTime.TryParseExact( + paramsToValidate["to_date"], + format, + null, + System.Globalization.DateTimeStyles.None, + out _ + ) + || paramsToValidate["description"].Length > 1000 + ) + { + throw new Exception("Wrong parameters"); + } + } + + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = + [ + "school", + "degree", + "field", + "from_date", + "to_date", + "description", + ]; + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + bodyParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + + MySqlCommand cmd = new(CreateInsertQuery("education", bodyParamNames)); + + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/CreateExperience.cs b/server/CreateExperience.cs new file mode 100644 index 0000000..d2e96e3 --- /dev/null +++ b/server/CreateExperience.cs @@ -0,0 +1,75 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class CreateExperience : SecuredRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = + [ + "job", + "company", + "location", + "from_date", + "to_date", + "description", + ]; + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + bodyParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + + MySqlCommand cmd = new(CreateInsertQuery("experience", bodyParamNames)); + + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + private static void ValidateParams(Dictionary paramsToValidate) + { + string format = "yyyy-MM-dd"; + if ( + paramsToValidate["job"].Length > 70 + || string.IsNullOrEmpty(paramsToValidate["job"]) + || paramsToValidate["company"].Length > 120 + || string.IsNullOrEmpty(paramsToValidate["company"]) + || paramsToValidate["location"].Length > 100 + || string.IsNullOrEmpty(paramsToValidate["location"]) + || !DateTime.TryParseExact( + paramsToValidate["from_date"], + format, + null, + System.Globalization.DateTimeStyles.None, + out _ + ) + || !DateTime.TryParseExact( + paramsToValidate["to_date"], + format, + null, + System.Globalization.DateTimeStyles.None, + out _ + ) + || paramsToValidate["description"].Length > 1000 + ) + { + throw new Exception("Wrong parameters"); + } + } +} diff --git a/server/CreatePost.cs b/server/CreatePost.cs new file mode 100644 index 0000000..a6b6ae4 --- /dev/null +++ b/server/CreatePost.cs @@ -0,0 +1,44 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class CreatePost : SecuredRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = ["message"]; + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + bodyParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + + MySqlCommand cmd = new(CreateInsertQuery("post", bodyParamNames)); + + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + private static void ValidateParams(Dictionary paramsToValidate) + { + if (paramsToValidate["message"].Length > 1000) + { + throw new Exception("Wrong parameters"); + } + } +} diff --git a/server/CreateProfile.cs b/server/CreateProfile.cs new file mode 100644 index 0000000..11c804f --- /dev/null +++ b/server/CreateProfile.cs @@ -0,0 +1,80 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class CreateProfile : SecuredRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = + [ + "f_name", + "l_name", + "company", + "website", + "location", + "github", + "status", + "bio", + "skills", + "twitter", + "facebook", + "youtube", + "linkedin", + "instagram", + ]; + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + bodyParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + + MySqlCommand cmd = new(CreateInsertQuery("profile", bodyParamNames)); + + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + private static void ValidateParams(Dictionary paramsToValidate) + { + if ( + paramsToValidate["f_name"].Length > 30 + || string.IsNullOrEmpty(paramsToValidate["f_name"]) + || paramsToValidate["l_name"].Length > 30 + || string.IsNullOrEmpty(paramsToValidate["l_name"]) + || paramsToValidate["company"].Length > 70 + || string.IsNullOrEmpty(paramsToValidate["company"]) + || paramsToValidate["website"].Length > 120 + || paramsToValidate["location"].Length > 100 + || string.IsNullOrEmpty(paramsToValidate["location"]) + || paramsToValidate["skills"].Length > 300 + || paramsToValidate["github"].Length > 120 + || paramsToValidate["status"].Length > 20 + || string.IsNullOrEmpty(paramsToValidate["status"]) + || paramsToValidate["bio"].Length > 1000 + || paramsToValidate["twitter"].Length > 100 + || paramsToValidate["facebook"].Length > 100 + || paramsToValidate["youtube"].Length > 100 + || paramsToValidate["linkedin"].Length > 100 + || paramsToValidate["instagram"].Length > 100 + ) + { + throw new Exception("Wrong parameters"); + } + } +} diff --git a/server/DeleteComment.cs b/server/DeleteComment.cs new file mode 100644 index 0000000..9c91a0a --- /dev/null +++ b/server/DeleteComment.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace Server; + +public class DeleteComment : DeleteRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + DeleteFromDB(request, "comment", ["id"], true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/DeleteEducation.cs b/server/DeleteEducation.cs new file mode 100644 index 0000000..15d230f --- /dev/null +++ b/server/DeleteEducation.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace Server; + +public class DeleteEducation : DeleteRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + DeleteFromDB(request, "education", ["id"], true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/DeleteExperience.cs b/server/DeleteExperience.cs new file mode 100644 index 0000000..076dba1 --- /dev/null +++ b/server/DeleteExperience.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace Server; + +public class DeleteExperience : DeleteRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + DeleteFromDB(request, "education", ["id"], true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/DeletePost.cs b/server/DeletePost.cs new file mode 100644 index 0000000..9b34a90 --- /dev/null +++ b/server/DeletePost.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace Server; + +public class DeletePost : DeleteRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + DeleteFromDB(request, "post", ["id"], true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/DeleteProfile.cs b/server/DeleteProfile.cs new file mode 100644 index 0000000..200263d --- /dev/null +++ b/server/DeleteProfile.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace Server; + +public class DeleteProfile : DeleteRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + DeleteFromDB(request, "profile", ["id"], false); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/DeleteRoute.cs b/server/DeleteRoute.cs new file mode 100644 index 0000000..f324d97 --- /dev/null +++ b/server/DeleteRoute.cs @@ -0,0 +1,35 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class DeleteRoute : SecuredRoute +{ + protected static void DeleteFromDB( + HttpListenerRequest request, + string table, + List validParamNames, + bool requireId + ) + // TODO should return error when it cant find the comment + { + // extract userid compare userid to the comment userid + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, validParamNames); + + if (requireId && bodyParamValues["id"] is null) + throw new Exception("missing id"); + + validParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + table += requireId ? " Where user_id=@user_id;" : " WHERE id=@id AND user_id=@user_id;"; + MySqlCommand cmd = new("DELETE from " + table); + + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + } +} diff --git a/server/Login.cs b/server/Login.cs new file mode 100644 index 0000000..113f0d5 --- /dev/null +++ b/server/Login.cs @@ -0,0 +1,118 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using MySql.Data.MySqlClient; +using Newtonsoft.Json; + +namespace Server; + +public class Login : SecuredRoute +{ + private static void ValidateParams(Dictionary paramsToValidate) + { + if ( + string.IsNullOrEmpty(paramsToValidate["email"]) + || string.IsNullOrEmpty(paramsToValidate["password"]) + ) + throw new Exception("Invalid parameters"); + } + + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = ["email", "password"]; + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + string query = + @"SELECT id, password FROM user + WHERE email=@email;"; + MySqlCommand cmd = new(query); + cmd.Parameters.AddWithValue("@email", bodyParamValues["email"]); + + var userId = ExtractUserIdFromDB(cmd, bodyParamValues["password"]); + + string? jsonResponse = JsonConvert.SerializeObject(GenerateToken(userId)); + // prepare response + SendSuccess(response, jsonResponse); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + private static string ExtractUserIdFromDB(MySqlCommand cmd, string password) + { + using MySqlConnection conn = new(connectionString); + cmd.Connection = conn; + conn.Open(); + // execute query and read results + MySqlDataReader reader = cmd.ExecuteReader(); + string? userId = ""; + string? hashedPass = ""; + while (reader.Read()) + { + userId = Convert.ToString(reader["id"]); + hashedPass = reader.GetString("password"); + } + // check username + if (string.IsNullOrEmpty(userId)) + { + throw new Exception("Invalid Username or Password"); + } + //check password + if ( + string.IsNullOrEmpty(password) + || string.IsNullOrEmpty(hashedPass) + || !VerifyPassword(password, hashedPass) + ) + { + throw new Exception("Invalid Username or Password"); + } + return userId; + } + + public static string GenerateToken(string user) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: "TimeLogServer", + audience: "TimeLogWebsite", + claims: [new Claim("user", user)], + expires: DateTime.Now.AddHours(2), + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public static bool VerifyPassword(string enteredPassword, string storedHash) + { + byte[] hashBytes = Convert.FromBase64String(storedHash); + + byte[] salt = new byte[16]; + Array.Copy(hashBytes, 0, salt, 0, 16); + + using var pbkdf2 = new Rfc2898DeriveBytes( + enteredPassword, + salt, + 10000, + HashAlgorithmName.SHA256 + ); + byte[] newHash = pbkdf2.GetBytes(32); + + for (int i = 0; i < 32; i++) + { + if (newHash[i] != hashBytes[i + 16]) + return false; + } + return true; + } +} diff --git a/server/Program.cs b/server/Program.cs new file mode 100644 index 0000000..103fc8b --- /dev/null +++ b/server/Program.cs @@ -0,0 +1,199 @@ +using System.Net; +using System.Text; + +namespace Server; + +class Program +{ + static void Main() + { + // create server + HttpListener listener = new(); + // routes need to be added first + listener.Prefixes.Add("http://localhost:5000/api/login/"); + listener.Prefixes.Add("http://localhost:5000/api/register/"); + listener.Prefixes.Add("http://localhost:5000/api/posts/"); + listener.Prefixes.Add("http://localhost:5000/api/posts/like/"); + listener.Prefixes.Add("http://localhost:5000/api/posts/unlike/"); + listener.Prefixes.Add("http://localhost:5000/api/comment/"); + listener.Prefixes.Add("http://localhost:5000/api/profile/"); + listener.Prefixes.Add("http://localhost:5000/api/profile/experience/"); + listener.Prefixes.Add("http://localhost:5000/api/profile/education/"); + // listen + listener.Start(); + Console.WriteLine("Server is listening on http://localhost:5000/"); + while (true) + { + HttpListenerContext context = listener.GetContext(); + HttpListenerRequest request = context.Request; + HttpListenerResponse response = context.Response; + response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:5173"); + response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + // url after localhost:5000/ + string uri; + if (request != null && request.Url != null) + uri = request.Url.AbsolutePath; + else + return; + switch (request.HttpMethod) + { + case "GET": + HandleGet(uri, request, response); + break; + case "POST": + HandlePost(uri, request, response); + break; + case "DELETE": + HandleDelete(uri, request, response); + break; + case "PUT": + HandlePut(uri, request, response); + break; + default: + HandleMissingPath(response); + break; + } + } + } + + private static void HandlePut( + string uri, + HttpListenerRequest request, + HttpListenerResponse response + ) + { + if (request.HasEntityBody) + switch (uri) + { + case "/api/profile": + UpdateProfile.HandleRequest(request, response); + break; + case "/api/education": + UpdateEducation.HandleRequest(request, response); + break; + case "/api/experience": + UpdateExperience.HandleRequest(request, response); + break; + case "/api/post": + UpdatePost.HandleRequest(request, response); + break; + case "/api/comment": + UpdateComment.HandleRequest(request, response); + break; + case "/api/posts/like": + UpdatePost.HandleLikes(request, response); + break; + // case "/api/posts/unlike": + // RemoveLike.HandleRequest(request, response); + // break; + default: + HandleMissingPath(response); + break; + } + else + { + HandleMissingPath(response); + } + } + + private static void HandleDelete( + string uri, + HttpListenerRequest request, + HttpListenerResponse response + ) + { + if (request.HasEntityBody) + switch (uri) + { + case "/api/profile": + DeleteProfile.HandleRequest(request, response); + break; + case "/api/profile/education": + DeleteEducation.HandleRequest(request, response); + break; + case "/api/profile/experience": + DeleteExperience.HandleRequest(request, response); + break; + case "/api/posts": + DeletePost.HandleRequest(request, response); + break; + case "/api/comment": + DeleteComment.HandleRequest(request, response); + break; + default: + HandleMissingPath(response); + break; + } + else + { + HandleMissingPath(response); + } + } + + private static void HandlePost( + string uri, + HttpListenerRequest request, + HttpListenerResponse response + ) + { + if (request.HasEntityBody) + switch (uri) + { + case "/api/profile": + CreateProfile.HandleRequest(request, response); + break; + case "/api/profile/education": + CreateEducation.HandleRequest(request, response); + break; + case "/api/profile/experience": + CreateExperience.HandleRequest(request, response); + break; + case "/api/posts": + CreatePost.HandleRequest(request, response); + break; + case "/api/comment": + CreateComment.HandleRequest(request, response); + break; + case "/api/register": + Register.HandleRequest(request, response); + break; + case "/api/login": + Login.HandleRequest(request, response); + break; + default: + HandleMissingPath(response); + break; + } + else + { + HandleMissingPath(response); + } + } + + private static void HandleGet( + string uri, + HttpListenerRequest request, + HttpListenerResponse response + ) + { + switch (uri) + { + default: + HandleMissingPath(response); + break; + } + } + + private static void HandleMissingPath(HttpListenerResponse response) + { + response.StatusCode = 404; + string errorMessage = "Not Found"; + byte[] buffer = Encoding.UTF8.GetBytes(errorMessage); + response.ContentType = "text/plain"; + response.ContentLength64 = buffer.Length; + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Write(buffer, 0, buffer.Length); + } +} diff --git a/server/Register.cs b/server/Register.cs new file mode 100644 index 0000000..6664a7b --- /dev/null +++ b/server/Register.cs @@ -0,0 +1,65 @@ +using System.Net; +using System.Security.Cryptography; +using MySql.Data.MySqlClient; + +namespace Server; + +public class Register : SecuredRoute +{ + private static void ValidateParams(Dictionary paramsToValidate) + { + if ( + string.IsNullOrEmpty(paramsToValidate["username"]) + || paramsToValidate["username"].Length > 30 + || paramsToValidate["username"].Length < 4 + || string.IsNullOrEmpty(paramsToValidate["email"]) + || paramsToValidate["email"].Length > 50 + || paramsToValidate["email"].Length < 6 + || string.IsNullOrEmpty(paramsToValidate["password"]) + || paramsToValidate["password"].Length > 50 + || paramsToValidate["password"].Length < 10 + ) + { + throw new Exception("Wrong parameters"); + } + } + + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List bodyParamNames = ["username", "email", "password"]; + var bodyParamValues = ExtractBody(request, bodyParamNames); + ValidateParams(bodyParamValues); + + MySqlCommand cmd = new(CreateInsertQuery("user", bodyParamNames)); + bodyParamValues["password"] = HashPassword(bodyParamValues["password"]); + cmd = AddValuesToCmd(bodyParamValues, cmd); + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + private static string HashPassword(string password) + { + byte[] salt = new byte[16]; + RandomNumberGenerator.Fill(salt); + + using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256); + byte[] hash = pbkdf2.GetBytes(32); + + byte[] hashBytes = new byte[48]; // 16 (salt) + 32 (hash) + Array.Copy(salt, 0, hashBytes, 0, 16); + Array.Copy(hash, 0, hashBytes, 16, 32); + + return Convert.ToBase64String(hashBytes); + } +} diff --git a/server/Route.cs b/server/Route.cs new file mode 100644 index 0000000..ce9e8b3 --- /dev/null +++ b/server/Route.cs @@ -0,0 +1,64 @@ +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; + +namespace Server; + +public abstract class Route +{ + public static readonly string connectionString = + "server=127.0.0.1;uid=monty;pwd=some_pass;database=devcon"; + + public static void SendError(HttpListenerResponse response, Exception ex) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + string errorMessage = $"Error: {ex.Message}"; + byte[] buffer = Encoding.UTF8.GetBytes(errorMessage); + response.ContentType = "text/plain"; + response.ContentLength64 = buffer.Length; + response.OutputStream.Write(buffer, 0, buffer.Length); + response.Close(); + } + + public static void SendSuccess(HttpListenerResponse response) + { + response.StatusCode = (int)HttpStatusCode.OK; + response.StatusDescription = "Status OK"; + response.Close(); + } + + public static void SendSuccess(HttpListenerResponse response, string jsonResponse) + { + response.StatusCode = (int)HttpStatusCode.OK; + response.StatusDescription = "Status OK"; + byte[] buffer = Encoding.UTF8.GetBytes(jsonResponse); + response.ContentType = "application/json"; + response.ContentLength64 = buffer.Length; + response.OutputStream.Write(buffer, 0, buffer.Length); + response.Close(); + } + + public static bool ValidateDate(string date) + { + Regex regex = new(@"^\d{4}-\d{2}-\d{2}$"); + return regex.IsMatch(date); + } + + protected static Dictionary ExtractBody( + HttpListenerRequest request, + List allowedParams + ) + { + using StreamReader bodyReader = new(request.InputStream, request.ContentEncoding); + JObject bodyJO = JObject.Parse(bodyReader.ReadToEnd()); + + Dictionary bodyParamValues = []; + foreach (var prop in bodyJO.Properties()) + { + if (allowedParams.Contains(prop.Name)) + bodyParamValues[prop.Name] = prop.Value.ToString(); + } + return bodyParamValues; + } +} diff --git a/server/SecuredRoute.cs b/server/SecuredRoute.cs new file mode 100644 index 0000000..863eaf0 --- /dev/null +++ b/server/SecuredRoute.cs @@ -0,0 +1,92 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using MySql.Data.MySqlClient; + +namespace Server; + +public class SecuredRoute : Route +{ + protected static readonly string secretKey = + "stronk-key-much-sercret-much-more-stronk-stronk-key-much-sercret-much-more-stronk"; + protected delegate void DelegateValidate(Dictionary bodyparamValues); + + protected static string ExtractUserId(HttpListenerRequest request) + { + var headers = request.Headers; + string token = headers["token"] ?? ""; + string? usernameClaim = GetUserFromToken(token); + if ( + !string.IsNullOrEmpty(token) + && !ValidateToken(token) + && string.IsNullOrEmpty(usernameClaim) + ) + return ""; + else + return usernameClaim; + } + + protected static MySqlCommand AddValuesToCmd( + Dictionary values, + MySqlCommand cmd + ) + { + foreach (var item in values) + { + cmd.Parameters.AddWithValue("@" + item.Key, item.Value); + } + return cmd; + } + + // create an insert route and move this func there + protected static string CreateInsertQuery(string table, List valuesToAdd) + { + string query = + "INSERT INTO " + + table + + "(" + + string.Join(",", valuesToAdd) + + ") VALUES(@" + + string.Join(",@", valuesToAdd) + + ");"; + return query; + } + + private static bool ValidateToken(string token) + { + try + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); + var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidIssuer = "TimeLogServer", + ValidAudience = "TimeLogWebsite", + IssuerSigningKey = key, + }; + + var principal = tokenHandler.ValidateToken( + token, + validationParameters, + out SecurityToken validatedToken + ); + return validatedToken != null; + } + catch + { + return false; + } + } + + private static string GetUserFromToken(string token) + { + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + string? usernameClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "user")?.Value; + return string.IsNullOrEmpty(usernameClaim) ? "" : usernameClaim; + } +} diff --git a/server/Server.csproj b/server/Server.csproj new file mode 100644 index 0000000..1d1315c --- /dev/null +++ b/server/Server.csproj @@ -0,0 +1,17 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + diff --git a/server/TODO.md b/server/TODO.md new file mode 100644 index 0000000..1a7126a --- /dev/null +++ b/server/TODO.md @@ -0,0 +1,20 @@ +GET api/auth/ -- get token +GET api/posts +GET api/posts/:id +GET api/profile/me +GET api/profile/user/:user_id +GET api/profile/github/:username +# PUT api/profile/experience +# PUT api/profile/education +# PUT api/posts/like/:id +# PUT api/posts/unlike/:id +POST api/users -- register user +POST api/auth/ -- login +# POST api/profile +# POST api/posts +# POST api/posts/comment/:id +DELETE api/profile -- delete everything the user has done +# DELETE api/profile/education/:exp_id +# DELETE api/posts/comment/:id/:comment_id +# DELETE api/profile/experience/:exp_id +# DELETE api/posts/:id diff --git a/server/UpdateComment.cs b/server/UpdateComment.cs new file mode 100644 index 0000000..4a4032b --- /dev/null +++ b/server/UpdateComment.cs @@ -0,0 +1,36 @@ +using System.Net; + +namespace Server; + +public class UpdateComment : UpdateRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List validParamNames = ["message", "post", "id"]; + + UpdateDb(request, "comment", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + public static void LikeComment(HttpListenerRequest request, HttpListenerResponse response) + { + try + { + List validParamNames = ["likes", "id"]; + + UpdateDb(request, "comment", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/UpdateEducation.cs b/server/UpdateEducation.cs new file mode 100644 index 0000000..fc95fe1 --- /dev/null +++ b/server/UpdateEducation.cs @@ -0,0 +1,30 @@ +using System.Net; + +namespace Server; + +public class UpdateEducation : UpdateRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + List validParamNames = + [ + "school", + "degree", + "field", + "from_date", + "to_date", + "description", + "id", + ]; + + try + { + UpdateDb(request, "education", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/UpdateExperience.cs b/server/UpdateExperience.cs new file mode 100644 index 0000000..937be17 --- /dev/null +++ b/server/UpdateExperience.cs @@ -0,0 +1,30 @@ +using System.Net; + +namespace Server; + +public class UpdateExperience : UpdateRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + List validParamNames = + [ + "job", + "company", + "location", + "from_date", + "to_date", + "description", + "id", + ]; + + try + { + UpdateDb(request, "experience", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/UpdateLikes.cs b/server/UpdateLikes.cs new file mode 100644 index 0000000..3fc452b --- /dev/null +++ b/server/UpdateLikes.cs @@ -0,0 +1,21 @@ +using System.Net; + +namespace Server; + +public class UpdateLikes : UpdateRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + List validParamNames = ["id"]; + + try + { + UpdateLikes(request, "post", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/UpdatePost.cs b/server/UpdatePost.cs new file mode 100644 index 0000000..9846d69 --- /dev/null +++ b/server/UpdatePost.cs @@ -0,0 +1,53 @@ +using System.Net; + +namespace Server; + +public class UpdatePost : UpdateRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + List validParamNames = + [ + "f_name", + "l_name", + "company", + "website", + "location", + "github", + "status", + "bio", + "skills", + "twitter", + "facebook", + "youtube", + "linkedin", + "instagram", + "id", + ]; + + try + { + UpdateDb(request, "post", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } + + public static void HandleLikes(HttpListenerRequest request, HttpListenerResponse response) + { + List validParamNames = ["id"]; + + try + { + UpdateLikes(request, "post", validParamNames, true); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/UpdateProfile.cs b/server/UpdateProfile.cs new file mode 100644 index 0000000..389093e --- /dev/null +++ b/server/UpdateProfile.cs @@ -0,0 +1,37 @@ +using System.Net; + +namespace Server; + +public class UpdateProfile : UpdateRoute +{ + public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) + { + List validParamNames = + [ + "f_name", + "l_name", + "company", + "website", + "location", + "github", + "status", + "bio", + "skills", + "twitter", + "facebook", + "youtube", + "linkedin", + "instagram", + ]; + + try + { + UpdateDb(request, "profile", validParamNames, false); + SendSuccess(response); + } + catch (Exception ex) + { + SendError(response, ex); + } + } +} diff --git a/server/UpdateRoute.cs b/server/UpdateRoute.cs new file mode 100644 index 0000000..5b6d401 --- /dev/null +++ b/server/UpdateRoute.cs @@ -0,0 +1,79 @@ +using System.Net; +using MySql.Data.MySqlClient; + +namespace Server; + +public class UpdateRoute : SecuredRoute +{ + //TODO create editied time on field in db for comments and posts + //TODO all updates need validation and deletes + protected static void UpdateDb( + HttpListenerRequest request, + string table, + List validParamNames, + bool requireId + ) + { + string user_id = ExtractUserId(request); + var bodyParamValues = ExtractBody(request, validParamNames); + + if (requireId && bodyParamValues["id"] is null) + throw new Exception("missing id"); + + string temp = ""; + foreach (var item in bodyParamValues) + { + temp += item.Key + "=\"" + item.Value + "\","; + } + // remove last chat from str + temp = temp[..^1]; + + validParamNames.Add("user_id"); + bodyParamValues["user_id"] = user_id; + + temp += requireId ? " WHERE user_id=@user_id AND id=@id;" : " WHERE user_id=@user_id;"; + + MySqlCommand cmd = new("UPDATE " + table + " SET " + temp); + cmd = AddValuesToCmd(bodyParamValues, cmd); + + using MySqlConnection conn = new(connectionString); + conn.Open(); + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + } + + protected static void UpdateLikes( + HttpListenerRequest request, + string table, + List validParamNames, + bool requireId + ) + { + var bodyParamValues = ExtractBody(request, validParamNames); + if (requireId && bodyParamValues["id"] is null) + throw new Exception("missing id"); + + string query = "SELECT likes from post Where id=@id;"; + MySqlCommand cmd2 = new(query); + using MySqlConnection conn = new(connectionString); + cmd2.Connection = conn; + conn.Open(); + cmd2.Parameters.AddWithValue("@id", bodyParamValues["id"]); + MySqlDataReader reader = cmd2.ExecuteReader(); + string? id = ""; + string? likes = ""; + while (reader.Read()) + { + id = Convert.ToString(reader["id"]); + likes = Convert.ToString(reader["likes"]); + } + Console.WriteLine(id); + + query = "Update post SET likes=2 where id=1;"; + MySqlCommand cmd = new(query); + cmd = AddValuesToCmd(bodyParamValues, cmd); + + cmd.Connection = conn; + cmd.ExecuteNonQuery(); + } +} diff --git a/server/commands.curl b/server/commands.curl new file mode 100644 index 0000000..42e225d --- /dev/null +++ b/server/commands.curl @@ -0,0 +1,145 @@ +# register +curl -X POST localhost:5000/api/register +-d +{ + "username":"tombo" , + "password":"1234567890" , + "email":"temp@mail.com" +} +# login +curl -X POST localhost:5000/api/login +-d +{ + "password":"1234567890" , + "email":"temp@mail.com" +} +# add profile +curl -X POST localhost:5000/api/profile +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "f_name":"somef_name" , + "l_name":"somel_name" , + "company":"somecompany" , + "website":"somewebsite" , + "location":"somelocation" , + "skills":"someskills" , + "github":"somegithub" , + "status":"somestatus" , + "bio":"somebio" , + "twitter":"sometwitter" , + "youtube":"someyoutube" , + "facebook":"somefacebook" , + "linkedin":"somelinkedi" , + "instagram":"someinstagram" +} + +#update profile +curl -X PUT localhost:5000/api/profile/update +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "f_name":"Rombo" , + "l_name":"Tombo" , + "website":"TOMBOBOBOBO" +} +# "company":"somecompany" , +# "website":"somewebsite" , +# "location":"somelocation" , +# "skills":"someskills" , +# "github":"somegithub" , +# "status":"somestatus" , +# "bio":"somebio" , +# "twitter":"sometwitter" , +# "youtube":"someyoutube" , +# "facebook":"somefacebook" , +# "linkedin":"somelinkedi" , +# "instagram":"someinstagram" +# } + +# add education +curl -X POST localhost:5000/api/profile/education +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "school": "someschool" , + "degree": "somedegree" , + "field": "somefield" , + "from_date": "2020-01-01" , + "to_date": "2020-02-02" , + "description": "somedescription" +} +#update education +curl -X PUT localhost:5000/api/profile/education +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "school": "TOOBOBOOB" , + "degree": "somedegree" , + "field": "somefield" , + "from_date": "2020-01-01" , + "to_date": "2020-02-02" , + "description": "somedescription", + "id":"1" +} + +# add exp +curl -X POST localhost:5000/api/profile/experience +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "job":"somejob" , + "company":"somecompany" , + "location":"somelocation" , + "from_date":"2020-01-01" , + "to_date":"2020-02-02" , + "description":"12312312312312312312" +} + +# add post +curl -X POST localhost:5000/api/post +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "message":"lskadfjalsk;djf;laksdjf;lsa" +} +#update profile +curl -X PUT localhost:5000/api/post/like +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "id":"4" +} + +# add comment +curl -X POST localhost:5000/api/comment +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "message":"lskadfjalsk;djf;laksdjf;lsa" , + "post":"1" +} + +# remove comment +curl -X DELETE localhost:5000/api/comment +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "id":"2" +} + +# remove education +curl -X DELETE localhost:5000/api/profile/education +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "id":"2" +} + +# remove experience +curl -X DELETE localhost:5000/api/profile/experience +-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg" +-d +{ + "id":"2" +}