diff --git a/csharp/App/Backend/Controllers/Controller.cs b/csharp/App/Backend/Controllers/Controller.cs index 9136ac151..2351d5e92 100644 --- a/csharp/App/Backend/Controllers/Controller.cs +++ b/csharp/App/Backend/Controllers/Controller.cs @@ -4,7 +4,6 @@ using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Model; using InnovEnergy.App.Backend.Model.Relations; using InnovEnergy.App.Backend.Utils; -using InnovEnergy.Lib.Utils; using Microsoft.AspNetCore.Mvc; using HttpContextAccessor = Microsoft.AspNetCore.Http.HttpContextAccessor; @@ -58,18 +57,26 @@ public class Controller [Returns(HttpStatusCode.OK)] [Returns(HttpStatusCode.Unauthorized)] - [HttpPost($"{nameof(UpdateS3Credentials)}")] - public Object UpdateS3Credentials() + [HttpGet($"{nameof(GetInstallationS3Key)}")] + public Object GetInstallationS3Key(Int64 installationId) { - // TODO: S3Credentials should be per session, not per user - var caller = GetCaller(); if (caller is null) return new HttpResponseMessage(HttpStatusCode.Unauthorized); using var db = Db.Connect(); + + var installation = db + .GetAllAccessibleInstallations(caller) + .FirstOrDefault(i => i.Id == installationId); - return db.CreateAndSaveUserS3ApiKey(caller); + if(installation == null) + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized); + } + + var key = db.GetInstallationS3Key(installationId); + return key ?? db.CreateAndSaveInstallationS3ApiKey(installation); } @@ -219,24 +226,64 @@ public class Controller return folder; } + + [Returns(HttpStatusCode.OK)] + [Returns(HttpStatusCode.Unauthorized)] + [HttpPost($"{nameof(CreateUser)}/")] + public Object CreateUser(User newUser) + { + var caller = GetCaller(); + using var db = Db.Connect(); + if (caller == null || !caller.HasWriteAccess) + return new HttpResponseMessage(HttpStatusCode.Unauthorized); + newUser.ParentId = caller.Id; + + return db.CreateUser(newUser); + } + + [Returns(HttpStatusCode.OK)] + [Returns(HttpStatusCode.Unauthorized)] + [HttpPost($"{nameof(CreateInstallation)}/")] + public Object CreateInstallation(Installation installation) + { + var caller = GetCaller(); + using var db = Db.Connect(); + if (caller == null || !caller.HasWriteAccess) + return new HttpResponseMessage(HttpStatusCode.Unauthorized); + + var id = db.CreateInstallation(installation); + + return db.AddToAccessibleInstallations(caller.Id, id); + + } + + [Returns(HttpStatusCode.OK)] + [Returns(HttpStatusCode.Unauthorized)] + [HttpPost($"{nameof(CreateFolder)}/")] + public Object CreateFolder(Folder folder) + { + var caller = GetCaller(); + using var db = Db.Connect(); + if (caller == null || !caller.HasWriteAccess) + return new HttpResponseMessage(HttpStatusCode.Unauthorized); + + var id = db.CreateFolder(folder); + return db.AddToAccessibleFolders(caller.Id, id); + + } [Returns(HttpStatusCode.OK)] [Returns(HttpStatusCode.Unauthorized)] [HttpPut($"{nameof(UpdateUser)}/")] public Object UpdateUser(User updatedUser) { - // TODO: distinguish between create and update - var caller = GetCaller(); - if (caller == null) + using var db = Db.Connect(); + if (caller == null || !db.IsParentOfChild(caller.Id, updatedUser) || !caller.HasWriteAccess) return new HttpResponseMessage(HttpStatusCode.Unauthorized); - using var db = Db.Connect(); - - return db.GetUserById(updatedUser.Id) != null - ? db.UpdateUser(updatedUser) - : db.CreateUser(updatedUser); + return db.UpdateUser(updatedUser); } @@ -252,15 +299,21 @@ public class Controller using var db = Db.Connect(); - var hasAccessToInstallation = db + var installationFromAccessibleInstallations = db .GetAllAccessibleInstallations(caller) - .Any(i => i.Id == installation.Id); + .FirstOrDefault(i => i.Id == installation.Id); - if (!hasAccessToInstallation) + if (installationFromAccessibleInstallations == null) return new HttpResponseMessage(HttpStatusCode.Unauthorized); // TODO: accessibility by other users etc // TODO: sanity check changes + // foreach(var property in installationFromAccessibleInstallations.GetType().GetProperties()){ + // if(installation.GetType().GetProperties().Contains(property)) + // { + // property.SetValue(installationFromAccessibleInstallations, property.GetValue(installation)); + // } + // } return db.UpdateInstallation(installation); } @@ -278,17 +331,24 @@ public class Controller using var db = Db.Connect(); - var hasAccessToFolder = db + var installationFromAccessibleFolders = db .GetAllAccessibleFolders(caller) - .Any(f => f.Id == folder.Id); + .FirstOrDefault(f => f.Id == folder.Id); - if (!hasAccessToFolder) + if (installationFromAccessibleFolders == null) return new HttpResponseMessage(HttpStatusCode.Unauthorized); // TODO: accessibility by other users etc // TODO: sanity check changes - - return db.UpdateFolder(folder); + + // foreach(var property in installationFromAccessibleFolders.GetType().GetProperties()){ + // if(folder.GetType().GetProperties().Contains(property)) + // { + // property.SetValue(installationFromAccessibleFolders, property.GetValue(folder)); + // } + // } + + return db.UpdateFolder(installationFromAccessibleFolders); } [Returns(HttpStatusCode.OK)] diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 5c87cc9c8..3e673e05c 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -10,16 +10,16 @@ namespace InnovEnergy.App.Backend.Database; public partial class Db : IDisposable { internal const String DbPath = "./db.sqlite"; - + private readonly SQLiteConnection _Db; // internal handle to the connection, disposable - + private TableQuery Sessions => _Db.Table(); - + [SuppressMessage("ReSharper", "AccessToDisposedClosure")] static Db() { // on startup create/migrate tables - + using var db = new SQLiteConnection(DbPath); db.RunInTransaction(() => @@ -31,105 +31,115 @@ public partial class Db : IDisposable db.CreateTable(); db.CreateTable(); }); - } // private, force access through Connect() private Db() => _Db = new SQLiteConnection(DbPath); - + public static Db Connect() => new Db(); public void Dispose() => _Db.Dispose(); - - + + // the C in CRUD - private Result Create(TreeNode treeNode) + private Int64 Create(TreeNode treeNode) { try { _Db.Insert(treeNode); + return SQLite3.LastInsertRowid(_Db.Handle); } catch (Exception e) { - return Result.Error(e); + return -1; } + } + + + private Boolean Create(Session session) + { + try + { + _Db.Insert(session); + return true; + } + catch (Exception e) + { + return false; + } + } - return Result.Ok; - } - // the U in CRUD - private Result Update(TreeNode treeNode) + private Boolean Update(TreeNode treeNode) { try { _Db.InsertOrReplace(treeNode); + return true; } catch (Exception e) { - return Result.Error(e); + return false; } - - return Result.Ok; } - + // the D in CRUD - private Result Delete(TreeNode treeNode) + private Boolean Delete(TreeNode treeNode) { try { _Db.Delete(treeNode); + return true; } catch (Exception e) { - return Result.Error(e); + return false; } - - return Result.Ok; } - + public IEnumerable GetAllAccessibleInstallations(User user) { - var direct = GetDirectlyAccessibleInstallations(user); + var direct = GetDirectlyAccessibleInstallations(user); var fromFolders = GetAllAccessibleFolders(user) - .SelectMany(GetChildInstallations); + .SelectMany(GetChildInstallations); return direct - .Concat(fromFolders) - .Distinct(); + .Concat(fromFolders) + .Distinct(); } - + public IEnumerable GetAllAccessibleFolders(User user) { return GetDirectlyAccessibleFolders(user) - .SelectMany(GetDescendantFolders) - .Distinct(); - + .SelectMany(GetDescendantFolders) + .Distinct(); + // Distinct because the user might have direct access // to a child folder of a folder he has already access to } - - + + public IEnumerable GetDirectlyAccessibleInstallations(User user) { return User2Installation - .Where(r => r.UserId == user.Id) - .Select(r => r.InstallationId) - .Select(GetInstallationById) - .NotNull() - .Do(i => i.ParentId = 0); // hide inaccessible parents from calling user + .Where(r => r.UserId == user.Id) + .Select(r => r.InstallationId) + .Select(GetInstallationById) + .NotNull() + .Do(i => i.ParentId = 0); // hide inaccessible parents from calling user } - + public IEnumerable GetDirectlyAccessibleFolders(User user) { return User2Folder - .Where(r => r.UserId == user.Id) - .Select(r => r.FolderId) - .Select(GetFolderById) - .NotNull() - .Do(i => i.ParentId = 0); // hide inaccessible parents from calling user; + .Where(r => r.UserId == user.Id) + .Select(r => r.FolderId) + .Select(GetFolderById) + .NotNull() + .Do(i => i.ParentId = 0); // hide inaccessible parents from calling user; } - - public Result AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId) + + public Boolean AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId) { var con = new User2Installation { @@ -139,17 +149,16 @@ public partial class Db : IDisposable try { - _Db.InsertOrReplace(con); + _Db.Insert(con); + return true; } catch (Exception e) { - return Result.Error(e); + return false; } - - return Result.Ok; } - - public Result AddToAccessibleFolders(Int64 userId, Int64 updatedFolderId) + + public Boolean AddToAccessibleFolders(Int64 userId, Int64 updatedFolderId) { var con = new User2Folder { @@ -159,16 +168,14 @@ public partial class Db : IDisposable try { - _Db.InsertOrReplace(con); + _Db.Insert(con); + return true; } catch (Exception e) { - return Result.Error(e); + return false; } - - return Result.Ok; } - public User? GetUserByToken(String token) @@ -182,33 +189,33 @@ public partial class Db : IDisposable } - public Result NewSession(Session ses) - { - try - { - _Db.InsertOrReplace(ses); - } - catch (Exception e) - { - return Result.Error(e); - } + public Boolean NewSession(Session ses) => Create(ses); - return Result.Ok; - } - - public Result DeleteSession(Int64 id) + public Boolean DeleteSession(Int64 id) { try { Sessions.Delete(u => u.UserId == id); + return true; } catch (Exception e) { - return Result.Error(e); + return false; } - - return Result.Ok; } - -} + public Object? GetInstallationS3Key(Int64 installationId) + { + return Installations + .FirstOrDefault(installation => installation.Id == installationId).S3Key; + } + + public void DeleteS3KeysDaily() + { + foreach (var installation in Installations.ToList()) + { + installation.S3Key = null; + Update(installation); + } + } +} \ No newline at end of file diff --git a/csharp/App/Backend/Database/DbConnection.cs b/csharp/App/Backend/Database/DbConnection.cs new file mode 100644 index 000000000..4676ca498 --- /dev/null +++ b/csharp/App/Backend/Database/DbConnection.cs @@ -0,0 +1,18 @@ +using InnovEnergy.App.Backend.Model; +using SQLite; + +namespace InnovEnergy.App.Backend.Database; + + +// TODO ? +public struct DbConnection +{ + public DbConnection(SQLiteConnection connection, User caller) + { + Connection = connection; + Caller = caller; + } + + public SQLiteConnection Connection { get;} + public User Caller { get;} +} \ No newline at end of file diff --git a/csharp/App/Backend/Database/Folder.cs b/csharp/App/Backend/Database/Folder.cs index 8a4785639..1204ed06c 100644 --- a/csharp/App/Backend/Database/Folder.cs +++ b/csharp/App/Backend/Database/Folder.cs @@ -26,7 +26,6 @@ public partial class Db return Folders.Where(f => f.ParentId == parent.Id); } - public IEnumerable GetDescendantFolders(Folder parent) { return parent.Traverse(GetChildFolders); @@ -47,31 +46,31 @@ public partial class Db return parent.Traverse(GetChildUsers); } - public Result CreateFolder(Folder folder) + public Int64 CreateFolder(Folder folder) { return Create(folder); } - public Result UpdateFolder(Folder folder) + public Boolean UpdateFolder(Folder folder) { // TODO: no circles in path return Update(folder); } - public Result ChangeParent(Installation child, Int64 parentId) + public Boolean ChangeParent(Installation child, Int64 parentId) { child.ParentId = parentId; return UpdateInstallation(child); } - public Result ChangeParent(Folder child, Int64 parentId) + public Boolean ChangeParent(Folder child, Int64 parentId) { child.ParentId = parentId; return UpdateFolder(child); } - public Result DeleteFolder(Folder folder) + public Boolean DeleteFolder(Folder folder) { // Delete direct children User2Folder .Delete(f => f.FolderId == folder.Id); diff --git a/csharp/App/Backend/Database/Installation.cs b/csharp/App/Backend/Database/Installation.cs index 6e554f483..de8aae85b 100644 --- a/csharp/App/Backend/Database/Installation.cs +++ b/csharp/App/Backend/Database/Installation.cs @@ -14,22 +14,24 @@ public partial class Db public Installation? GetInstallationById(Int64 id) => Installations .FirstOrDefault(u => u.Id == id); - public Result CreateInstallation(Installation installation) + public Int64 CreateInstallation(Installation installation) { return Create(installation); } - public Result UpdateInstallation(Installation installation) + public Boolean UpdateInstallation(Installation installation) { return Update(installation); } - public Result DeleteInstallation(Installation installation) + public Boolean DeleteInstallation(Installation installation) { User2Installation.Delete(i => i.InstallationId == installation.Id); return Delete(installation); } + + } diff --git a/csharp/App/Backend/Database/User.cs b/csharp/App/Backend/Database/User.cs index 0719ab9fe..980786076 100644 --- a/csharp/App/Backend/Database/User.cs +++ b/csharp/App/Backend/Database/User.cs @@ -1,9 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Net.Mail; using System.Security.Cryptography; using System.Text; using System.Text.Json.Nodes; -using Flurl.Http; +using System.Text.RegularExpressions; using InnovEnergy.App.Backend.Model; using InnovEnergy.App.Backend.Utils; using InnovEnergy.Lib.Utils; @@ -25,27 +25,35 @@ public partial class Db return Users.FirstOrDefault(u => u.Id == id); } - public Boolean IsParentOfChild(User parent, User child) + public Boolean IsParentOfChild(Int64 parentId, User child) { - var parentPointer = child.ParentId; + return Ancestors(child) + .Any(u => u.Id == parentId); + } - if (parent.Id == child.Id) - return true; + private IEnumerable Ancestors(User child) + { + return child.Unfold(GetParent); + } - while (parentPointer != null && parentPointer != parent.Id) - { - parentPointer = GetUserById(parentPointer).ParentId; - } + public User? GetParent(User u) + { + return IsRoot(u) + ? null + : GetUserById(u.ParentId); + } - return parentPointer == parent.Id; + public static Boolean IsRoot(User u) + { + return u.ParentId == 0; // root has ParentId 0 by definition } public User? GetUserByEmail(String email) => Users.FirstOrDefault(u => u.Email == email); - public Result CreateUser(User user) + public Int64 CreateUser(User user) { if (GetUserByEmail(user.Email) is not null) - return Result.Error("User with that email already exists"); + return -1; // TODO: User with that email already exists //Salting and Hashing password var salt = Crypto.GenerateSalt(); @@ -58,73 +66,149 @@ public partial class Db return Create(user); } - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public Object CreateAndSaveUserS3ApiKey(User user) + + private static Byte[] HmacSha256Digest(String message, String secret) + { + var encoding = new UTF8Encoding(); + var keyBytes = encoding.GetBytes(secret); + var messageBytes = encoding.GetBytes(message); + var cryptographer = new HMACSHA256(keyBytes); + + var bytes = cryptographer.ComputeHash(messageBytes); + return bytes; + } + + private static String BuildSignature(String method, String path, String data, Int64 time, String secret) + { + var messageToSign = ""; + messageToSign += method + " /v2/" + path + "\n"; + messageToSign += data + "\n"; + + // query strings + messageToSign += "\n"; + // headers + messageToSign += "\n"; + + messageToSign += time; + + Console.WriteLine("Message to sign:\n" + messageToSign); + + + var hmac = HmacSha256Digest(messageToSign, secret); + return Convert.ToBase64String(hmac); + } + + // public Object CreateAndSaveUserS3ApiKey(User user) + // { + // //EXOSCALE API URL + // const String url = "https://api-ch-dk-2.exoscale.com/v2/"; + // const String path = "access-key"; + // + // //TODO HIDE ME + // const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0"; + // const String apiKey = "EXOb98ec9008e3ec16e19d7b593"; + // + // var installationList = User2Installation + // .Where(i => i.UserId == user.Id) + // .SelectMany(i => Installations.Where(f => i.InstallationId == f.Id)) + // .ToList(); + // + // + // var instList = new JsonArray(); + // + // foreach (var installation in installationList) + // { + // instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"}); + // } + // + // var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList}; + // var stringPayload = jsonPayload.ToJsonString(); + // + // var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60; + // var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret); + // + // var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature; + // + // var client = new HttpClient(); + // client.DefaultRequestHeaders.Authorization = + // new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader); + // + // var content = new StringContent(stringPayload, Encoding.UTF8, "application/json"); + // + // + // var response = client.PostAsync(url+path, content).Result; + // + // if (response.StatusCode.ToString() != "OK") + // { + // return response; + // } + // + // var responseString = response.Content.ReadAsStringAsync().Result; + // return Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"')); + // // return SetUserS3ApiKey(user, newKey); + // + // } + + public Object? CreateAndSaveInstallationS3ApiKey(Installation installation) { //EXOSCALE API URL - const String url = "https://api-ch-dk-2.exoscale.com/v2/access-key"; + const String url = "https://api-ch-dk-2.exoscale.com/v2/"; + const String path = "access-key"; + + //TODO HIDE ME const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0"; const String apiKey = "EXOb98ec9008e3ec16e19d7b593"; - var installationList = User2Installation - .Where(i => i.UserId == user.Id) - .SelectMany(i => Installations.Where(f => i.InstallationId == f.Id)) - .ToList(); - - var instList = new JsonArray(); + instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"}); + + var jsonPayload = new JsonObject { ["name"] = installation.Id, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList}; + var stringPayload = jsonPayload.ToJsonString(); - foreach (var installation in installationList) + var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60; + var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret); + + var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature; + + var client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader); + + var content = new StringContent(stringPayload, Encoding.UTF8, "application/json"); + + + var response = client.PostAsync(url+path, content).Result; + + if (response.StatusCode.ToString() != "OK") { - instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"}); + return response; } - - var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "getObject", "listBucket" }, ["content"] = instList}; - var expiration = DateTime.Now.AddSeconds(60); - - var signature = $"POST /v2/access-key\n{jsonPayload}\n\n\n{((DateTimeOffset)expiration).ToUnixTimeSeconds()}"; - using var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); - - signature = Encoding.UTF8 - .GetBytes(signature) - .Apply(hmacSha256.ComputeHash) - .Apply(Convert.ToBase64String); - var keyJson = url - .WithHeader("Authorization", - $"EXO2-HMAC-SHA256 credential={apiKey},expires={((DateTimeOffset)expiration).ToUnixTimeSeconds()},signature={signature}"); - - - var result = keyJson.PostJsonAsync(jsonPayload.ToString()) - .ReceiveJson() - .Result; - return result; - // return SetUserS3ApiKey(user, keyJson.GetValue("key")); + var responseString = response.Content.ReadAsStringAsync().Result; + var newKey = Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"')); + + installation.S3Key = newKey; + UpdateInstallation(installation); + return newKey; } - - public Result SetUserS3ApiKey(User user, String key) - { - user.S3Key = key; - return Update(user); - } - - public Result UpdateUser(User user) + + public Boolean UpdateUser(User user) { var oldUser = GetUserById(user.Id); if (oldUser == null) - return Result.Error("User doesn't exist"); + return false; // TODO: "User doesn't exist" //Checking for unchangeable things // TODO: depends on privileges of caller - user.Id = oldUser.Id; + user.Id = oldUser.Id; user.ParentId = oldUser.ParentId; - user.Email = oldUser.Email; + user.Email = oldUser.Email; return Update(user); } - public Result DeleteUser(User user) + public Boolean DeleteUser(User user) { User2Folder.Delete(u => u.UserId == user.Id); User2Installation.Delete(u => u.UserId == user.Id); diff --git a/csharp/App/Backend/Model/Installation.cs b/csharp/App/Backend/Model/Installation.cs index b9858e9c8..b6c4b7968 100644 --- a/csharp/App/Backend/Model/Installation.cs +++ b/csharp/App/Backend/Model/Installation.cs @@ -14,6 +14,7 @@ public class Installation : TreeNode public Double Long { get; set; } public String S3Bucket { get; set; } = ""; + public String? S3Key { get; set; } } diff --git a/csharp/App/Backend/Model/User.cs b/csharp/App/Backend/Model/User.cs index 762c819a4..a6fd45c61 100644 --- a/csharp/App/Backend/Model/User.cs +++ b/csharp/App/Backend/Model/User.cs @@ -5,11 +5,10 @@ namespace InnovEnergy.App.Backend.Model; public class User : TreeNode { [Indexed] - public String Email { get; set; } = ""; - public Boolean HasWriteAccess { get; set; } - public String S3Key { get; set; } - public String Salt { get; set; } - public String Password { get; set; } + public String Email { get; set; } = null!; + public Boolean HasWriteAccess { get; set; } = false; + public String Salt { get; set; } = null!; + public String Password { get; set; } = null!; // TODO: must reset pwd } diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index ae662b779..7b6df34b9 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -1,3 +1,4 @@ +using System.Reactive.Linq; using InnovEnergy.App.Backend.Database; using Microsoft.OpenApi.Models; @@ -10,11 +11,13 @@ public static class Program using (var db = Db.Connect()) db.CreateFakeRelations(); + Observable.Interval(TimeSpan.FromDays(1)).Subscribe((_) => deleteInstallationS3KeysDaily()); + var builder = WebApplication.CreateBuilder(args); - + builder.Services.AddControllers(); // TODO: remove magic, specify controllers explicitly // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - + builder.Services.AddHttpContextAccessor(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.WithOrigins("*").AllowAnyHeader().AllowAnyMethod())); @@ -34,7 +37,7 @@ public static class Program app.UseSwagger(); app.UseSwaggerUI(cfg => cfg.EnableFilter()); } - + app.UseCors(); app.UseHttpsRedirection(); app.UseAuthorization(); @@ -44,6 +47,13 @@ public static class Program app.Run(); } + private static void deleteInstallationS3KeysDaily() + { + using var db = Db.Connect(); + db.DeleteS3KeysDaily(); + + } + private static async Task SetSessionUser(HttpContext ctx, RequestDelegate next) { var headers = ctx.Request.Headers; diff --git a/csharp/App/Backend/Utils/Result.cs b/csharp/App/Backend/Utils/Result.cs deleted file mode 100644 index 979e9184e..000000000 --- a/csharp/App/Backend/Utils/Result.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace InnovEnergy.App.Backend.Utils; - -public class Result -{ - private const String OkMsg = "Ok"; - - private readonly String _Error; - - public static Result Ok = new Result(OkMsg); - - public Boolean IsError => _Error != OkMsg; - public Boolean IsSuccess => _Error == OkMsg; - - private Result(String error) => _Error = error; - - public static Result Error(String error) => new Result(error); - - public static Result Error(Exception exception) - { - - #if DEBUG - var msg = exception.ToString(); // includes stacktrace - #else - var msg = exception.Message; // excludes stacktrace - #endif - - return new Result(msg); - } -} \ No newline at end of file diff --git a/csharp/App/Backend/db.sqlite b/csharp/App/Backend/db.sqlite index 92600a486..6b0d2d492 100644 Binary files a/csharp/App/Backend/db.sqlite and b/csharp/App/Backend/db.sqlite differ diff --git a/csharp/App/EmuMeterDriver/Config.cs b/csharp/App/EmuMeterDriver/Config.cs index ee0ad64db..0b5182735 100644 --- a/csharp/App/EmuMeterDriver/Config.cs +++ b/csharp/App/EmuMeterDriver/Config.cs @@ -20,32 +20,32 @@ public static class Config public static readonly IReadOnlyList Signals = new Signal[] { - new(s => s.CurrentL1, "/Ac/L1/Current", "0.0 A"), - new(s => s.CurrentL2, "/Ac/L2/Current", "0.0 A"), - new(s => s.CurrentL3, "/Ac/L3/Current", "0.0 A"), - new(s => s.CurrentL123, "/Ac/Current", "0.0 A"), + new(s => s.Ac.L1.Current, "/Ac/L1/Current", "0.0 A"), + new(s => s.Ac.L2.Current, "/Ac/L2/Current", "0.0 A"), + new(s => s.Ac.L3.Current, "/Ac/L3/Current", "0.0 A"), + new(s => s.Ac.L1.Current + s.Ac.L2.Current + s.Ac.L3.Current, "/Ac/Current", "0.0 A"), - new(s => s.VoltageL1N, "/Ac/L1/Voltage", "0.0 A"), - new(s => s.VoltageL2N, "/Ac/L2/Voltage", "0.0 A"), - new(s => s.VoltageL3N, "/Ac/L3/Voltage", "0.0 A"), - new(s => (s.VoltageL1N + s.VoltageL2N + s.VoltageL3N) / 3.0m, "/Ac/Voltage", "0.0 A"), + new(s => s.Ac.L1.Voltage, "/Ac/L1/Voltage", "0.0 A"), + new(s => s.Ac.L2.Voltage, "/Ac/L2/Voltage", "0.0 A"), + new(s => s.Ac.L3.Voltage, "/Ac/L3/Voltage", "0.0 A"), + new(s => (s.Ac.L1.Voltage + s.Ac.L2.Voltage + s.Ac.L3.Voltage) / 3.0m, "/Ac/Voltage", "0.0 A"), - new(s => s.ActivePowerL1, "/Ac/L1/Power", "0 W"), - new(s => s.ActivePowerL2, "/Ac/L2/Power", "0 W"), - new(s => s.ActivePowerL3, "/Ac/L3/Power", "0 W"), - new(s => s.ActivePowerL123, "/Ac/Power", "0 W"), + new(s => s.Ac.L1.ActivePower, "/Ac/L1/Power", "0 W"), + new(s => s.Ac.L2.ActivePower, "/Ac/L2/Power", "0 W"), + new(s => s.Ac.L3.ActivePower, "/Ac/L3/Power", "0 W"), + new(s => s.Ac.ActivePower, "/Ac/Power", "0 W"), - new(s => s.EnergyImportL123, "Ac/Energy/Forward", "0.00 kWh"), - new(s => s.EnergyExportL123, "Ac/Energy/Reverse", "0.00 kWh"), - - new(s => s.EnergyImportL1, "Ac/L1/Energy/Forward", "0.00 kWh"), - new(s => s.EnergyExportL1, "Ac/L1/Energy/Reverse", "0.00 kWh"), - - new(s => s.EnergyImportL2, "Ac/L2/Energy/Forward", "0.00 kWh"), - new(s => s.EnergyExportL2, "Ac/L2/Energy/Reverse", "0.00 kWh"), - - new(s => s.EnergyImportL3, "Ac/L3/Energy/Forward", "0.00 kWh"), - new(s => s.EnergyExportL3, "Ac/L3/Energy/Reverse", "0.00 kWh"), + // new(s => s.EnergyImportL123, "Ac/Energy/Forward", "0.00 kWh"), + // new(s => s.EnergyExportL123, "Ac/Energy/Reverse", "0.00 kWh"), + // + // new(s => s.EnergyImportL1, "Ac/L1/Energy/Forward", "0.00 kWh"), + // new(s => s.EnergyExportL1, "Ac/L1/Energy/Reverse", "0.00 kWh"), + // + // new(s => s.EnergyImportL2, "Ac/L2/Energy/Forward", "0.00 kWh"), + // new(s => s.EnergyExportL2, "Ac/L2/Energy/Reverse", "0.00 kWh"), + // + // new(s => s.EnergyImportL3, "Ac/L3/Energy/Forward", "0.00 kWh"), + // new(s => s.EnergyExportL3, "Ac/L3/Energy/Reverse", "0.00 kWh"), }; public static VeProperties DefaultProperties => new VeProperties diff --git a/csharp/Lib/Devices/AMPT/AmptCommunicationUnit.cs b/csharp/Lib/Devices/AMPT/AmptCommunicationUnit.cs index e701c8463..6491f4e8b 100644 --- a/csharp/Lib/Devices/AMPT/AmptCommunicationUnit.cs +++ b/csharp/Lib/Devices/AMPT/AmptCommunicationUnit.cs @@ -1,46 +1,72 @@ -using DecimalMath; using InnovEnergy.Lib.Protocols.Modbus.Clients; using InnovEnergy.Lib.Protocols.Modbus.Connections; -using InnovEnergy.Lib.StatusApi.Connections; +using InnovEnergy.Lib.Units.Composite; +using static DecimalMath.DecimalEx; namespace InnovEnergy.Lib.Devices.AMPT; public class AmptCommunicationUnit { - private ModbusTcpClient Modbus { get; } + private ModbusTcpClient? Modbus { get; set; } - private const Int32 RegistersPerDevice = 16; - private const Int32 FirstDeviceOffset = 85; + private const UInt16 RegistersPerDevice = 16; + private const UInt16 FirstDeviceOffset = 85; + + public String Hostname { get; } + public UInt16 Port { get; } + public Byte SlaveAddress { get; } - public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1) { - var connection = new ModbusTcpConnection(hostname, port); - Modbus = new ModbusTcpClient(connection, slaveAddress); + Hostname = hostname; + Port = port; + SlaveAddress = slaveAddress; } - public AmptStatus? ReadStatus() + public AmptCommunicationUnitStatus? ReadStatus() { try { + OpenConnection(); return TryReadStatus(); } - catch (Exception) + catch { - Modbus.CloseConnection(); + CloseConnection(); return null; } } - - private AmptStatus TryReadStatus() - { - // Console.WriteLine("Reading Ampt Device"); - - var r = Modbus.ReadHoldingRegisters(1, 116); - var currentFactor = DecimalEx.Pow(10.0m, r.GetInt16(73)); - var voltageFactor = DecimalEx.Pow(10.0m, r.GetInt16(74)); - var energyFactor = DecimalEx.Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh + private void CloseConnection() + { + try + { + Modbus?.CloseConnection(); + } + catch + { + // ignored + } + + Modbus = null; + } + + private void OpenConnection() + { + if (Modbus is null) + { + var connection = new ModbusTcpConnection(Hostname, Port); + Modbus = new ModbusTcpClient(connection, SlaveAddress); + } + } + + private AmptCommunicationUnitStatus TryReadStatus() + { + var r = Modbus!.ReadHoldingRegisters(1, 116); + + var currentFactor = Pow(10.0m, r.GetInt16(73)); + var voltageFactor = Pow(10.0m, r.GetInt16(74)); + var energyFactor = Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh var nbrOfDevices = r.GetUInt16(78); var devices = Enumerable @@ -48,84 +74,45 @@ public class AmptCommunicationUnit .Select(ReadDeviceStatus) .ToList(); - var amptSt = new AmptStatus - ( - Sid : r.GetUInt32(1), - IdSunSpec : r.GetUInt16(3), - Manufacturer : r.GetString(5, 16), - Model : r.GetString(21, 16), - Version : r.GetString(45, 8), - SerialNumber : r.GetString(53, 16), - DeviceAddress : r.GetInt16(69), - IdVendor : r.GetUInt16(71), - Devices : devices - // devices.d Current1 = r.GetInt16(90) * currentFactor, - // Current2 = r.GetInt16(106) * currentFactor, - // Voltage1 = r.GetUInt32(91) * voltageFactor, - // Voltage2 = r.GetUInt32(107) * voltageFactor - - ); - - - return amptSt; - - Decimal ReadDevicesVoltage(Int32 numberOfDevice) + return new AmptCommunicationUnitStatus { - var avgVoltage = 0.0m; - - for (var i = 0; i < numberOfDevice; i++) + Sid = r.GetUInt32(1), + IdSunSpec = r.GetUInt16(3), + Manufacturer = r.GetString(5, 16), + Model = r.GetString(21, 16), + Version = r.GetString(45, 8), + SerialNumber = r.GetString(53, 16), + DeviceAddress = r.GetInt16(69), + IdVendor = r.GetUInt16(71), + Devices = devices + }; + + AmptStatus ReadDeviceStatus(Int32 deviceNumber) + { + var baseAddress = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address + + return new AmptStatus { - var b = (UInt16)(FirstDeviceOffset + i * RegistersPerDevice); // base address - - avgVoltage+= r.GetUInt32((UInt16)(b + 6)) * voltageFactor; - } - - return avgVoltage / numberOfDevice; - } - - Decimal ReadDevicesCurrent(Int32 numberOfDevice) - { - Decimal avgCurrent = 0; - - for (var i = 0; i < numberOfDevice; i++) - { - var b = (UInt16)(FirstDeviceOffset + i * RegistersPerDevice); // base address - - avgCurrent+= r!.GetUInt32((UInt16)(b + 5)) * voltageFactor; - } - - return avgCurrent / numberOfDevice; - } - - AmptDeviceStatus ReadDeviceStatus(Int32 deviceNumber) - { - var b = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address - - return new AmptDeviceStatus - ( - Dc : new DcConnection - ( - Voltage:r.GetUInt32((UInt16)(b + 6)) * voltageFactor, - Current:r.GetUInt16((UInt16)(b + 5)) * currentFactor - ), - DeviceId : r.GetInt16 (b) , - Timestamp : r.GetUInt32((UInt16)(b + 3)), - ProductionToday : r.GetUInt32((UInt16)(b + 12))* energyFactor, - - Strings : new [] + Dc = new DcBus { - new DcConnection - ( - Voltage : r.GetUInt32((UInt16)(b + 8)) * voltageFactor, - Current : r.GetUInt16((UInt16)(b + 14)) * currentFactor - ), - new DcConnection - ( - Voltage : r.GetUInt32((UInt16)(b + 9)) * voltageFactor, - Current : r.GetUInt16((UInt16)(b + 15)) * currentFactor - ) - } - ); + Voltage = r.GetUInt32((UInt16)(baseAddress + 6)) * voltageFactor, + Current = r.GetUInt16((UInt16)(baseAddress + 5)) * currentFactor + }, + Strings = new DcBus[] + { + new() + { + Voltage = r.GetUInt32((UInt16)(baseAddress + 8)) * voltageFactor, + Current = r.GetUInt16((UInt16)(baseAddress + 14)) * currentFactor + }, + new() + { + Voltage = r.GetUInt32((UInt16)(baseAddress + 9)) * voltageFactor, + Current = r.GetUInt16((UInt16)(baseAddress + 15)) * currentFactor + } + }, + ProductionToday = r.GetUInt32((UInt16)(baseAddress + 12)) * energyFactor, + }; } } } \ No newline at end of file diff --git a/csharp/Lib/Devices/AMPT/AmptCommunicationUnitStatus.cs b/csharp/Lib/Devices/AMPT/AmptCommunicationUnitStatus.cs new file mode 100644 index 000000000..757d3592f --- /dev/null +++ b/csharp/Lib/Devices/AMPT/AmptCommunicationUnitStatus.cs @@ -0,0 +1,16 @@ +namespace InnovEnergy.Lib.Devices.AMPT; + +public record AmptCommunicationUnitStatus +{ + public UInt32 Sid { get; init; } // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map + public UInt16 IdSunSpec { get; init; } // A well-known value 1, uniquely identifies this as a SunSpec Common Model + + public String Manufacturer { get; init; } = "undefined"; // A well-known value registered with SunSpec for compliance: "Ampt" + public String Model { get; init; } = "undefined"; // Manufacturer specific value "Communication Unit" + public String Version { get; init; } = "undefined"; // Software Version + public String SerialNumber { get; init; } = "undefined"; // Manufacturer specific value + public Int16 DeviceAddress { get; init; } // Modbus Device ID + public UInt16 IdVendor { get; init; } // Ampt SunSpec Vendor Code 64050 + + public IReadOnlyList Devices { get; init; } = Array.Empty(); +} \ No newline at end of file diff --git a/csharp/Lib/Devices/AMPT/AmptDeviceStatus.cs b/csharp/Lib/Devices/AMPT/AmptDeviceStatus.cs deleted file mode 100644 index 8ee809b72..000000000 --- a/csharp/Lib/Devices/AMPT/AmptDeviceStatus.cs +++ /dev/null @@ -1,15 +0,0 @@ -using InnovEnergy.Lib.StatusApi; -using InnovEnergy.Lib.StatusApi.Connections; - -namespace InnovEnergy.Lib.Devices.AMPT; - -public record AmptDeviceStatus -( - DcConnection Dc, - // UInt16 NbrOfStrings, - Int16 DeviceId, // The string number - UInt32 Timestamp, // The UTC timestamp of the measurements - Decimal ProductionToday, // converted to kW in AmptCU class - IReadOnlyList Strings -): MpptStatus(Dc, Strings) -{} \ No newline at end of file diff --git a/csharp/Lib/Devices/AMPT/AmptStatus.cs b/csharp/Lib/Devices/AMPT/AmptStatus.cs index 45ff33662..8e2140bdc 100644 --- a/csharp/Lib/Devices/AMPT/AmptStatus.cs +++ b/csharp/Lib/Devices/AMPT/AmptStatus.cs @@ -1,24 +1,9 @@ +using InnovEnergy.Lib.StatusApi; +using InnovEnergy.Lib.Units; + namespace InnovEnergy.Lib.Devices.AMPT; -public record AmptStatus -( - UInt32 Sid, // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map - UInt16 IdSunSpec, // A well-known value 1, uniquely identifies this as a SunSpec Common Model - // UInt16 L, // Well-known # of 16-bit registers to follow : 66 - String? Manufacturer, // A well-known value registered with SunSpec for compliance: "Ampt" - String? Model, // Manufacturer specific value "Communication Unit" - String? Version, // Software Version - String? SerialNumber, // Manufacturer specific value - Int16 DeviceAddress, // Modbus Device ID - UInt16 IdVendor, // Ampt SunSpec Vendor Code 64050 - // Decimal Current1, - // Decimal Current2, - // Decimal Voltage1, - // Decimal Voltage2, - IReadOnlyList Devices - //internal const UInt16 StartRegister = 1; - //internal const UInt16 TotalNbOfRegister = 116; -) +public record AmptStatus : MpptStatus { - -} \ No newline at end of file + public Energy ProductionToday { get; init; } // converted to kW in AmptCU class +} diff --git a/csharp/Lib/Devices/AMPT/AmptStringStatus.cs b/csharp/Lib/Devices/AMPT/AmptStringStatus.cs deleted file mode 100644 index bd3b2c5e0..000000000 --- a/csharp/Lib/Devices/AMPT/AmptStringStatus.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace InnovEnergy.Lib.Devices.AMPT; - -public record AmptStringStatus -{ - public Decimal Voltage { get; init; } - public Decimal Current { get; init; } -} \ No newline at end of file diff --git a/csharp/Lib/Devices/Adam6060/Adam6060Device.cs b/csharp/Lib/Devices/Adam6060/Adam6060Device.cs index 09af1a1a0..d910baf41 100644 --- a/csharp/Lib/Devices/Adam6060/Adam6060Device.cs +++ b/csharp/Lib/Devices/Adam6060/Adam6060Device.cs @@ -7,31 +7,60 @@ namespace InnovEnergy.Lib.Devices.Adam6060; public class Adam6060Device { - private ModbusTcpClient Modbus { get; } + public String Hostname { get; } + public UInt16 Port { get; } + public Byte SlaveAddress { get; } + + private ModbusTcpClient? Modbus { get; set; } public Adam6060Device(String hostname, UInt16 port = 5004, Byte slaveAddress = 2) { - var connection = new ModbusTcpConnection(hostname, port); - Modbus = new ModbusTcpClient(connection, slaveAddress); + Hostname = hostname; + Port = port; + SlaveAddress = slaveAddress; + } + + private void OpenConnection() + { + if (Modbus is null) + { + var connection = new ModbusTcpConnection(Hostname, Port); + Modbus = new ModbusTcpClient(connection, SlaveAddress); + } } public Adam6060Status? ReadStatus() { try { + OpenConnection(); return TryReadStatus(); } - catch (Exception) + catch { - Modbus.CloseConnection(); + CloseConnection(); return null; } } + private void CloseConnection() + { + try + { + Modbus?.CloseConnection(); + } + catch + { + // ignored + } + + Modbus = null; + } + private Adam6060Status TryReadStatus() { - var inputs = Modbus.ReadDiscreteInputs(DigitalInputsStartRegister, NbDigitalInputs); - var relays = Modbus.ReadDiscreteInputs(RelaysStartRegister, NbRelays); + var inputs = Modbus!.ReadDiscreteInputs(DigitalInputsStartRegister, NbDigitalInputs); + var relays = Modbus!.ReadDiscreteInputs(RelaysStartRegister, NbRelays); return new Adam6060Status { @@ -55,17 +84,19 @@ public class Adam6060Device { try { - Modbus.WriteMultipleCoils(RelaysStartRegister, control.Relay0, - control.Relay1, - control.Relay2, - control.Relay3, - control.Relay4, - control.Relay5); + OpenConnection(); + + Modbus!.WriteMultipleCoils(RelaysStartRegister, control.Relay0, + control.Relay1, + control.Relay2, + control.Relay3, + control.Relay4, + control.Relay5); return true; } - catch (Exception) + catch { - Modbus.CloseConnection(); + CloseConnection(); return false; } } diff --git a/csharp/Lib/Units/State.cs b/csharp/Lib/Units/State.cs index 3e8e48395..88610517c 100644 --- a/csharp/Lib/Units/State.cs +++ b/csharp/Lib/Units/State.cs @@ -34,4 +34,11 @@ public readonly struct State : IReadOnlyList public Int32 Count => Values.Count; public String this[Int32 index] => Values[index]; -} \ No newline at end of file + + public static State From(T t) where T : Enum + { + return new State(t); + } +} + + diff --git a/csharp/Lib/Units/StateOfT.cs b/csharp/Lib/Units/StateOfT.cs new file mode 100644 index 000000000..4a1242dc2 --- /dev/null +++ b/csharp/Lib/Units/StateOfT.cs @@ -0,0 +1,27 @@ +using System.Collections; + +namespace InnovEnergy.Lib.Units; + +public readonly struct State : IReadOnlyList where T:Enum +{ + public IReadOnlyList Values { get; } + + public State(IReadOnlyList values) => Values = values; + + public State(params T[] values) : this((IReadOnlyList)values){} + public State(params State[] states) : this((IReadOnlyList)states.SelectMany(s => s.Values).ToList()){} + + public static implicit operator State(T s) => new((IReadOnlyList)s); + public static implicit operator State(List s) => new((IReadOnlyList)s); + public static implicit operator State(T[] s) => new((IReadOnlyList)s); + + public static State operator |(State left, State right) => new State(left, right); + + public IEnumerator GetEnumerator() => Values.GetEnumerator(); + + public override String ToString() => String.Join("; ", Values); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public Int32 Count => Values.Count; + public T this[Int32 index] => Values[index]; +} \ No newline at end of file