From f86fda4a6df476d2138977837ff7116d11fdcecc Mon Sep 17 00:00:00 2001 From: nugroho Date: Sat, 10 May 2025 02:24:14 +0700 Subject: [PATCH] Working APIs and Middlewares --- .gitignore | 5 + APIHandler.cs | 75 +++++++++++ Auth.cs | 112 ++++++++++++++++ Commons.cs | 343 +++++++++++++++++++++++++++++++++++++++++++++++ Middlewares.cs | 78 +++++++++++ Partials.cs | 34 +++++ Program.cs | 66 ++++++++- perubahan.csproj | 10 ++ 8 files changed, 721 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 APIHandler.cs create mode 100644 Auth.cs create mode 100644 Commons.cs create mode 100644 Middlewares.cs create mode 100644 Partials.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c879935 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +bin/ +obj/ +settings.json +origin.pfx \ No newline at end of file diff --git a/APIHandler.cs b/APIHandler.cs new file mode 100644 index 0000000..e6a12a4 --- /dev/null +++ b/APIHandler.cs @@ -0,0 +1,75 @@ +using System.Text.Json; + +namespace perubahan; + +public static class APIHandler +{ + public static void Handle(IApplicationBuilder App) + { + App + .Map("/getunits", units =>{ + units.Run(async runner=>{ + if (!await RequestValidated(runner,2)) return; + await WriteJsonResponse(runner,StatusCodes.Status200OK,"Success",Deployments); + }); + }) + .Map("/chunit", unit =>{ + unit.Run(async runner=>{ + if (!await RequestValidated(runner, 2, "POST", true)) return; + if(await TryGetBodyJsonAsync(runner, ["deplid", "unitkerja"], CTS.Token) is Dictionary InElement) + { + Deployment CorrectDeployment = new( + InElement["deplid"].GetInt16(), + InElement["unitkerja"].GetString() ?? "" + ); + if (CorrectDeployment.UnitKerja.Length < 1) + { + await WriteJsonResponse(runner,StatusCodes.Status400BadRequest, "Unit Kerja can't be empty string."); + return; + } + int i = Deployments.FindIndex(depl=>depl.DeplID == CorrectDeployment.DeplID); + if(i<0) + { + await WriteJsonResponse(runner,StatusCodes.Status404NotFound,"Deployment ID not found."); + return; + } + _ = await RunNonQueryAsync(CS,"UPDATE deployment SET unitkerja = @uk WHERE deplid = @id",Comm=>{ + Comm.Parameters.AddWithValue("@id", CorrectDeployment.DeplID); + Comm.Parameters.AddWithValue("@uk", CorrectDeployment.UnitKerja); + },CTS.Token); + Deployments[i] = CorrectDeployment; + await WriteJsonResponse(runner,StatusCodes.Status202Accepted,"Data updated.",Deployments[i]); + } + }); + }) + .Map("/addunit", unit =>{ + unit.Run(async runner=>{ + if (!await RequestValidated(runner, 2, "POST", true)) return; + if(await TryGetBodyJsonAsync(runner, ["unitkerja"], CTS.Token) is Dictionary InElement) + { + string UnitKerja = InElement["unitkerja"].GetString() ?? ""; + if (UnitKerja.Length < 1) + { + await WriteJsonResponse(runner,StatusCodes.Status400BadRequest, "Unit Kerja can't be empty string."); + return; + } + short DeplID = (short)await RunScalarAsync(CS,"INSERT INTO deployment OUTPUT INSERTED.deplid VALUES (@uk)",Comm=>{ + Comm.Parameters.AddWithValue("@uk", UnitKerja); + },CTS.Token); + Deployment Inserted = new(DeplID,UnitKerja); + Deployments.Add(Inserted); + // EventsMarker.CacheUpdates = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("X"); + await WriteJsonResponse(runner,StatusCodes.Status201Created,"Data Created.",Inserted); + } + }); + }) + + .Map("/getagents", agents=>{ + agents.Run(async runner=>{ + if (!await RequestValidated(runner,2)) return; + await WriteJsonResponse(runner,StatusCodes.Status200OK,"Success",Agents); + }); + }) + ; + } +} diff --git a/Auth.cs b/Auth.cs new file mode 100644 index 0000000..c97ac1d --- /dev/null +++ b/Auth.cs @@ -0,0 +1,112 @@ +using System; +using System.Text.Json; + +namespace perubahan; + +public static class Auth +{ + internal static bool TryGetUser(HttpContext context, out SafeUser user) + { + if (context.Items.TryGetValue("AuthorizedUser", out object? userObj) && userObj is SafeUser safeUser) + { + user = safeUser; + return true; + } + + user = null!; + return false; + } + + internal static bool IsAuthorized(HttpContext context, int requiredLevel) + { + return TryGetUser(context, out SafeUser user) && user.Level <= requiredLevel; + } + + internal static Task RejectUnauthorized(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return context.Response.WriteAsJsonAsync(new SimpleApiResponse(401, "Unauthorized."), SGContext.Default.SimpleApiResponse); + } + internal static void Handle(IApplicationBuilder App) + { + App + .Map("/logout", logout =>{ + logout.Run(async runner=>{ + if(!HttpMethods.IsGet(runner.Request.Method) || runner.Request.HasJsonContentType() || runner.Request.HasFormContentType) + { + await WriteJsonResponse(runner,StatusCodes.Status400BadRequest,"Improper request to log out."); + return; + } + runner.Response.Cookies.Append("session", "", Delete); + if(runner.Items.ContainsKey("AuthorizedUser")) + { + await WriteJsonResponse(runner,StatusCodes.Status200OK,"Log out successful. Authorisation token deleted."); + } + else + { + await WriteJsonResponse(runner,StatusCodes.Status200OK,"No user to log out. Authorisation token does not exists."); + } + }); + }) + .Map("/login", login =>{ + login.Run(async runner=>{ + if(runner.Items.ContainsKey("AuthorizedUser")) + { + await WriteJsonResponse(runner,StatusCodes.Status409Conflict,"Log in not allowed when an authorized user is already logged in."); + } + else if(runner.Request.ContentType != "application/json") + { + await WriteJsonResponse(runner,StatusCodes.Status400BadRequest,"Request Content-Type mismatch."); + } + else + { + LoginUser LoginInfo = (await runner.Request.ReadFromJsonAsync(SGContext.Default.LoginUser))!; + if(LoginInfo.Username is null || LoginInfo.Password is null) + { + await WriteJsonResponse(runner,StatusCodes.Status401Unauthorized,"No valid user information provided."); + return; + } + string SHA256Pass = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(LoginInfo.Password))); + if (UserAccounts.TryGetValue(LoginInfo.Username, out User? FoundUser) && FoundUser.Password.Equals(SHA256Pass,StringComparison.InvariantCultureIgnoreCase) && FoundUser.Active) + { + SafeUser LoggedIn = SafeUser.FromUser(FoundUser); + string LoggedInBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(LoggedIn,SGContext.Default.SafeUser))); + string Signature = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(LoggedInBase64+SecretKey))); + runner.Response.Cookies.Append("session", $"{LoggedInBase64}.{Signature}", HttpOnly); + await WriteJsonResponse(runner,StatusCodes.Status200OK,"User authorised. Authorisation token created.",LoggedIn); + } + else + { + await WriteJsonResponse(runner,StatusCodes.Status401Unauthorized,"Provided user information cannot be authorized"); + } + } + }); + }) + .Map("/gate", auth =>{ + auth.Run(async runner=>{ + if(runner.Request.ContentType != "application/json") + { + await WriteJsonResponse(runner,StatusCodes.Status400BadRequest,"Request Content-Type mismatch."); + } + else + { + LoginUser LoginInfo = (await runner.Request.ReadFromJsonAsync(SGContext.Default.LoginUser))!; + if(LoginInfo.Username is null || LoginInfo.Password is null) + { + await WriteJsonResponse(runner,StatusCodes.Status401Unauthorized,"No valid user information provided."); + return; + } + string SHA256Pass = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(LoginInfo.Password))); + if (UserAccounts.TryGetValue(LoginInfo.Username, out User? FoundUser) && FoundUser.Password.Equals(SHA256Pass,StringComparison.InvariantCultureIgnoreCase) && FoundUser.Active) + { + await WriteJsonResponse(runner,StatusCodes.Status200OK,"User authorised. Operation may continue.",SafeUser.FromUser(FoundUser)); + } + else + { + await WriteJsonResponse(runner,StatusCodes.Status401Unauthorized,"Provided user information cannot be authorized"); + } + } + }); + }); + } +} diff --git a/Commons.cs b/Commons.cs new file mode 100644 index 0000000..ca712e2 --- /dev/null +++ b/Commons.cs @@ -0,0 +1,343 @@ +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.Data.SqlClient; +global using System.Text; +global using System.Text.Json.Nodes; +global using static perubahan.Commons; +global using static perubahan.Logging; +global using static perubahan.Middlewares; +global using System.Collections.Concurrent; +global using System.Security.Cryptography; +using System.Text.Json; +using System.Runtime.InteropServices; +namespace perubahan; + +internal static class Commons +{ + internal readonly static string VerNum = "0.1.250509.2301"; + + internal static ConcurrentDictionary UserAccounts = []; + internal static List Deployments = []; + internal static List Agents = []; + internal static readonly string SecretKey = "userandomlaterlikethecommented"; //RandomNumberGenerator.GetHexString(32); + internal static string CS = string.Empty ; + internal static JsonNode Settings = JsonNode.Parse("{}")!; + internal static readonly CookieOptions HttpOnly; + internal static readonly CookieOptions Delete; + // internal static EventStore EventsMarker = new (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("X"),DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("X")); + internal static readonly CancellationTokenSource CTS = new(); + static Commons() + { + Delete = new () + { + Domain = "", + Path = "/", + Expires = DateTime.Parse("2023-06-14") + }; + HttpOnly = new () + { + Domain = "", + Path = "/", + HttpOnly = true, + IsEssential = true + }; + Console.CancelKeyPress += (sender, e) => + { + e.Cancel = true; // Prevents immediate termination + CTS.Cancel(); + }; + AppDomain.CurrentDomain.ProcessExit += (sender, e) => + { + CTS.Cancel(); + }; + } + internal static async Task WriteJsonResponse(HttpContext Context, int Status, string Message, object Data) + { + Context.Response.StatusCode = Status; + await Context.Response.WriteAsJsonAsync(new ApiResponse(Status, Message, Data), SGContext.Default.ApiResponse,cancellationToken: CTS.Token); + } + internal static async Task WriteJsonResponse(HttpContext Context, int Status, string Message) + { + Context.Response.StatusCode = Status; + await Context.Response.WriteAsJsonAsync(new SimpleApiResponse(Status, Message), SGContext.Default.SimpleApiResponse,cancellationToken: CTS.Token); + } + internal static async Task RequestValidated(HttpContext Context, int RequiredLevel = 0, string ValidMethod = "GET", bool CheckJson = false) + { + if (!ValidMethod.Equals(Context.Request.Method,StringComparison.OrdinalIgnoreCase) || + (CheckJson && !Context.Request.HasJsonContentType())) + { + await WriteJsonResponse(Context, StatusCodes.Status405MethodNotAllowed, "Method Not Allowed."); + return false; + } + + if (!Auth.IsAuthorized(Context, RequiredLevel)) + { + await WriteJsonResponse(Context, StatusCodes.Status401Unauthorized, "Unauthorized."); + return false; + } + return true; + } + internal static async Task RunNonQueryAsync(string ConnectionString, string SQL, Action? Configure = null, CancellationToken Token = default) + { + await using SqlConnection Conn = new(ConnectionString); + await Conn.OpenAsync(Token); + await using SqlCommand Cmd = new(SQL, Conn); + Configure?.Invoke(Cmd); + return await Cmd.ExecuteNonQueryAsync(Token); + } + internal static async Task RunReaderAsync(string ConnectionString, string SQL, Action? Configure = null, CancellationToken Token = default) + { + SqlConnection Conn = new(ConnectionString); + await Conn.OpenAsync(Token); + SqlCommand Cmd = new(SQL, Conn); + Configure?.Invoke(Cmd); + return await Cmd.ExecuteReaderAsync(System.Data.CommandBehavior.CloseConnection, Token); + } + internal static async Task RunScalarAsync(string ConnectionString, string SQL, Action? Configure = null, CancellationToken Token = default) + { + await using SqlConnection Conn = new(ConnectionString); + await Conn.OpenAsync(Token); + await using SqlCommand Cmd = new(SQL, Conn); + Configure?.Invoke(Cmd); + return await Cmd.ExecuteScalarAsync(Token); + } + public static async Task RunTransactionAsync(string ConnStr, Func Logic, CancellationToken Token) + { + using SqlConnection Conn = new(ConnStr); + await Conn.OpenAsync(Token); + using SqlTransaction Tran = Conn.BeginTransaction(); + try + { + await Logic(Conn, Tran); + await Tran.CommitAsync(Token); + } + catch (SqlException) + { + + await Tran.RollbackAsync(Token); + // if (ex.State != 255) throw; //state = 255 is custom sql error, designed just for this purpose of not rethrowing. + } + catch + { + throw; + } + } + + internal static string GenerateUuidV7() + { + Span uuidBytes = stackalloc byte[16]; + long time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + RandomNumberGenerator.Fill(uuidBytes[7..]); + if (BitConverter.IsLittleEndian) + { + MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref time, 1))[..6].CopyTo(uuidBytes[..6]); + uuidBytes[..6].Reverse(); + } + else + { + MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref time, 1))[2..8].CopyTo(uuidBytes[..6]); + } + uuidBytes[6] = (byte)((uuidBytes[6] & 0x0F) | 0x70); // Set version to 7 + uuidBytes[8] = (byte)((uuidBytes[8] & 0x3F) | 0x80); // Set variant to 10xx + return Convert.ToHexString(uuidBytes).ToLower().Insert(8, "-").Insert(13, "-").Insert(18, "-").Insert(23, "-"); + } + internal static async Task?> TryGetBodyJsonAsync(HttpContext Context, string[] Keys, CancellationToken Token) + { + using JsonDocument BodyDoc = await JsonDocument.ParseAsync(Context.Request.Body,cancellationToken: Token); + JsonElement Element = BodyDoc.RootElement; + Dictionary? Result = new(StringComparer.OrdinalIgnoreCase); + foreach (string Key in Keys) + { + if (!Element.TryGetProperty(Key, out JsonElement Value)) + { + await WriteJsonResponse(Context, StatusCodes.Status400BadRequest, + "Bad Request. One or more required properties were not received."); + return null; + } + Result[Key] = Value.Clone(); + } + return Result; + } + internal static Dictionary? JsonElementToDict(JsonElement Element, string[] Keys) + { + Dictionary? Result = new(StringComparer.OrdinalIgnoreCase); + foreach (string Key in Keys) + { + if (!Element.TryGetProperty(Key, out JsonElement Value)) + { + return null; + } + Result[Key] = Value.Clone(); + } + return Result; + } + + internal static async Task UpdateCache() + { + Console.WriteLine("Updating app cache."); + Console.Write(" Caching User Accounts... "); + await using SqlConnection FConn = new(CS); + await FConn.OpenAsync().ConfigureAwait(false); + using SqlCommand FComm = new("SELECT * FROM users",FConn); + await using (SqlDataReader URead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + { + while(await URead.ReadAsync(CTS.Token).ConfigureAwait(false)) + { + _ = UserAccounts.TryAdd((string)URead["uname"], new User((string)URead["uname"],(string)URead["name"],(string)URead["pass"],(byte)URead["level"],(bool)URead["active"])); + } + } + FComm.CommandText = "SELECT * FROM deployment"; + await using (SqlDataReader SRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + { + Deployments = await SRead.ToListAsync(r=>new( + (short)r["deplid"], + (string)r["unitkerja"] + ),CTS.Token); + } + FComm.CommandText = "SELECT * FROM agents"; + await using (SqlDataReader SRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + { + Agents = await SRead.ToListAsync(r=>new( + (string)r["agentid"], + (string)r["name"], + (string)r["jabatan"], + (short)r["deplid"], + (string)r["skangkat"], + DateOnly.FromDateTime((DateTime)r["tmt"]), + r["skperubahan"] == DBNull.Value ? null : (string)r["skperubahan"], + r["tgperubahan"] == DBNull.Value ? null : DateOnly.FromDateTime((DateTime)r["tgperubahan"]), + r["vision"] == DBNull.Value ? null : (string)r["vision"], + r["mission"] == DBNull.Value ? null : (string)r["mission"], + r["photourl"] == DBNull.Value ? null : (string)r["photourl"] + ),CTS.Token); + } + // FComm.CommandText = "SELECT shift_sched.*, shifts.name FROM shifts LEFT JOIN shift_sched ON shifts.shiftid = shift_sched.shiftid ORDER BY shifts.shiftid, [day]"; + // await using (SqlDataReader SRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + // { + // ShiftSched = await SRead.ToListAsync(r=>new( + // (byte)r["shiftid"], + // (string)r["name"], + // (byte)r["schedid"], + // (byte)r["day"], + // TimeOnly.FromTimeSpan((TimeSpan)r["start"]), + // TimeOnly.FromTimeSpan((TimeSpan)r["end"]) + // ),CTS.Token); + // } + Console.WriteLine("Done."); + Console.WriteLine("App cache updated."); + return 0; + // FComm.CommandText = "SELECT shift_sched.*, shifts.name FROM shifts LEFT JOIN shift_sched ON shifts.shiftid = shift_sched.shiftid ORDER BY shifts.shiftid, [day]"; + // await using (SqlDataReader SRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + // { + // ShiftSched = await SRead.ToListAsync(r=>new( + // (byte)r["shiftid"], + // (string)r["name"], + // (byte)r["schedid"], + // (byte)r["day"], + // TimeOnly.FromTimeSpan((TimeSpan)r["start"]), + // TimeOnly.FromTimeSpan((TimeSpan)r["end"]) + // ),CTS.Token); + // } + // FComm.CommandText = "SELECT tariffs.*, tariff_sched.schedid, tariff_sched.amount FROM tariffs LEFT JOIN tariff_sched ON tariffs.tariffid = tariff_sched.tariffid ORDER BY tariffs.tariffid"; + // await using (SqlDataReader TRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + // { + // TariffSched = await TRead.ToListAsync(r=>new( + // (byte)r["tariffid"], + // (string)r["name"], + // (bool)r["active"], + // r["schedid"] == DBNull.Value ? null : (byte)r["schedid"], + // r["amount"] == DBNull.Value ? null : (int)r["amount"] + // ),CTS.Token); + // } + // FComm.CommandText = "SELECT * FROM charges"; + // await using (SqlDataReader CRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + // { + // Charges = await CRead.ToListAsync(r=>new( + // (byte)r["chargid"], + // (string)r["name"], + // r["percentage"] == DBNull.Value ? null : (byte)r["percentage"], + // r["amount"] == DBNull.Value ? null : (int)r["amount"] + // ),CTS.Token); + // } + // FComm.CommandText = "SELECT * FROM packview"; + // await using (SqlDataReader PRead = await FComm.ExecuteReaderAsync(CTS.Token).ConfigureAwait(false)) + // { + // Packages = await PRead.ToListAsync(r=>new( + // (byte)r["packid"], + // (string)r["name"], + // (short)r["durinmin"], + // (int)r["price"], + // (byte)r["shiftid"], + // (string)r["shift"], + // (byte)r["schedid"], + // (byte)r["day"], + // (bool)r["active"] + // ), CTS.Token); + // } + // EventsMarker.CacheUpdates = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("X"); + } + + +} +internal static class Logging +{ + private static readonly object Lock = new(); + internal static async void WriteLog(Exception ex, string location = "") + { + await Task.Run(()=>{ + string Time = $"{DateTime.Now:yyy-MM-dd HH:mm:ss}"; + string msg = $"{Time} [{"Error".PadRight(7)[0..7]}] {ex.Message}{(location.Length > 0 ? $" {location}" : "")}{Environment.NewLine}"; + Console.Write(msg); + try + { + byte[] msgbytes = Encoding.UTF8.GetBytes(msg); + lock (Lock) + { + using FileStream LogWriter = new($"{AppContext.BaseDirectory}/{DateTime.Now:yyyyMMdd}.log", FileMode.Append); + LogWriter.Write(msgbytes, 0, msgbytes.Length); + } + } + finally + { + + } + }); + } + internal static async void WriteLog(string exm, string location = "") + { + await Task.Run(()=>{ + string Time = $"{DateTime.Now:yyy-MM-dd HH:mm:ss}"; + string msg = $"{Time} [{"Info".PadRight(7)[0..7]}] {exm}{(location.Length > 0 ? $" {location}" : "")}{Environment.NewLine}"; + Console.Write(msg); + try + { + byte[] msgbytes = Encoding.UTF8.GetBytes(msg); + lock (Lock) + { + using FileStream LogWriter = new($"{AppContext.BaseDirectory}/{DateTime.Now:yyyyMMdd}.log", FileMode.Append); + LogWriter.Write(msgbytes, 0, msgbytes.Length); + } + } + finally + { + + } + }); + } +} +internal static class DataReaderExtensions +{ + internal static async Task> ToListAsync( + this SqlDataReader reader, + Func map, + CancellationToken cancellationToken = default) + { + var list = new List(); + while (await reader.ReadAsync(cancellationToken)) + { + list.Add(map(reader)); + } + return list; + } +} \ No newline at end of file diff --git a/Middlewares.cs b/Middlewares.cs new file mode 100644 index 0000000..3e7ef24 --- /dev/null +++ b/Middlewares.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.Extensions.Hosting; +namespace perubahan; + +public class Middlewares +{ + public class AuthCheck(RequestDelegate next) + { + public async Task Invoke(HttpContext runner) + { + if(runner.Request.Cookies.ContainsKey("session")) + { + string Token = runner.Request.Cookies["session"] ?? "."; + string LoggedInBase64 = Token.Split(".")[0]; + string Signature = Token.Split(".")[1]; + if (Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(LoggedInBase64+SecretKey))).Equals(Signature,StringComparison.InvariantCultureIgnoreCase)) + { + string Username = (string?)JsonNode.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(LoggedInBase64)))?["username"] ?? ""; + SafeUser LoggedIn = SafeUser.FromUser(UserAccounts[Username]); + if (LoggedIn.Active) + { + runner.Items.Add("AuthorizedUser",LoggedIn); + } + else + { + runner.Response.Cookies.Append("session", "", Delete); + } + } + else + { + runner.Response.Cookies.Append("session", "", Delete); + } + } + await next(runner); + } + } + public class DomainNormalize(RequestDelegate next) + { + + public async Task Invoke(HttpContext runner) + { + HttpOnly.Domain = runner.Request.Host.ToString(); + Delete.Domain = runner.Request.Host.ToString(); + await next(runner); + } + } + public class ErrorHandling(RequestDelegate next) + { + public async Task Invoke(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + throw; + } + } + + private static Task HandleExceptionAsync(HttpContext context, Exception ex) + { + WriteLog(ex, $"at {Path.Combine(context.Request.PathBase, context.Request.Path)}"); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + return context.Response.WriteAsJsonAsync(new SimpleApiResponse(context.Response.StatusCode, "Internal Server Error."), SGContext.Default.SimpleApiResponse); + } + } + public class CancellationTokenSourceLinker(RequestDelegate next, IHostApplicationLifetime Lifetime) + { + public async Task Invoke(HttpContext context) + { + CancellationTokenSource LinkedToken = CancellationTokenSource.CreateLinkedTokenSource(Lifetime.ApplicationStopping,context.RequestAborted,CTS.Token); + context.RequestAborted = LinkedToken.Token; + await next(context); + } + } +} diff --git a/Partials.cs b/Partials.cs new file mode 100644 index 0000000..c9dd5c7 --- /dev/null +++ b/Partials.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +internal partial record Agent(string AgentID, string Name, string Jabatan, short DeplID, string SKAngkat, DateOnly TMT, string? SKPerb, DateOnly? TMUbah, string? Vision, string? Mission, string? PhotoURL); +internal partial record ApiResponse(int Status, string Message, object Data); +internal partial record Deployment(short DeplID, string UnitKerja); +internal partial record LoginUser(string Username, string Password); +internal partial record PasswdUser(string Username, string PlainPassword); +internal partial record SafeUser(string Username, string Name, byte Level, bool Active){ + internal static SafeUser FromUser(User Source) + { + return new(Source.Username, Source.Name, Source.Level, Source.Active); + } +}; +internal partial record SimpleApiResponse(int Status, string Message); +internal partial record User(string Username, string Name, string Password, byte Level, bool Active); +[JsonSerializable(typeof(Agent))] +[JsonSerializable(typeof(ApiResponse))] +[JsonSerializable(typeof(Deployment))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(JsonElement[]))] +[JsonSerializable(typeof(LoginUser))] +[JsonSerializable(typeof(PasswdUser))] +[JsonSerializable(typeof(SafeUser))] +[JsonSerializable(typeof(SimpleApiResponse))] +[JsonSerializable(typeof(User))] +//////////----------DICTIONARIES----------////////// +[JsonSerializable(typeof(Dictionary))] +//////////-------------LISTS--------------////////// +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSourceGenerationOptions(GenerationMode =JsonSourceGenerationMode.Default, PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +internal partial class SGContext : JsonSerializerContext{} diff --git a/Program.cs b/Program.cs index 83fa4f4..ffe1716 100644 --- a/Program.cs +++ b/Program.cs @@ -1,2 +1,64 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using perubahan; +using Microsoft.Extensions.DependencyInjection; +try +{ + Console.WriteLine($"Kementerian ATR/BPN Kanwil Riau"); + Console.WriteLine($"Agen Perubahan Server Versi {VerNum}"); + Console.Write($"Loading settings file..."); + if (!File.Exists(Path.Combine(AppContext.BaseDirectory, "settings.json"))) + { + Console.WriteLine("No settings file found. Make sure settings.json exists at BaseDirectory."); + WriteLog("No default settings file found. Make sure settings.json exists at BaseDirectory."); + return; + } + string SettingsJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "settings.json")); + Settings = JsonNode.Parse(SettingsJson) ?? JsonNode.Parse("{}")!; + Console.WriteLine($"Done."); + Console.Write($"Preparing database connection string... "); + SqlConnectionStringBuilder CSBuilder = new () + { + DataSource = (string)Settings["database"]!["datasource"]! ?? "", + UserID = (string)Settings["database"]!["user"]! ?? "", + Password = (string)Settings["database"]!["pass"]! ?? "", + InitialCatalog = (string)Settings["database"]!["initialcatalog"]! ?? "", + Encrypt = SqlConnectionEncryptOption.Mandatory, + TrustServerCertificate = true + }; + CS = CSBuilder.ConnectionString; + Console.WriteLine($"Done."); + _ = await UpdateCache(); + Console.Write($"Configuring Kestrel Backend... "); + IWebHost host = new WebHostBuilder() + .UseKestrel(options => { + options.ListenAnyIP(443, opt =>{ + opt.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; + opt.UseHttps(Path.Combine(AppContext.BaseDirectory,"origin.pfx")); + }); + options.Limits.MaxRequestBodySize = 104857600; + }) + .UseContentRoot(AppContext.BaseDirectory) + // .ConfigureServices(r=>{ + // r.AddResponseCompression(o=>{ + // o.EnableForHttps = true; + // o.Providers.Add(); + // o.Providers.Add(); + // }); + // }) + .Configure(app=>{ + app + .UseMiddleware() + .UseMiddleware() + .UseMiddleware() + .UseMiddleware() + .UseDefaultFiles() + .Map("/api",APIHandler.Handle) + .Map("/auth",Auth.Handle) + .UseStaticFiles(); + }) + .Build(); + host.Run(); +} +catch (Exception ex) +{ + WriteLog(ex, "Program.cs"); +} \ No newline at end of file diff --git a/perubahan.csproj b/perubahan.csproj index 206b89a..8c32e67 100644 --- a/perubahan.csproj +++ b/perubahan.csproj @@ -5,6 +5,16 @@ net8.0 enable enable + true + + + + + + + + +