refractoring backend

This commit is contained in:
QkoSad
2024-12-09 18:43:03 +02:00
parent 80040638a6
commit 2db33bb966
53 changed files with 814 additions and 2389 deletions
+134
View File
@@ -0,0 +1,134 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.IdentityModel.Tokens;
using MySql.Data.MySqlClient;
using Newtonsoft.Json.Linq;
namespace TimelogBackend;
public class CreateLog : Route
{
private static readonly string secretKey =
"stronk-key-much-sercret-much-more-stronk-stronk-key-much-sercret-much-more-stronk";
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
try
{
// check header
var headers = request.Headers;
string token = headers["token"] ?? "";
if (!string.IsNullOrEmpty(token) && !ValidateToken(token))
{
throw new Exception("Invalid token");
}
MySqlCommand cmd = new();
string body;
using (StreamReader bodyReader = new(request.InputStream, request.ContentEncoding))
{
body = bodyReader.ReadToEnd();
}
JObject jsonObject = JObject.Parse(body);
string project = jsonObject["project"]?.ToString() ?? "";
string time = jsonObject["time"]?.ToString() ?? "";
string date = jsonObject["date"]?.ToString() ?? "";
// TODO check if the hours on given date don't combine to more
// than 8
if (!ValidateTime(time))
{
throw new Exception("Incorrect date format");
}
if (!ValidateDate(date))
{
throw new Exception("Incorrect date format");
}
// validate user
string? usernameClaim = GetUserFromToken(token);
if (string.IsNullOrEmpty(usernameClaim))
{
throw new Exception("wrong user id");
}
// validate project
// TODO better project validation
if (string.IsNullOrEmpty(project))
{
throw new Exception("wrong project");
}
SaveTimeLogToDatabase(usernameClaim, project, date, time);
SendSuccess(response);
}
catch (Exception ex)
{
SendError(response, ex);
}
}
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 bool ValidateTime(string time)
{
return int.TryParse(time, out int myInt) && myInt >= 0 && myInt <= 8;
}
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;
}
private static void SaveTimeLogToDatabase(
string username,
string project,
string date,
string time
)
{
using MySqlConnection conn = new(connectionString);
conn.Open();
using MySqlCommand cmd = new(
@"INSERT INTO Timelog(user, project, date, time)
VALUES(@user, @project, @date, @time);",
conn
);
cmd.Parameters.AddWithValue("@user", username);
cmd.Parameters.AddWithValue("@project", project);
cmd.Parameters.AddWithValue("@date", date);
cmd.Parameters.AddWithValue("@time", time);
cmd.ExecuteNonQuery();
}
}
+126
View File
@@ -0,0 +1,126 @@
using System.Net;
using MySql.Data.MySqlClient;
namespace TimelogBackend;
public class CreateProcedure : Route
{
public static void HandleRequest(HttpListenerResponse response)
{
try
{
MySqlCommand cmd = new();
using MySqlConnection conn = new(connectionString);
conn.Open();
cmd.Connection = conn;
cmd.CommandText =
@" CREATE PROCEDURE fill_timelog ()
BEGIN
DECLARE j INT DEFAULT 1;
DECLARE users INT DEFAULT 1;
DECLARE logs INT;
DECLARE hours FLOAT;
DECLARE project INT;
DECLARE curDate DATE DEFAULT '2024-11-18';
DECLARE h2 INT;
WHILE users <= 100 DO
SET logs = FLOOR(1 + (RAND() * 20));
SET j = 1;
WHILE j <= logs DO
SET project = FLOOR(1 + (RAND() * 3));
SET curDate = DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * DATEDIFF('2020-02-01', '2020-01-01')) DAY);
SET hours = (RAND() * (8 - 0.25)) + 0.25;
SELECT SUM(time) INTO h2
FROM Timelog
WHERE date = curdate && user = users;
WHILE(h2 + hours) > 8 DO
SET curDate = DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * DATEDIFF('2020-02-01', '2020-01-01')) DAY);
SELECT SUM(time)INTO h2
FROM Timelog
WHERE date = curdate && user = users;
END WHILE;
INSERT INTO Timelog(user, project, date, time) VALUES(users, project, curDate, hours);
SET j = j + 1;
END WHILE;
SET users = users + 1;
END WHILE;
END;";
cmd.ExecuteNonQuery();
cmd.CommandText =
@"CREATE PROCEDURE InitDB()
BEGIN
DECLARE i INT DEFAULT 1;
TRUNCATE TABLE Timelog;
TRUNCATE TABLE Project;
SET foreign_key_checks = 0;
TRUNCATE TABLE User;
SET foreign_key_checks = 1;
INSERT INTO Project(name) VALUES('My own'),('Outcons'),('Free Time');
CREATE TEMPORARY TABLE temp_fname (fname VARCHAR(255));
INSERT INTO temp_fname (fname) VALUES
( 'John' ),
( 'Gringo' ),
( 'Mark' ),
( 'Lisa' ),
( 'Maria' ),
( 'Sonya' ),
( 'Philip' ),
( 'Jose' ),
( 'Lorenzo' ),
( 'George' ),
( 'Justin' );
CREATE TEMPORARY TABLE temp_lname (lname VARCHAR(255));
INSERT INTO temp_lname (lname) VALUES
( 'Johnson' ),
( 'Lamas' ),
( 'Jackson' ),
( 'Brown' ),
( 'Mason' ),
( 'Rodriguez' ),
( 'Roberts' ),
( 'Thomas' ),
( 'Rose' ),
( 'McDonalds' );
CREATE TEMPORARY TABLE temp_mail (mail VARCHAR(255));
INSERT INTO temp_mail (mail) VALUES
( 'hotmail.com' ),
( 'gmail.com' ),
( 'live.com' );
WHILE i <= 100 DO
INSERT INTO User (f_name, l_name, mail)
SELECT
(SELECT fname FROM temp_fname ORDER BY RAND() LIMIT 1),
(SELECT lname FROM temp_lname ORDER BY RAND() LIMIT 1),
(SELECT mail FROM temp_mail ORDER BY RAND() LIMIT 1);
SET i = i + 1;
END WHILE;
UPDATE User
SET User.mail = CONCAT(User.f_name,'.', User.l_name,'@', User.mail);
CALL fill_timelog();
DROP TABLE temp_mail;
DROP TABLE temp_fname;
DROP TABLE temp_lname;
END;";
cmd.ExecuteNonQuery();
// prepare response
SendSuccess(response);
}
catch (Exception ex)
{
SendError(response, ex);
}
}
}
+126
View File
@@ -0,0 +1,126 @@
using System.Net;
using MySql.Data.MySqlClient;
using Newtonsoft.Json;
namespace TimelogBackend;
// there should be a better way to deal with data comming from sql
public class Log
{
public object? FName { get; set; }
public object? LName { get; set; }
public object? Mail { get; set; }
public object? Name { get; set; }
public object? Time { get; set; }
public object? Date { get; set; }
public object? User { get; set; }
}
public class Getall : Route
{
private static string ConstructQuery(
string from,
string to,
string order,
string offset,
string sortby
)
{
string mainQuery =
@"SELECT u.f_name,u.l_name,u.mail,p.name,t.time,t.date,t.user
FROM Timelog t
INNER JOIN Project p ON p.id=t.project
INNER JOIN User u ON u.id=t.user ";
// this shenanigan is needed to remove the "" around group by
string offsetQuery = " LIMIT 10 OFFSET " + offset + ";";
// depending on the incoming parameters construct a Query
if (!string.IsNullOrEmpty(to) && !string.IsNullOrEmpty(from))
{
mainQuery += AddWhereClause(from, to);
}
if (!string.IsNullOrEmpty(sortby))
{
mainQuery += AddSortBy(sortby, order);
}
if (!int.TryParse(offset, out int myInt) || myInt < 0)
throw new Exception("Incorect offset");
return mainQuery + offsetQuery;
}
private static string AddWhereClause(string from, string to)
{
if (!ValidateDate(to) || !ValidateDate(from))
{
throw new Exception("Incorrect date format");
}
string whereQuery = " WHERE t.date BETWEEN @from AND @to ";
return whereQuery;
}
private static string AddSortBy(string sortby, string order)
{
List<string> validSorting = ["f_name", "l_name", "mail", "time", "date", "user", "project"];
if (!validSorting.Contains(sortby))
{
throw new Exception("Incorrect sorting value");
}
string orderQuery = " ORDER BY " + sortby + " " + order;
return orderQuery;
}
private static List<Log> ExtractDataFromDB(MySqlCommand cmd)
{
using MySqlConnection conn = new(connectionString);
conn.Open();
cmd.Connection = conn;
// execute query and read results
MySqlDataReader reader = cmd.ExecuteReader();
List<Log> entries = [];
while (reader.Read())
{
entries.Add(
new Log
{
FName = reader["f_name"],
LName = reader["l_name"],
User = reader["user"],
Date = reader["date"],
Name = reader["name"],
Time = reader["time"],
Mail = reader["mail"],
}
);
}
return entries;
}
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
try
{
// extract data from url
var queryString = request.QueryString;
string? from = queryString["from"] ?? "";
string? to = queryString["to"] ?? "";
string? sortby = queryString["sortby"] ?? "";
string? offset = queryString["offset"] ?? "";
string? order = queryString["order"] ?? "";
order = order == "true" ? "ASC" : "DESC";
// SQL
MySqlCommand cmd = new(ConstructQuery(from, to, order, offset, sortby));
cmd.Parameters.AddWithValue("@from", from);
cmd.Parameters.AddWithValue("@to", to);
var entries = ExtractDataFromDB(cmd);
// serialize JSON
string jsonResponse = JsonConvert.SerializeObject(entries);
SendSuccess(response, jsonResponse);
}
catch (Exception ex)
{
SendError(response, ex);
}
}
}
+98
View File
@@ -0,0 +1,98 @@
using System.Net;
using MySql.Data.MySqlClient;
using Newtonsoft.Json;
namespace TimelogBackend;
public class TopTen
{
public object? User { get; set; }
public object? Date { get; set; }
public object? Project { get; set; }
public object? FName { get; set; }
public object? LName { get; set; }
public object? Name { get; set; }
public object? TotalTime { get; set; }
}
public class Gettopten : Route
{
private static List<TopTen> ExtractDataFromDB(MySqlCommand cmd)
{
using MySqlConnection conn = new(connectionString);
cmd.Connection = conn;
conn.Open();
// Execute the query and read the results
MySqlDataReader reader = cmd.ExecuteReader();
List<TopTen> entries = [];
while (reader.Read())
{
entries.Add(
new TopTen
{
User = reader["user"],
Date = reader["date"],
Project = reader["project"],
FName = reader["f_name"],
LName = reader["l_name"],
Name = reader["name"],
TotalTime = reader["total_time"],
}
);
}
return entries;
}
private static void ValidateQueryStrings(string from, string to, string filterBy)
{
if (!string.IsNullOrEmpty(to) && !string.IsNullOrEmpty(from))
{
ValidateDate(to);
ValidateDate(from);
}
else
{
throw new Exception("Empty date format");
}
if (string.IsNullOrEmpty(filterBy))
{
throw new Exception("Empty filterby");
}
if (filterBy != "user" && filterBy != "project")
{
throw new Exception("Incorrect filterby");
}
}
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
try
{
var queryString = request.QueryString;
string? from = queryString["from"] ?? "";
string? to = queryString["to"] ?? "";
string? filterBy = queryString["filterBy"] ?? "";
ValidateQueryStrings(from, to, filterBy);
string query =
@"SELECT t.user,t.date,t.project,u.f_name,u.l_name,p.name,SUM(t.time) as total_time
FROM Timelog t
INNER JOIN Project p ON p.id=t.project
INNER JOIN User u ON u.id=t.user
WHERE t.date BETWEEN @from AND @to
GROUP BY "
+ filterBy
+ " ORDER BY total_time DESC LIMIT 10;";
MySqlCommand cmd = new(query);
cmd.Parameters.AddWithValue("@from", from);
cmd.Parameters.AddWithValue("@to", to);
var entries = ExtractDataFromDB(cmd);
string jsonResponse = JsonConvert.SerializeObject(entries);
SendSuccess(response, jsonResponse);
}
catch (Exception ex)
{
SendError(response, ex);
}
}
}
+59
View File
@@ -0,0 +1,59 @@
using System.Dynamic;
using System.Net;
using MySql.Data.MySqlClient;
using Newtonsoft.Json;
namespace TimelogBackend;
public class Getuser : Route
{
private static dynamic ExtractDataFromDB(MySqlCommand cmd)
{
using MySqlConnection conn = new(connectionString);
conn.Open();
cmd.Connection = conn;
// execute query and read results
MySqlDataReader reader = cmd.ExecuteReader();
dynamic expando = new ExpandoObject();
while (reader.Read())
{
((IDictionary<string?, object>)expando)[reader["name"].ToString()] = reader[
"SUM(t.time)"
];
}
return expando;
}
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
try
{
var queryString = request.QueryString;
string? userid = queryString["userid"];
if (string.IsNullOrEmpty(userid))
{
throw new Exception("Missing userid");
}
// prepare SQL query
string query =
@"SELECT p.name, SUM(t.time)
FROM Timelog t
INNER JOIN Project p ON p.id=t.project
INNER JOIN User u ON u.id=t.user
WHERE User = @userid
GROUP BY name;";
MySqlCommand cmd = new(query);
cmd.Parameters.AddWithValue("@userid", userid);
var expando = ExtractDataFromDB(cmd);
// serialize JSON
string jsonResponse = JsonConvert.SerializeObject(expando);
// prepare response
SendSuccess(response, jsonResponse);
}
catch (Exception ex)
{
SendError(response, ex);
}
}
}
+119
View File
@@ -0,0 +1,119 @@
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;
using Newtonsoft.Json.Linq;
namespace TimelogBackend;
public class Login : Route
{
private static readonly string secretKey =
"stronk-key-much-sercret-much-more-stronk-stronk-key-much-sercret-much-more-stronk";
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;
}
private static string ExtractDataFromDB(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 void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
try
{
// extract data from body
string body;
using (StreamReader bodyReader = new(request.InputStream, request.ContentEncoding))
{
body = bodyReader.ReadToEnd();
}
JObject jsonObject = JObject.Parse(body);
string mail = jsonObject["mail"]?.ToString() ?? "";
string password = jsonObject["password"]?.ToString() ?? "";
// prepare SQL query
string query =
@"SELECT u.id, password FROM User u
INNER JOIN Password p ON p.user=u.id
WHERE mail=@mail;";
MySqlCommand cmd = new(query);
cmd.Parameters.AddWithValue("@mail", mail);
var userId = ExtractDataFromDB(cmd, password);
string? jsonResponse = JsonConvert.SerializeObject(GenerateToken(userId));
// prepare response
SendSuccess(response, jsonResponse);
}
catch (Exception ex)
{
SendError(response, ex);
}
}
}
+95
View File
@@ -0,0 +1,95 @@
using System.Net;
using System.Security.Cryptography;
using MySql.Data.MySqlClient;
using Newtonsoft.Json.Linq;
namespace TimelogBackend;
public class Register : Route
{
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);
}
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
MySqlTransaction? transaction = null;
try
{
// extract parameters from req body
string body;
using (StreamReader bodyReader = new(request.InputStream, request.ContentEncoding))
{
body = bodyReader.ReadToEnd();
}
JObject jsonObject = JObject.Parse(body);
string f_name = jsonObject["f_name"]?.ToString() ?? "";
string l_name = jsonObject["l_name"]?.ToString() ?? "";
string password = jsonObject["password"]?.ToString() ?? "";
string mail = jsonObject["mail"]?.ToString() ?? "";
// validate parameters
if (
string.IsNullOrEmpty(f_name)
|| f_name.Length > 30
|| f_name.Length < 2
|| string.IsNullOrEmpty(l_name)
|| l_name.Length > 30
|| l_name.Length < 2
|| string.IsNullOrEmpty(mail)
|| mail.Length > 50
|| mail.Length < 6
|| string.IsNullOrEmpty(password)
|| password.Length > 30
|| password.Length < 10
)
{
throw new Exception("Wrong parameters");
}
// TODO Validate dupes of email
string query = "INSERT INTO User(f_name,l_name,mail) VALUES(@f_name,@l_name,@mail)";
MySqlCommand cmd = new(query);
cmd.Parameters.AddWithValue("@f_name", f_name);
cmd.Parameters.AddWithValue("@l_name", l_name);
cmd.Parameters.AddWithValue("@mail", mail);
using MySqlConnection conn = new(connectionString);
conn.Open();
transaction = conn.BeginTransaction();
cmd.Connection = conn;
cmd.ExecuteNonQuery();
// Get user ID
cmd.CommandText = "SELECT id FROM User WHERE mail=@mail;";
MySqlDataReader reader = cmd.ExecuteReader();
reader.Read();
var id = reader["id"];
reader.Close();
// Insert into password
cmd.CommandText = "INSERT INTO Password(user,password) VALUES(@id,@password)";
cmd.Parameters.AddWithValue("@password", HashPassword(password));
cmd.Parameters.AddWithValue("@id", id);
cmd.ExecuteNonQuery();
transaction.Commit();
SendSuccess(response);
}
catch (Exception ex)
{
transaction?.Rollback();
SendError(response, ex);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
using System.Net;
using MySql.Data.MySqlClient;
namespace TimelogBackend;
public class Reset : Route
{
public static void HandleRequest(HttpListenerResponse response)
{
try
{
// prepare SQL query
MySqlCommand cmd = new() { CommandText = "CALL InitDB" };
using (MySqlConnection conn = new MySqlConnection(connectionString))
{
cmd.Connection = conn;
// open connection
conn.Open();
// execute query
cmd.ExecuteNonQuery();
// set up and send response
SendSuccess(response);
}
}
catch (Exception ex)
{
SendError(response, ex);
}
}
}