diff --git a/csharp/App/Backend/Backend.csproj b/csharp/App/Backend/Backend.csproj index bf230bd4e..71cefbddc 100644 --- a/csharp/App/Backend/Backend.csproj +++ b/csharp/App/Backend/Backend.csproj @@ -9,10 +9,10 @@ - + @@ -33,12 +33,207 @@ + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index ada5ed86a..12f0db7c1 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -197,7 +197,10 @@ public class Controller : ControllerBase if (user == null) return Unauthorized(); - return user.DescendantUsers().Select(u => u.HidePassword()).ToList(); + return user + .DescendantUsers() + .Select(u => u.HidePassword()) + .ToList(); } @@ -233,8 +236,6 @@ public class Controller : ControllerBase public ActionResult> GetAllFoldersAndInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; - - "GetAllFoldersAndInstallations".WriteLine(); if (user is null) return Unauthorized(); @@ -252,7 +253,7 @@ public class Controller : ControllerBase [HttpPost(nameof(CreateUser))] - public ActionResult CreateUser(User newUser, Token authToken) + public ActionResult CreateUser([FromBody] User newUser, Token authToken) { return Db.GetSession(authToken).Create(newUser) ? newUser.HidePassword() @@ -260,7 +261,7 @@ public class Controller : ControllerBase } [HttpPost(nameof(CreateInstallation))] - public async Task> CreateInstallation([FromBody]Installation installation, Token authToken) + public async Task> CreateInstallation([FromBody] Installation installation, Token authToken) { var session = Db.GetSession(authToken); @@ -271,7 +272,7 @@ public class Controller : ControllerBase } [HttpPost(nameof(CreateFolder))] - public ActionResult CreateFolder(Folder folder, Token authToken) + public ActionResult CreateFolder([FromBody] Folder folder, Token authToken) { var session = Db.GetSession(authToken); @@ -331,7 +332,7 @@ public class Controller : ControllerBase var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization - var installation = Db.GetFolderById(installationAccess.InstallationId); + var installation = Db.GetInstallationById(installationAccess.InstallationId); var user = Db.GetUserById(installationAccess.UserId); return session.RevokeUserAccessTo(user, installation) @@ -342,7 +343,7 @@ public class Controller : ControllerBase [HttpPut(nameof(UpdateUser))] - public ActionResult UpdateUser(User updatedUser, Token authToken) + public ActionResult UpdateUser([FromBody] User updatedUser, Token authToken) { var session = Db.GetSession(authToken); @@ -366,7 +367,7 @@ public class Controller : ControllerBase [HttpPut(nameof(UpdateInstallation))] - public ActionResult UpdateInstallation(Installation installation, Token authToken) + public ActionResult UpdateInstallation([FromBody] Installation installation, Token authToken) { var session = Db.GetSession(authToken); @@ -378,7 +379,7 @@ public class Controller : ControllerBase [HttpPut(nameof(UpdateFolder))] - public ActionResult UpdateFolder(Folder folder, Token authToken) + public ActionResult UpdateFolder([FromBody] Folder folder, Token authToken) { var session = Db.GetSession(authToken); @@ -441,6 +442,36 @@ public class Controller : ControllerBase : Unauthorized(); } + + [HttpPost(nameof(ResetPasswordRequest))] + public ActionResult> ResetPasswordRequest(String username) + { + var user = Db.GetUserByEmail(username); + + if (user is null) + return Unauthorized(); + + var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user)); + + return Db.Create(session) && Db.SendPasswordResetEmail(user, session.Token) + ? Ok() + : Unauthorized(); + } + + [HttpGet(nameof(ResetPassword))] + public ActionResult> ResetPassword(Token token) + { + var user = Db.GetSession(token)?.User; + + if (user is null) + return Unauthorized(); + + //todo dont hardcode url + return Db.DeleteUserPassword(user) + ? RedirectToRoute("https://monitor.innov.energy") + : Unauthorized(); + } + } diff --git a/csharp/App/Backend/DataTypes/Installation.cs b/csharp/App/Backend/DataTypes/Installation.cs index 71fb0d640..492558c54 100644 --- a/csharp/App/Backend/DataTypes/Installation.cs +++ b/csharp/App/Backend/DataTypes/Installation.cs @@ -9,8 +9,9 @@ public class Installation : TreeNode public String Country { get; set; } = ""; // TODO: make relation - [Ignore] public IReadOnlyList OrderNumbers { get; set; } = Array.Empty(); - + //public IReadOnlyList OrderNumbers { get; set; } = Array.Empty(); + public String OrderNumbers { get; set; } = ""; + public Double Lat { get; set; } public Double Long { get; set; } diff --git a/csharp/App/Backend/DataTypes/Methods/Folder.cs b/csharp/App/Backend/DataTypes/Methods/Folder.cs index a71960f52..8098b53c0 100644 --- a/csharp/App/Backend/DataTypes/Methods/Folder.cs +++ b/csharp/App/Backend/DataTypes/Methods/Folder.cs @@ -52,6 +52,11 @@ public static class FolderMethods .Skip(1); // skip self } + public static IEnumerable DescendantFoldersAndSelf(this Folder parent) + { + return parent + .TraverseDepthFirstPreOrder(ChildFolders); + } public static Boolean IsDescendantOf(this Folder folder, Folder ancestor) { return folder diff --git a/csharp/App/Backend/DataTypes/Methods/Installation.cs b/csharp/App/Backend/DataTypes/Methods/Installation.cs index fbae86041..cf6bdb014 100644 --- a/csharp/App/Backend/DataTypes/Methods/Installation.cs +++ b/csharp/App/Backend/DataTypes/Methods/Installation.cs @@ -1,4 +1,5 @@ using InnovEnergy.App.Backend.Database; +using InnovEnergy.App.Backend.Relations; using InnovEnergy.App.Backend.S3; using InnovEnergy.Lib.Utils; @@ -119,12 +120,28 @@ public static class InstallationMethods return Db.Installations.Any(i => i.Id == installation.Id); } - public static IReadOnlyList GetOrderNumbers(this Installation installation) + public static Boolean SetOrderNumbers(this Installation installation) { - return Db.OrderNumber2Installation + foreach (var orderNumber in installation.OrderNumbers.Split(',')) + { + + var o2I = new OrderNumber2Installation + { + OrderNumber = orderNumber, + InstallationId = installation.Id + }; + Db.Create(o2I); + } + + return true; + } + + public static String GetOrderNumbers(this Installation installation) + { + return string.Join(", ", Db.OrderNumber2Installation .Where(i => i.InstallationId == installation.Id) .Select(i => i.OrderNumber) - .ToReadOnlyList(); + .ToReadOnlyList()); } public static Installation FillOrderNumbers(this Installation installation) diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index 9dddd78e7..488465263 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -83,18 +83,19 @@ public static class SessionMethods var user = session?.User; return user is not null - && installation is not null - && user.HasWriteAccess - && user.HasAccessToParentOf(installation) - && Db.Create(installation) // TODO: these two in a transaction - && Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id }) - && await installation.CreateBucket() - && await installation.RenewS3Credentials(); // generation of access _after_ generation of - // bucket to prevent "zombie" access-rights. - // This might fuck us over if the creation of access rights fails, - // as bucket-names are unique and bound to the installation id... -K + && installation is not null + && user.HasWriteAccess + && user.HasAccessToParentOf(installation) + && Db.Create(installation) // TODO: these two in a transaction + && installation.SetOrderNumbers() + && Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id }) + && await installation.CreateBucket() + && await installation.RenewS3Credentials(); // generation of access _after_ generation of + // bucket to prevent "zombie" access-rights. + // This might fuck us over if the creation of access rights fails, + // as bucket-names are unique and bound to the installation id... -K } - + public static Boolean Update(this Session? session, Installation? installation) { var user = session?.User; @@ -104,7 +105,7 @@ public static class SessionMethods if (!Equals(originalOrderNumbers, installation?.OrderNumbers)) { - foreach (var orderNumber in installation!.OrderNumbers) + foreach (var orderNumber in installation!.OrderNumbers.Split(',')) { if (originalOrderNumbers.Contains(orderNumber)) continue; var o2I = new OrderNumber2Installation @@ -115,7 +116,7 @@ public static class SessionMethods Db.Create(o2I); } - foreach (var orderNumberOld in originalOrderNumbers) + foreach (var orderNumberOld in originalOrderNumbers.Split(',')) { if (!installation!.OrderNumbers.Contains(orderNumberOld)) { @@ -138,13 +139,13 @@ public static class SessionMethods public static async Task Delete(this Session? session, Installation? installation) { var user = session?.User; - + return user is not null - && installation is not null - && user.HasWriteAccess - && user.HasAccessTo(installation) - && Db.Delete(installation) - && await installation.DeleteBucket(); + && installation is not null + && user.HasWriteAccess + && user.HasAccessTo(installation) + && Db.Delete(installation) + && await installation.DeleteBucket(); } public static Boolean Create(this Session? session, User newUser) diff --git a/csharp/App/Backend/DataTypes/TreeNode.cs b/csharp/App/Backend/DataTypes/TreeNode.cs index b1a4a72fa..5613fbbdf 100644 --- a/csharp/App/Backend/DataTypes/TreeNode.cs +++ b/csharp/App/Backend/DataTypes/TreeNode.cs @@ -5,7 +5,7 @@ namespace InnovEnergy.App.Backend.DataTypes; public abstract partial class TreeNode { [PrimaryKey, AutoIncrement] - public virtual Int64 Id { get; set; } + public virtual Int64 Id { get; set; } public virtual String Name { get; set; } = ""; // overridden by User (unique) public String Information { get; set; } = ""; // unstructured random info diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 0a630bc50..09adf1776 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -1,14 +1,10 @@ -using System.Data.SQLite; using System.Reactive.Concurrency; using System.Reactive.Linq; -using System.Runtime.InteropServices; using CliWrap; using CliWrap.Buffered; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; -using InnovEnergy.Lib.Utils; -using Microsoft.Identity.Client; using SQLite; using SQLiteConnection = SQLite.SQLiteConnection; @@ -27,7 +23,8 @@ public static partial class Db .Last().Name; var fileConnection = new SQLiteConnection("DbBackups/"+latestDb); - + + Console.Out.Write(latestDb); var memoryConnection = new SQLiteConnection(":memory:"); // fileConnection.Backup(memoryConnection.DatabasePath); @@ -74,7 +71,7 @@ public static partial class Db public static void BackupDatabase() { var filename = "db-" + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ".sqlite"; - Connection.Backup("DbBackups/"+filename); + Connection.Backup("DbBackups/" + filename); } public static TableQuery Sessions => Connection.Table(); @@ -166,5 +163,16 @@ public static partial class Db await installation.RenewS3Credentials(); } } - + + public static Boolean SendPasswordResetEmail(User user, String sessionToken) + { + return Email.Email.SendPasswordResetMessage(user, sessionToken); + } + + public static Boolean DeleteUserPassword(User user) + { + user.Password = ""; + user.MustResetPassword = true; + return Update(user); + } } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index 55ff12f84..cb3895037 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -10,12 +10,18 @@ public static partial class Db { public static Boolean Delete(Folder folder) { - return RunTransaction(DeleteFolderAndAllItsDependencies); + var deleteSuccess= RunTransaction(DeleteFolderAndAllItsDependencies); + if (deleteSuccess) + { + BackupDatabase(); + } + + return deleteSuccess; Boolean DeleteFolderAndAllItsDependencies() { return folder - .DescendantFolders() + .DescendantFoldersAndSelf() .All(DeleteDescendantFolderAndItsDependencies); } @@ -24,10 +30,8 @@ public static partial class Db FolderAccess .Delete(r => r.FolderId == f.Id); Installations.Delete(r => r.ParentId == f.Id); var delete = Folders.Delete(r => r.Id == f.Id); - var deleteSuccess = delete > 0; - if (deleteSuccess) - BackupDatabase(); - return deleteSuccess; + + return delete>0; } } @@ -42,6 +46,7 @@ public static partial class Db Boolean DeleteInstallationAndItsDependencies() { InstallationAccess.Delete(i => i.InstallationId == installation.Id); + OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id); return Installations.Delete(i => i.Id == installation.Id) > 0; } } diff --git a/csharp/App/Backend/Email/Email.cs b/csharp/App/Backend/Email/Email.cs new file mode 100644 index 000000000..f54021152 --- /dev/null +++ b/csharp/App/Backend/Email/Email.cs @@ -0,0 +1,54 @@ +using System.Diagnostics.CodeAnalysis; +using InnovEnergy.App.Backend.DataTypes; +using InnovEnergy.Lib.Mailer; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace InnovEnergy.App.Backend.Email; +public static class Email + { + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public static Boolean SendVerificationMessage(User emailRecipientUser) + { + var config = JsonSerializer.Deserialize(File.OpenRead("./Resources/smtpConfig.json"))!; + var mailer = new Mailer(); + + mailer.From("InnovEnergy", "noreply@innov.energy"); + mailer.To(emailRecipientUser.Name, emailRecipientUser.Email); + + mailer.Subject("Create a new password for your Innovenergy-Account"); + mailer.Body("Dear " + emailRecipientUser.Name + + "\n Please create a new password for your Innovenergy-account." + + "\n To do this just login at https://HEEEEELP"); + + return mailer.SendEmailUsingSmtpConfig(config); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public static Boolean SendPasswordResetMessage (User emailRecipientUser, String token) + { + var config = JsonSerializer.Deserialize(File.OpenRead("./Resources/smtpConfig.json"))!; + + //todo am I right? + const String resetLink = "https://monitor.innov.energy/api/ResetPassword"; + var mailer = new Mailer(); + + try + { + + mailer.From("InnovEnergy", "noreply@innov.energy"); + mailer.To(emailRecipientUser.Name, emailRecipientUser.Email); + + mailer.Subject("Reset the password of your Innovenergy-Account"); + mailer.Body("Dear " + emailRecipientUser.Name + + "\n To reset your password open this link:" + + resetLink + "?token=" + + token); + + return mailer.SendEmailUsingSmtpConfig(config); + } + catch (Exception) + { + return false; + } + } + } diff --git a/csharp/App/Backend/Mailer/Mailer.cs b/csharp/App/Backend/Mailer/Mailer.cs deleted file mode 100644 index a04097e06..000000000 --- a/csharp/App/Backend/Mailer/Mailer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using InnovEnergy.App.Backend.DataTypes; -using MailKit.Net.Smtp; -using MimeKit; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace InnovEnergy.App.Backend.Mailer; -public static class Mailer - { - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public static Boolean SendVerificationMessage (User emailRecipientUser) - { - var config = JsonSerializer.Deserialize(File.OpenRead("./Resources/smtpConfig.json"))!; - var email = new MimeMessage(); - - try - { - - email.From.Add(new MailboxAddress("InnovEnergy", "noreply@innov.energy")); - email.To.Add(new MailboxAddress(emailRecipientUser.Name, emailRecipientUser.Email)); - - email.Subject = "Create a new password for your Innovenergy-Account"; - email.Body = new TextPart(MimeKit.Text.TextFormat.Plain) { - Text = "Dear " + emailRecipientUser.Name + "\n Please create a new password for your Innovenergy-account." + - "\n To do this just login at https://HEEEEELP" - }; - - using var smtp = new SmtpClient(); - smtp.Connect(config.Url, config.Port, false); - - smtp.Authenticate(config.Username, config.Password); - - smtp.Send(email); - smtp.Disconnect(true); - } - catch (Exception) - { - return false; - } - - return true; - } - } diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 3e49baeb2..f39e5e463 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -3,6 +3,8 @@ using InnovEnergy.App.Backend.Database; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; +using System.Net; +using InnovEnergy.Lib.Utils; namespace InnovEnergy.App.Backend; @@ -12,8 +14,8 @@ public static class Program { //Db.CreateFakeRelations(); Db.Init(); - var builder = WebApplication.CreateBuilder(args); + builder.Services.AddControllers(); builder.Services.AddProblemDetails(setup => { @@ -21,7 +23,7 @@ public static class Program setup.IncludeExceptionDetails = (ctx, env) => builder.Environment.IsDevelopment() || builder.Environment.IsStaging(); //This handles our Exceptions - setup.Map(exception => new ProblemDetails() + setup.Map(exception => new ProblemDetails { Detail = exception.Detail, Status = exception.Status, @@ -38,6 +40,16 @@ public static class Program }); var app = builder.Build(); + + app.Use(async (context, next) => + { + var x = 2; + + context.Request.WriteLine(); + + await next(context); + }); + app.UseForwardedHeaders(new ForwardedHeadersOptions { @@ -51,12 +63,11 @@ public static class Program } app.UseCors(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()) ; - app.UseHttpsRedirection(); + //app.UseHttpsRedirection(); app.MapControllers(); app.UseProblemDetails(); - - app.Run(); + app.Run(); } private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo diff --git a/csharp/App/Backend/Relations/Session.cs b/csharp/App/Backend/Relations/Session.cs index 03af863ee..34cbff4f0 100644 --- a/csharp/App/Backend/Relations/Session.cs +++ b/csharp/App/Backend/Relations/Session.cs @@ -36,7 +36,7 @@ public class Session : Relation { var token = new Byte[24]; Random.Shared.NextBytes(token); - return Convert.ToBase64String(token); + return Convert.ToBase64String(token).Replace("/",""); } } \ No newline at end of file diff --git a/csharp/App/Backend/exoscale.toml b/csharp/App/Backend/S3/exoscale.toml similarity index 100% rename from csharp/App/Backend/exoscale.toml rename to csharp/App/Backend/S3/exoscale.toml diff --git a/csharp/App/S3Explorer/Program.cs b/csharp/App/S3Explorer/Program.cs index 287b9aa56..3fdc7baa5 100644 --- a/csharp/App/S3Explorer/Program.cs +++ b/csharp/App/S3Explorer/Program.cs @@ -1,12 +1,11 @@ -using InnovEnergy.App.Backend.S3; -using InnovEnergy.Lib.Time.Unix; +using System.ComponentModel; +using InnovEnergy.App.Backend.S3; using InnovEnergy.Lib.Utils; namespace S3Explorer; public static class Program { - private const String BucketSalt = "-3e5b3069-214a-43ee-8d85-57d72000c19d"; public static async Task Main(String[] args) { @@ -19,103 +18,78 @@ public static class Program } // Help message - if (args.Length < 1 || args.Contains("-h")) + if (args.Length < 4 || args.Contains("-h")) { - Console.WriteLine("Usage: S3Explorer installation-id [from-unix-time] [to-unix-time] [nb-data-points]"); + Console.WriteLine("Usage: S3Explorer [BucketId] [from:Unix-time] [to:Unix-time] [#Data-points]"); Console.WriteLine("-h Shows this message."); Console.WriteLine("-s 🐍"); return 0; } // Parsing Arguments - var bucketName = args[0] + BucketSalt; - var now = UnixTime.Now; + var bucketName = args[0] + "-3e5b3069-214a-43ee-8d85-57d72000c19d"; + var startTime = Int64.Parse(args[1]); + var endTime = Int64.Parse(args[2]); + var numberOfDataPoints = Int64.Parse(args[3]); - var startTime = Int64.Parse(args.ElementAtOr(1, (now - UnixTimeSpan.FromSeconds(20)).ToString())); - var endTime = Int64.Parse(args.ElementAtOr(2, now.ToString())); - var nDataPoints = Int64.Parse(args.ElementAtOr(3, "10")); - - var timestampList = GetDataTimestamps(startTime, endTime, nDataPoints); + var timeBetweenDataPoints = TimeBetweenDataPoints(startTime, endTime, numberOfDataPoints); - await PrintFiles(bucketName, timestampList); + // Building a List of the timestamps we want to grab the files for. + var timestampList = new List { }; + for (var i = startTime; i <= endTime; i += timeBetweenDataPoints) + { + //Rounding to even numbers only (we only save every second second) + timestampList.Add((i/2 *2).ToString()); + } + + await PrintFiles(bucketName,timestampList); // Success return 0; } - private static IEnumerable GetDataTimestamps(Int64 startTime, Int64 endTime, Int64 nDataPoints) + private static async Task PrintFiles(String bucketName, List timestampList) { - // Calculating temporal distance of data files from the number of requested points. (rounding for int division) - var timeSpan = endTime - startTime; - var timeBetweenDataPoints = (Double)(timeSpan / nDataPoints); - timeBetweenDataPoints = Math.Max(2, timeBetweenDataPoints); - // We only upload data every second second so sampling more is impossible. - // If this ever changes we might have to change this as well. + var newestDataFilename = timestampList.Last(); + var csvFileText = await GetFileText(bucketName, newestDataFilename); - // Building a List of the timestamps we want to grab the files for. - for (Double i = startTime; i <= endTime; i += timeBetweenDataPoints) - { - //Rounding to even numbers only (we only save every second second) - var integer = (Int64) Math.Round(i); - yield return integer/2 * 2; - } - } - - private static async Task PrintFiles(String bucketName, IEnumerable timestampList) - { - - var columns = new Dictionary> - { - ["timestamp"] = new() - }; - var index = 0; + // Building Header-Row from the newest data + csvFileText + .Select(l => l.Split(";")) + .Select(l => l[0]) + .Prepend("Timestamp") + .JoinWith(";") + .WriteLine(); foreach (var timestamp in timestampList) { - var csvFileText = await GetFileText(bucketName, timestamp); - - columns["timestamp"].Add(timestamp.ToString()); + csvFileText = await GetFileText(bucketName, timestamp); - var dict = csvFileText is null - ? new Dictionary() - : csvFileText - .Select(l => l.Split(";")) - .ToDictionary(kv => kv[0], kv => kv[1]); - - foreach (var key in dict.Keys) - { - // if a key is not yet present in columns we need to backfill it with nulls - if (!columns.ContainsKey(key)) - columns[key] = Enumerable.Repeat(null, index).ToList(); - - columns[key].Add(dict[key]); - } - - // if a key in columns is not present in this record (dict) (except the timestamp) we need to set it to null - foreach (var key in columns.Keys.Where(key => !dict.ContainsKey(key) && key != "timestamp")) - { - columns[key].Add(null); - } - index++; + // Writing Data below data-keys in a timestamped row + csvFileText.Select(l => l.Split(";")) + .Select(l => l[1]) + .Prepend(timestamp) + .JoinWith(";") + .WriteLine(); } - var headerKeys = columns - .Keys - .OrderBy(k => k) - .Where(k => k != "timestamp") - .Prepend("timestamp") - .ToList(); - - String.Join(';', headerKeys).WriteLine(); - - Enumerable.Range(0, index) - .Select(i => headerKeys.Select(hk => columns[hk][i]).JoinWith(";")) - .ForEach(Console.WriteLine); } - - // This Method extracts the Text from a given csv file on the s3 bucket - private static async Task?> GetFileText(String bucketName, Int64 timestamp) + + private static Int64 TimeBetweenDataPoints(Int64 startTime, Int64 endTime, Int64 numberOfDataPoints) { - return await S3Access.Admin.GetFileLines(bucketName, $"{timestamp}.csv"); + // Calculating temporal distance of data files from the number of requested points. + var timeSpan = endTime - startTime; + var timeBetweenDataPoints = timeSpan / numberOfDataPoints; + + // We only upload data every second second so sampling more is impossible. + // If this ever changes we might have to change this as well. + timeBetweenDataPoints = Math.Max(timeBetweenDataPoints, 2); + return timeBetweenDataPoints; + } + + // This Method extracts the Text from a given csv file on the s3 bucket + private static async Task GetFileText(String bucketName, String filename) + { + return await S3Access.Admin.GetFileText(bucketName, filename + ".csv"); } } \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Controller.cs b/csharp/App/VrmGrabber/Controller.cs index dcc6bfb66..cf174de10 100644 --- a/csharp/App/VrmGrabber/Controller.cs +++ b/csharp/App/VrmGrabber/Controller.cs @@ -188,7 +188,7 @@ th { /* header cell */ await SendNewBatteryFirmware(installationIp); var batteryTtyName = split[1].Split(".").Last(); - var localCommand = $"/opt/innovenergy/scripts/upload-bms-firmware {batteryTtyName} 2 /opt/innovenergy/{FirmwareVersion}.bin"; + var localCommand = $"/opt/innovenergy/scripts/upload-bms-firmware {batteryTtyName} 2 /opt/innovenergy/bms-firmware/{FirmwareVersion}.bin"; var installation = Db.Installations.First(installation => installation.Ip == installationIp); installation.BatteryUpdateStatus = "Running"; Db.Update(installation: installation); diff --git a/csharp/App/VrmGrabber/db.sqlite b/csharp/App/VrmGrabber/db.sqlite index 382d49a9e..81843140c 100644 Binary files a/csharp/App/VrmGrabber/db.sqlite and b/csharp/App/VrmGrabber/db.sqlite differ diff --git a/csharp/App/VrmGrabber/server.py b/csharp/App/VrmGrabber/server.py index 27abdbd52..5d5c350ec 100644 --- a/csharp/App/VrmGrabber/server.py +++ b/csharp/App/VrmGrabber/server.py @@ -3,7 +3,7 @@ from flask import Flask from json2html import json2html app = Flask(__name__) -serverUrl = "https://127.0.0.1:7087/api" #todo change me +serverUrl = "https://127.0.0.1:8000/api" #todo change me @app.route('/') def hello(): diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln index 311dd0f76..1a364ffe3 100644 --- a/csharp/InnovEnergy.sln +++ b/csharp/InnovEnergy.sln @@ -81,6 +81,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Explorer", "App\S3Explore EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VrmGrabber", "App\VrmGrabber\VrmGrabber.csproj", "{88633C71-D701-49B3-A6DE-9D7CED9046E3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mailer", "Lib\Mailer\Mailer.csproj", "{73B97F6E-2BDC-40DA-84A7-7FB0264387D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -208,6 +210,10 @@ Global {88633C71-D701-49B3-A6DE-9D7CED9046E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {88633C71-D701-49B3-A6DE-9D7CED9046E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {88633C71-D701-49B3-A6DE-9D7CED9046E3}.Release|Any CPU.Build.0 = Release|Any CPU + {73B97F6E-2BDC-40DA-84A7-7FB0264387D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73B97F6E-2BDC-40DA-84A7-7FB0264387D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73B97F6E-2BDC-40DA-84A7-7FB0264387D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73B97F6E-2BDC-40DA-84A7-7FB0264387D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} @@ -244,5 +250,6 @@ Global {1391165D-51F1-45B4-8B7F-042A20AA0277} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} {EB56EF94-D8A7-4111-A8E7-A87EF13596DA} = {145597B4-3E30-45E6-9F72-4DD43194539A} {88633C71-D701-49B3-A6DE-9D7CED9046E3} = {145597B4-3E30-45E6-9F72-4DD43194539A} + {73B97F6E-2BDC-40DA-84A7-7FB0264387D6} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854} EndGlobalSection EndGlobal diff --git a/csharp/Lib/Mailer/Mailer.cs b/csharp/Lib/Mailer/Mailer.cs new file mode 100644 index 000000000..bb3659b54 --- /dev/null +++ b/csharp/Lib/Mailer/Mailer.cs @@ -0,0 +1,55 @@ +using MailKit.Net.Smtp; +using MimeKit; + +namespace InnovEnergy.Lib.Mailer; + + +public class Mailer +{ + private static MimeMessage Email = new(); + + public MimeMessage To(String name, String emailAddress) + { + Email.To.Add(new MailboxAddress(name, emailAddress)); + return Email; + } + + public MimeMessage From(String name, String emailAddress) + { + Email.From.Add(new MailboxAddress(name, emailAddress)); + return Email; + } + + public MimeMessage Subject(String subjectText) + { + Email.Subject = subjectText; + return Email; + } + + public MimeMessage Body(String bodyText) + { + Email.Body = new TextPart(MimeKit.Text.TextFormat.Plain) + { + Text = bodyText + }; + return Email; + } + + public Boolean SendEmailUsingSmtpConfig(SmtpConfig config) + { + try{ + using var smtp = new SmtpClient(); + smtp.Connect(config.Url, config.Port, false); + + smtp.Authenticate(config.Username, config.Password); + + smtp.Send(Email); + smtp.Disconnect(true); + } + catch (Exception) + { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/csharp/Lib/Mailer/Mailer.csproj b/csharp/Lib/Mailer/Mailer.csproj new file mode 100644 index 000000000..bebf4a19d --- /dev/null +++ b/csharp/Lib/Mailer/Mailer.csproj @@ -0,0 +1,10 @@ + + + + InnovEnergy.Lib.Mailer + + + + + + diff --git a/csharp/App/Backend/Mailer/SmptConfig.cs b/csharp/Lib/Mailer/SmtpConfig.cs similarity index 81% rename from csharp/App/Backend/Mailer/SmptConfig.cs rename to csharp/Lib/Mailer/SmtpConfig.cs index 02b63653a..22fdb84bd 100644 --- a/csharp/App/Backend/Mailer/SmptConfig.cs +++ b/csharp/Lib/Mailer/SmtpConfig.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; +namespace InnovEnergy.Lib.Mailer; -namespace InnovEnergy.App.Backend.Mailer; [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] -public class SmptConfig +public class SmtpConfig { public String Url { get; init; } = null!; public String Username { get; init; } = null!; public String Password { get; init; } = null!; public Int32 Port { get; init; } = 587; - } \ No newline at end of file diff --git a/typescript/Frontend/package.json b/typescript/Frontend/package.json index 97d6cfe9c..6e32e3d2f 100644 --- a/typescript/Frontend/package.json +++ b/typescript/Frontend/package.json @@ -35,6 +35,7 @@ "yup": "^1.1.0" }, "scripts": { + "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", diff --git a/typescript/Frontend/src/App.tsx b/typescript/Frontend/src/App.tsx index ea3ca8592..e7216a7c5 100644 --- a/typescript/Frontend/src/App.tsx +++ b/typescript/Frontend/src/App.tsx @@ -1,11 +1,11 @@ import useToken from "./hooks/useToken"; import Login from "./Login"; -import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom"; -import { Container, Grid, Box } from "@mui/material"; +import {Box, Container, Grid} from "@mui/material"; import routes from "./routes.json"; -import { IntlProvider } from "react-intl"; -import { useContext, useState } from "react"; +import {IntlProvider} from "react-intl"; +import {useContext, useState} from "react"; import en from "./lang/en.json"; import de from "./lang/de.json"; import fr from "./lang/fr.json"; @@ -14,10 +14,10 @@ import LogoutButton from "./components/Layout/LogoutButton"; import Users from "./components/Users/Users"; import NavigationTabs from "./components/Layout/NavigationTabs"; import InstallationPage from "./components/Installations/InstallationPage"; -import { UserContext } from "./components/Context/UserContextProvider"; +import {UserContext} from "./components/Context/UserContextProvider"; import ResetPassword from "./ResetPassword"; import innovenergyLogo from "./resources/innoveng_logo_on_orange.png"; -import { colors } from "./index"; +import {colors} from "./index"; const App = () => { const { token, setToken, removeToken } = useToken(); @@ -51,8 +51,10 @@ const App = () => { > - - innovenergy logo + + + innovenergy logo + void; fetchData: (timestamp: UnixTime) => Promise>; } - + export const S3CredentialsContext = createContext({ s3Credentials: {} as I_S3Credentials, @@ -27,7 +27,7 @@ const S3CredentialsContextProvider = ({ const [s3Credentials, setS3Credentials] = useState(); const saveS3Credentials = (credentials: I_S3Credentials, id: string) => { - const s3Bucket = id + "-3e5b3069-214a-43ee-8d85-57d72000c10d"; + const s3Bucket = id + "-3e5b3069-214a-43ee-8d85-57d72000c19d"; setS3Credentials({ s3Bucket, ...credentials }); }; diff --git a/typescript/Frontend/src/components/Context/UserContextProvider.tsx b/typescript/Frontend/src/components/Context/UserContextProvider.tsx index 2a1ea29b5..f7a83464c 100644 --- a/typescript/Frontend/src/components/Context/UserContextProvider.tsx +++ b/typescript/Frontend/src/components/Context/UserContextProvider.tsx @@ -1,6 +1,7 @@ import { createContext, ReactNode, useState } from "react"; import { I_User } from "../../util/user.util"; + interface I_InstallationContextProviderProps { currentUser?: I_User; setCurrentUser: (value: I_User) => void; diff --git a/typescript/Frontend/src/components/Installations/Detail/InstallationForm.tsx b/typescript/Frontend/src/components/Installations/Detail/InstallationForm.tsx index e780ab854..307ab6118 100644 --- a/typescript/Frontend/src/components/Installations/Detail/InstallationForm.tsx +++ b/typescript/Frontend/src/components/Installations/Detail/InstallationForm.tsx @@ -33,7 +33,7 @@ const InstallationForm = (props: I_InstallationFormProps) => { const readOnly = !getCurrentUser().hasWriteAccess; const intl = useIntl(); - + const validationSchema = Yup.object().shape({ name: Yup.string().required( intl.formatMessage({ @@ -53,6 +53,19 @@ const InstallationForm = (props: I_InstallationFormProps) => { defaultMessage: "Location is required", }) ), + country: Yup.string().required( + intl.formatMessage({ + id: "requiredCountry", + defaultMessage: "Country is required", + }) + ), + orderNumbers: Yup.string().required( + intl.formatMessage({ + id: "requiredOrderNumber", + defaultMessage: "Order Number is required", + }) + ), + }); const formik = useFormik({ @@ -64,6 +77,12 @@ const InstallationForm = (props: I_InstallationFormProps) => { orderNumbers: values.orderNumbers, }, onSubmit: (formikValues) => { + /*const updatedValues = { + ...formikValues, + + orderNumbers: formikValues.orderNumbers.split(','), + };*/ + handleSubmit(values, formikValues) .then(() => { setOpen(true); @@ -164,12 +183,22 @@ const InstallationForm = (props: I_InstallationFormProps) => { additionalButtons.map((button) => button)} {!readOnly && ( + + )} + {!readOnly && ( + + + + )} { ); }; -export default Installations; +export default Installations; \ No newline at end of file diff --git a/typescript/Frontend/src/components/Layout/NavigationTabs.tsx b/typescript/Frontend/src/components/Layout/NavigationTabs.tsx index b74f31a0d..bf8de966f 100644 --- a/typescript/Frontend/src/components/Layout/NavigationTabs.tsx +++ b/typescript/Frontend/src/components/Layout/NavigationTabs.tsx @@ -16,12 +16,7 @@ const NavigationTabs = () => { return ( <> diff --git a/typescript/Frontend/src/lang/en.json b/typescript/Frontend/src/lang/en.json index 3cbbc6634..2751313ca 100644 --- a/typescript/Frontend/src/lang/en.json +++ b/typescript/Frontend/src/lang/en.json @@ -2,6 +2,7 @@ "liveView": "Live view", "allInstallations": "All installations", "applyChanges": "Apply changes", + "deleteInstallation": "Delete Installation", "country": "Country", "customerName": "Customer name", "english": "English", @@ -44,6 +45,7 @@ "requiredLocation": "Location is required", "requiredName": "Name is required", "requiredRegion": "Region is required", + "requiredOrderNumber": "Required Order Number", "submit": "Submit", "user": "User", "userTabs": "user tabs" diff --git a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx new file mode 100644 index 000000000..7406652c7 --- /dev/null +++ b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx @@ -0,0 +1,26 @@ +import axios from 'axios'; + +export const axiosConfigWithoutToken = axios.create({ + baseURL: 'https://localhost:7087/api' +}); + +const axiosConfig = axios.create({ + baseURL: 'https://localhost:7087/api' +}); + +axiosConfig.defaults.params = {}; +axiosConfig.interceptors.request.use( + (config) => { + const tokenString = localStorage.getItem('token'); + const token = tokenString !== null ? tokenString : ''; + if (token) { + config.params['authToken'] = token; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +export default axiosConfig; diff --git a/typescript/frontend-marios2/src/Resources/innoveng_logo_on_orange.png b/typescript/frontend-marios2/src/Resources/innoveng_logo_on_orange.png new file mode 100644 index 000000000..4f1714ecd Binary files /dev/null and b/typescript/frontend-marios2/src/Resources/innoveng_logo_on_orange.png differ diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx new file mode 100644 index 000000000..452f81c9c --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { + Card, + Divider, + Grid, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme +} from '@mui/material'; +import { InnovEnergyUser } from 'src/interfaces/UserTypes'; +import User from './User'; + +interface FlatUsersViewProps { + users: InnovEnergyUser[]; + fetchDataAgain: () => void; +} + +const FlatUsersView = (props: FlatUsersViewProps) => { + const [selectedUser, setSelectedUser] = useState(-1); + const selectedBulkActions = selectedUser !== -1; + + const handleSelectOneUser = (installationID: number): void => { + if (selectedUser != installationID) { + setSelectedUser(installationID); + } else { + setSelectedUser(-1); + } + }; + + const theme = useTheme(); + const [isRowHovered, setHoveredRow] = useState(-1); + + const handleRowMouseEnter = (id: number) => { + setHoveredRow(id); + }; + + const handleRowMouseLeave = () => { + setHoveredRow(-1); + }; + const findUser = (id: number) => { + return props.users.find((user) => user.id === id); + }; + + return ( + + + + + + + + + + Username + Email + + + + {props.users.map((user) => { + const isInstallationSelected = user.id === selectedUser; + const rowStyles = + isRowHovered === user.id + ? { + cursor: 'pointer', + backgroundColor: theme.colors.primary.lighter // Set your desired hover background color here + } + : {}; + + return ( + handleSelectOneUser(user.id)} + style={rowStyles} + onMouseEnter={() => handleRowMouseEnter(user.id)} + onMouseLeave={() => handleRowMouseLeave()} + > + + + + {user.name} + + + + + {user.email} + + + + ); + })} + +
+
+
+
+ + {selectedBulkActions && ( + + )} +
+ ); +}; + +export default FlatUsersView; diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx new file mode 100644 index 000000000..3ae60674a --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx @@ -0,0 +1,90 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { + FormControl, + Grid, + InputAdornment, + TextField, + useTheme +} from '@mui/material'; +import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone'; +import FlatUsersView from './FlatUsersView'; +import { UsersContext } from '../../../contexts/UsersContextProvider'; +import Button from '@mui/material/Button'; +import UserForm from './userForm'; +import { UserContext } from '../../../contexts/userContext'; + +function UsersSearch() { + const theme = useTheme(); + const [searchTerm, setSearchTerm] = useState(''); + const { availableUsers, fetchAvailableUsers } = useContext(UsersContext); + const [filteredData, setFilteredData] = useState(availableUsers); + const [openModal, setOpenModal] = useState(false); + const context = useContext(UserContext); + const { currentUser, setUser } = context; + + useEffect(() => { + fetchAvailableUsers(); + }, []); + + const fetchDataAgain = () => { + fetchAvailableUsers(); + }; + + useEffect(() => { + const filtered = availableUsers.filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + setFilteredData(filtered); + }, [searchTerm, availableUsers]); + + const handleSubmit = () => { + setOpenModal(true); + }; + const handleUserFormSubmit = () => { + setOpenModal(false); + fetchAvailableUsers(); + }; + + const handleUserFormCancel = () => { + setOpenModal(false); + }; + + return ( + <> + + + {currentUser.hasWriteAccess && ( + + )} + + + {openModal && ( + + )} + + + + setSearchTerm(e.target.value)} + fullWidth + InputProps={{ + startAdornment: ( + + + + ) + }} + /> + + + + + + ); +} + +export default UsersSearch; diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/index.tsx new file mode 100644 index 000000000..5980287f0 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Users/index.tsx @@ -0,0 +1,25 @@ +import Footer from 'src/components/Footer'; +import { Box, Container, Grid, useTheme } from '@mui/material'; +import UsersSearch from './UsersSearch'; +import UsersContextProvider from 'src/contexts/UsersContextProvider'; + +function Users() { + const theme = useTheme(); + + return ( + <> + + + + + + + + +