From 6e7d337d924f02a9e5662199ee6a980ef45e80d7 Mon Sep 17 00:00:00 2001 From: ig Date: Thu, 26 Oct 2023 12:38:37 +0200 Subject: [PATCH 1/7] Add Unique constraint to email of users --- csharp/App/Backend/DataTypes/User.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/csharp/App/Backend/DataTypes/User.cs b/csharp/App/Backend/DataTypes/User.cs index 7397eb6e2..77924b3de 100644 --- a/csharp/App/Backend/DataTypes/User.cs +++ b/csharp/App/Backend/DataTypes/User.cs @@ -4,6 +4,8 @@ namespace InnovEnergy.App.Backend.DataTypes; public class User : TreeNode { + + [Unique] public String Email { get; set; } = null!; public Boolean HasWriteAccess { get; set; } = false; public Boolean MustResetPassword { get; set; } = false; From 76099131c2b40dffe5e76d0c8b9b1c64f74a7f40 Mon Sep 17 00:00:00 2001 From: ig Date: Thu, 26 Oct 2023 14:09:38 +0200 Subject: [PATCH 2/7] Fix multiple reset password emails Bug --- csharp/App/Backend/Backend.csproj | 200 +------------------ csharp/App/Backend/Controller.cs | 58 +++--- csharp/App/Backend/DataTypes/Methods/User.cs | 65 ++++-- csharp/App/Backend/Database/Db.cs | 88 ++++---- csharp/App/Backend/Database/Read.cs | 5 +- csharp/App/Backend/Email/Email.cs | 83 -------- csharp/App/Backend/Program.cs | 5 +- csharp/App/S3Explorer/S3Explorer.csproj | 2 +- csharp/Lib/Mailer/Mailer.cs | 66 +++--- csharp/Lib/Mailer/Mailer.csproj | 12 ++ csharp/Lib/Mailer/MailerConfig.cs | 17 ++ csharp/Lib/Mailer/MailerConfig.json | 9 + csharp/Lib/Mailer/SmtpConfig.cs | 12 -- csharp/Lib/S3Utils/S3.cs | 8 +- 14 files changed, 193 insertions(+), 437 deletions(-) delete mode 100644 csharp/App/Backend/Email/Email.cs create mode 100644 csharp/Lib/Mailer/MailerConfig.cs create mode 100644 csharp/Lib/Mailer/MailerConfig.json delete mode 100644 csharp/Lib/Mailer/SmtpConfig.cs diff --git a/csharp/App/Backend/Backend.csproj b/csharp/App/Backend/Backend.csproj index fc491daee..bbbac1191 100644 --- a/csharp/App/Backend/Backend.csproj +++ b/csharp/App/Backend/Backend.csproj @@ -7,6 +7,7 @@ + @@ -34,213 +35,20 @@ - + PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Never + diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 0c4af1a87..b9b5d3878 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices.ComTypes; -using System.Text.Json.Nodes; using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; @@ -21,17 +19,12 @@ public class Controller : ControllerBase var user = Db.GetUserByEmail(username); if (user is null) - { - throw new Exceptions(400,"Null User Exception", "Must provide a user to log in as.", Request.Path.Value!); - } + throw new Exceptions(400, "Null User Exception", "Must provide a user to log in as.", Request.Path.Value!); - if (!(user.Password.IsNullOrEmpty() && user.MustResetPassword)) + if (!(user.Password.IsNullOrEmpty() && user.MustResetPassword) && !user.VerifyPassword(password)) { - if (!user.VerifyPassword(password)) - { - //return Unauthorized("No Password set"); - throw new Exceptions(401,"Wrong Password Exception", "Please try again.", Request.Path.Value!); - } + //return Unauthorized("No Password set"); + throw new Exceptions(401, "Wrong Password Exception", "Please try again.", Request.Path.Value!); } var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user)); @@ -67,7 +60,9 @@ public class Controller : ControllerBase if (user is null || !session.HasAccessTo(user)) return Unauthorized(); - return user.HidePassword().HideParentIfUserHasNoAccessToParent(session); + return user + .HidePassword() + .HideParentIfUserHasNoAccessToParent(session); } @@ -102,10 +97,10 @@ public class Controller : ControllerBase return Unauthorized(); return installation - .UsersWithDirectAccess() - .Where(u => u.IsDescendantOf(user)) - .Select(u => u.HidePassword()) - .ToList(); + .UsersWithDirectAccess() + .Where(u => u.IsDescendantOf(user)) + .Select(u => u.HidePassword()) + .ToList(); } [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))] @@ -121,11 +116,11 @@ public class Controller : ControllerBase return Unauthorized(); return installation - .Ancestors() - .SelectMany(f => f.UsersWithDirectAccess() - .Where(u => u.IsDescendantOf(user)) - .Select(u => new { folderId = f.Id, folderName = f.Name, user = u.HidePassword() })) - .ToList(); + .Ancestors() + .SelectMany(f => f.UsersWithDirectAccess() + .Where(u => u.IsDescendantOf(user)) + .Select(u => new { folderId = f.Id, folderName = f.Name, user = u.HidePassword() })) + .ToList(); } [HttpGet(nameof(GetUsersWithDirectAccessToFolder))] @@ -255,11 +250,11 @@ public class Controller : ControllerBase [HttpPost(nameof(CreateUser))] - public ActionResult CreateUser([FromBody] User newUser, Token authToken) + public async Task> CreateUser([FromBody] User newUser, Token authToken) { var create = Db.GetSession(authToken).Create(newUser); - return create && Db.SendNewUserEmail(newUser) + return create && await Db.SendNewUserEmail(newUser) ? newUser.HidePassword() : Unauthorized() ; } @@ -326,8 +321,8 @@ public class Controller : ControllerBase var user = Db.GetUserById(installationAccess.UserId); return session.GrantUserAccessTo(user, installation) - ? Ok() - : Unauthorized(); + ? Ok() + : Unauthorized(); } [HttpPost(nameof(RevokeUserAccessToInstallation))] @@ -461,18 +456,19 @@ public class Controller : ControllerBase } [HttpPost(nameof(ResetPasswordRequest))] - public ActionResult> ResetPasswordRequest(String username) + public async Task>> ResetPasswordRequest(String email) { - var user = Db.GetUserByEmail(username); + var user = Db.GetUserByEmail(email); if (user is null) return Unauthorized(); var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user)); - var res = Db.Create(session); - return res && Db.SendPasswordResetEmail(user, session.Token) - ? Ok() - : Unauthorized(); + var success = Db.Create(session); + + return success && await Db.SendPasswordResetEmail(user, session.Token) + ? Ok() + : Unauthorized(); } diff --git a/csharp/App/Backend/DataTypes/Methods/User.cs b/csharp/App/Backend/DataTypes/Methods/User.cs index c3beece1a..b85020cfe 100644 --- a/csharp/App/Backend/DataTypes/Methods/User.cs +++ b/csharp/App/Backend/DataTypes/Methods/User.cs @@ -1,6 +1,6 @@ -using System.Net.Mail; using System.Security.Cryptography; using InnovEnergy.App.Backend.Database; +using InnovEnergy.Lib.Mailer; using InnovEnergy.Lib.Utils; using Convert = System.Convert; using static System.Text.Encoding; @@ -91,13 +91,14 @@ public static class UserMethods .Skip(1); // skip self } - public static Boolean VerifyPassword(this User user, String password) + public static Boolean VerifyPassword(this User user, String? password) { - return Db.GetUserByEmail(user.Email)?.Password == user.SaltAndHashPassword(password); + return password is not null + && Db.GetUserByEmail(user.Email)?.Password == user.SaltAndHashPassword(password); } - public static String? SaltAndHashPassword(this User user, String password) + public static String SaltAndHashPassword(this User user, String password) { var dataToHash = $"{password}{user.Salt()}"; @@ -150,8 +151,10 @@ public static class UserMethods if (installation is null) return false; - return user.HasDirectAccessTo(installation) || - installation.Ancestors().Any(user.HasDirectAccessTo); + return user.HasDirectAccessTo(installation) + || installation + .Ancestors() + .Any(user.HasDirectAccessTo); } public static Boolean HasAccessTo(this User user, User? other) @@ -159,10 +162,8 @@ public static class UserMethods if (other is null) return false; - if (other.Id == user.Id) - return true; - - return other + return other.Id == user.Id + || other .Ancestors() .Contains(user); } @@ -172,9 +173,9 @@ public static class UserMethods return other?.Type switch { "installation" => user.HasAccessTo((Installation)other), - "user" => user.HasAccessTo((User)other), - "folder" => user.HasAccessTo((Folder)other), - _ => false + "user" => user.HasAccessTo((User)other), + "folder" => user.HasAccessTo((Folder)other), + _ => false }; } @@ -183,13 +184,11 @@ public static class UserMethods return other?.Type switch { "Installation" => user.HasAccessTo(Db.GetFolderById(other.ParentId)), - "User" => user.HasAccessTo(Db.GetUserById(other.ParentId)), - "Folder" => user.HasAccessTo(Db.GetFolderById(other.ParentId)), - _ => false + "User" => user.HasAccessTo(Db.GetUserById(other.ParentId)), + "Folder" => user.HasAccessTo(Db.GetFolderById(other.ParentId)), + _ => false }; } - - private static String Salt(this User user) { @@ -211,4 +210,34 @@ public static class UserMethods return user; } + public static Task SendEmail(this User user, String subject, String body) + { + return Mailer.Send(user.Name, user.Email, subject, body); + } + + public static Task SendPasswordResetEmail(this User user, String token) + { + const String subject = "Reset the password of your InnovEnergy-Account"; + const String resetLink = "https://monitor.innov.energy/api/ResetPassword"; // TODO: move to settings file + + var body = $"Dear {user.Name}\n" + + $"To reset your password " + + $"please open this link:{resetLink}?token={token}"; + + return user.SendEmail(subject, body); + } + + public static Task SendNewUserWelcomeMessage(this User user) + { + const String subject = "Your new InnovEnergy-Account"; + + var resetLink = $"https://monitor.innov.energy/?username={user.Email}"; // TODO: move to settings file + + var body = $"Dear {user.Name}\n" + + $"To set your password and log in to your " + + $"Innovenergy-Account open this link:{resetLink}"; + + return user.SendEmail(subject, body); + } + } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 425389706..b8d86c071 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -1,12 +1,11 @@ using System.Reactive.Concurrency; using System.Reactive.Linq; -using CliWrap; -using CliWrap.Buffered; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; using InnovEnergy.Lib.S3Utils; using InnovEnergy.Lib.S3Utils.DataTypes; +using InnovEnergy.Lib.Utils; using SQLite; using SQLiteConnection = SQLite.SQLiteConnection; @@ -38,35 +37,15 @@ public static partial class Db memoryConnection.CreateTable(); memoryConnection.CreateTable(); memoryConnection.CreateTable(); + + fileConnection.Table ().ForEach(memoryConnection.Insert); + fileConnection.Table ().ForEach(memoryConnection.Insert); + fileConnection.Table ().ForEach(memoryConnection.Insert); + fileConnection.Table ().ForEach(memoryConnection.Insert); + fileConnection.Table ().ForEach(memoryConnection.Insert); + fileConnection.Table ().ForEach(memoryConnection.Insert); + fileConnection.Table().ForEach(memoryConnection.Insert); - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } - foreach (var obj in fileConnection.Table()) - { - memoryConnection.Insert(obj); - } return memoryConnection; }))(); @@ -152,34 +131,53 @@ public static partial class Db private static async Task UpdateS3Urls() { var regions = Installations - .Select(i => i.S3Region) - .Distinct().ToList(); + .Select(i => i.S3Region) + .Distinct() + .ToList(); + const String provider = "exo.io"; + foreach (var region in regions) { - var bucketList = await new S3Region($"https://{region}.{provider}", ExoCmd.S3Creds!).ListAllBuckets(); + var s3Region = new S3Region($"https://{region}.{provider}", ExoCmd.S3Creds!); + var bucketList = await s3Region.ListAllBuckets(); - foreach (var bucket in bucketList.Buckets) + var installations = from bucket in bucketList.Buckets + from installation in Installations + where installation.BucketName() == bucket.BucketName + select installation; + + foreach (var installation in installations) { - foreach (var installation in Installations) - { - if (installation.BucketName() == bucket.BucketName) - { - await installation.RenewS3Credentials(); - } - } + await installation.RenewS3Credentials(); } } } - public static Boolean SendPasswordResetEmail(User user, String sessionToken) + public static async Task SendPasswordResetEmail(User user, String sessionToken) { - return Email.Email.SendPasswordResetMessage(user, sessionToken); + try + { + await user.SendPasswordResetEmail(sessionToken); + return true; + } + catch + { + return false; + } } - public static Boolean SendNewUserEmail(User user) + public static async Task SendNewUserEmail(User user) { - return Email.Email.SendNewUserMessage(user); + try + { + await user.SendNewUserWelcomeMessage(); + return true; + } + catch + { + return false; + } } public static Boolean DeleteUserPassword(User user) diff --git a/csharp/App/Backend/Database/Read.cs b/csharp/App/Backend/Database/Read.cs index dcaa0e90d..14758c1b1 100644 --- a/csharp/App/Backend/Database/Read.cs +++ b/csharp/App/Backend/Database/Read.cs @@ -1,5 +1,4 @@ using InnovEnergy.App.Backend.DataTypes; -using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; @@ -26,10 +25,10 @@ public static partial class Db .FirstOrDefault(u => u.Id == id); } - public static User? GetUserByEmail(String userName) + public static User? GetUserByEmail(String email) { return Users - .FirstOrDefault(u => u.Email == userName); + .FirstOrDefault(u => u.Email == email); } public static Session? GetSession(String token) diff --git a/csharp/App/Backend/Email/Email.cs b/csharp/App/Backend/Email/Email.cs deleted file mode 100644 index c1c466f39..000000000 --- a/csharp/App/Backend/Email/Email.cs +++ /dev/null @@ -1,83 +0,0 @@ -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; - } - } - - - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public static Boolean SendNewUserMessage (User emailRecipientUser) - { - var config = JsonSerializer.Deserialize(File.OpenRead("./Resources/smtpConfig.json"))!; - - //todo am I right? - var resetLink = $"https://monitor.innov.energy/?username={emailRecipientUser.Email}"; - var mailer = new Mailer(); - - try - { - - mailer.From("InnovEnergy", "noreply@innov.energy"); - mailer.To(emailRecipientUser.Name, emailRecipientUser.Email); - - mailer.Subject("Your new Innovenergy-Account"); - mailer.Body("Dear " + emailRecipientUser.Name - + "\n To set your password and log in to your Innovenergy-Account open this link:" - + resetLink); - - return mailer.SendEmailUsingSmtpConfig(config); - } - catch (Exception) - { - return false; - } - } - } diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index f39e5e463..455a9ba8b 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -3,7 +3,6 @@ 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; @@ -20,7 +19,7 @@ public static class Program builder.Services.AddProblemDetails(setup => { //This includes the stacktrace in Development Env - setup.IncludeExceptionDetails = (ctx, env) => builder.Environment.IsDevelopment() || builder.Environment.IsStaging(); + setup.IncludeExceptionDetails = (_, _) => builder.Environment.IsDevelopment() || builder.Environment.IsStaging(); //This handles our Exceptions setup.Map(exception => new ProblemDetails @@ -43,8 +42,6 @@ public static class Program app.Use(async (context, next) => { - var x = 2; - context.Request.WriteLine(); await next(context); diff --git a/csharp/App/S3Explorer/S3Explorer.csproj b/csharp/App/S3Explorer/S3Explorer.csproj index a365244ed..920519e5e 100644 --- a/csharp/App/S3Explorer/S3Explorer.csproj +++ b/csharp/App/S3Explorer/S3Explorer.csproj @@ -14,7 +14,7 @@ - + diff --git a/csharp/Lib/Mailer/Mailer.cs b/csharp/Lib/Mailer/Mailer.cs index bb3659b54..74781ce14 100644 --- a/csharp/Lib/Mailer/Mailer.cs +++ b/csharp/Lib/Mailer/Mailer.cs @@ -1,55 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using MailKit.Net.Smtp; using MimeKit; namespace InnovEnergy.Lib.Mailer; -public class Mailer +public static class Mailer { - private static MimeMessage Email = new(); - - public MimeMessage To(String name, String emailAddress) + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public static async Task Send(String recipientName, String recipientEmailAddress, String subject, String body) { - 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) + var config = await ReadMailerConfig(); + + var from = new MailboxAddress(config!.SenderName, config.SenderAddress); + var to = new MailboxAddress(recipientName, recipientEmailAddress); + + var msg = new MimeMessage { - Text = bodyText + From = { from }, + To = { to }, + Subject = subject, + Body = new TextPart { Text = body } }; - return Email; + + using var smtp = new SmtpClient(); + + await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, false); + await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword); + await smtp.SendAsync(msg); + await smtp.DisconnectAsync(true); } - public Boolean SendEmailUsingSmtpConfig(SmtpConfig config) + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.DeserializeAsync(Stream, JsonSerializerOptions, CancellationToken)")] + private static async Task ReadMailerConfig() { - 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; + await using var fileStream = File.OpenRead(MailerConfig.DefaultFile); + return await JsonSerializer.DeserializeAsync(fileStream); } } \ No newline at end of file diff --git a/csharp/Lib/Mailer/Mailer.csproj b/csharp/Lib/Mailer/Mailer.csproj index bebf4a19d..2f74d8390 100644 --- a/csharp/Lib/Mailer/Mailer.csproj +++ b/csharp/Lib/Mailer/Mailer.csproj @@ -7,4 +7,16 @@ + + + + true + PreserveNewest + PreserveNewest + + + + + + diff --git a/csharp/Lib/Mailer/MailerConfig.cs b/csharp/Lib/Mailer/MailerConfig.cs new file mode 100644 index 000000000..68e611595 --- /dev/null +++ b/csharp/Lib/Mailer/MailerConfig.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +namespace InnovEnergy.Lib.Mailer; + + +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +public class MailerConfig +{ + public required String SmtpServerUrl { get; init; } + public required String SmtpUsername { get; init; } + public required String SmtpPassword { get; init; } + public UInt16 SmtpPort { get; init; } = 587; + + public required String SenderName { get; init; } + public required String SenderAddress { get; init; } + + public const String DefaultFile = $"{nameof(MailerConfig)}.json"; +} \ No newline at end of file diff --git a/csharp/Lib/Mailer/MailerConfig.json b/csharp/Lib/Mailer/MailerConfig.json new file mode 100644 index 000000000..a7a30b2eb --- /dev/null +++ b/csharp/Lib/Mailer/MailerConfig.json @@ -0,0 +1,9 @@ +{ + "SmtpServerUrl" : "mail.agenturserver.de", + "SmtpUsername" : "p518526p69", + "SmtpPassword" : "i;b*xqm4iB5uhl", + "SmtpPort" : 587, + "SenderName" : "InnovEnergy", + "SenderAddress" : "noreply@innov.energy" +} + diff --git a/csharp/Lib/Mailer/SmtpConfig.cs b/csharp/Lib/Mailer/SmtpConfig.cs deleted file mode 100644 index 22fdb84bd..000000000 --- a/csharp/Lib/Mailer/SmtpConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -namespace InnovEnergy.Lib.Mailer; - - -[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] -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/csharp/Lib/S3Utils/S3.cs b/csharp/Lib/S3Utils/S3.cs index 71decbd73..b8d2db382 100644 --- a/csharp/Lib/S3Utils/S3.cs +++ b/csharp/Lib/S3Utils/S3.cs @@ -42,11 +42,11 @@ public static class S3 .Select(o => new S3Url(o.Key, bucket)); } - public static async Task ListAllBuckets(this S3Region region) + public static Task ListAllBuckets(this S3Region region) { - return await region - .GetS3Client() - .ListBucketsAsync(); + return region + .GetS3Client() + .ListBucketsAsync(); } public static Task PutObject(this S3Url path, String data, Encoding encoding) => path.PutObject(encoding.GetBytes(data)); From 0a91445ddd67129764a8ba31516e938a2c29e5a0 Mon Sep 17 00:00:00 2001 From: Noe Date: Thu, 26 Oct 2023 16:38:37 +0200 Subject: [PATCH 3/7] Fixed mail bug with static variable, fixed token encoding --- csharp/App/Backend/Controller.cs | 6 +- csharp/App/Backend/DataTypes/Methods/User.cs | 6 +- .../Backend/Properties/launchSettings.json | 2 +- typescript/frontend-marios2/package-lock.json | 107 ++- typescript/frontend-marios2/package.json | 4 +- typescript/frontend-marios2/src/App.tsx | 120 ++- .../src/Resources/formatPower.tsx | 2 +- .../src/Resources/routes.json | 13 +- .../src/components/ForgotPassword.tsx | 16 +- .../src/components/ResetPassword.tsx | 2 +- .../frontend-marios2/src/components/login.tsx | 38 +- .../Configuration/Configuration.tsx | 43 +- .../Installations/FlatInstallationView.tsx | 19 +- .../dashboards/Installations/Installation.tsx | 742 ++++++++++-------- .../Installations/InstallationSearch.tsx | 26 +- .../dashboards/Installations/flatView.tsx | 19 - .../dashboards/Installations/index.tsx | 304 ++++++- .../Installations/installationForm.tsx | 6 +- .../src/content/dashboards/Log/Log.tsx | 21 +- .../src/content/dashboards/Log/graph.util.tsx | 48 +- .../dashboards/Overview/chartOptions.tsx | 71 +- .../content/dashboards/Overview/overview.tsx | 346 ++++++-- .../content/dashboards/Topology/Topology.tsx | 61 +- .../dashboards/Topology/dotsAnimation.css | 32 +- .../dashboards/Topology/topologyBox.tsx | 74 +- .../dashboards/Topology/topologyColumn.tsx | 4 +- .../dashboards/Topology/topologyFlow.tsx | 22 +- .../dashboards/Tree/CustomTreeItem.tsx | 6 +- .../src/content/dashboards/Tree/Folder.tsx | 134 +++- .../dashboards/Tree/InstallationTree.tsx | 73 +- .../content/dashboards/Tree/folderForm.tsx | 4 +- .../src/content/dashboards/Tree/treeView.tsx | 5 +- .../dashboards/Users/FlatUsersView.tsx | 15 +- .../src/content/dashboards/Users/User.tsx | 121 ++- .../content/dashboards/Users/UsersSearch.tsx | 4 +- .../src/content/dashboards/Users/index.tsx | 1 + .../src/content/dashboards/Users/userForm.tsx | 6 +- .../contexts/InstallationsContextProvider.tsx | 6 +- .../src/contexts/LogContextProvider.tsx | 19 +- .../src/contexts/tokenContext.tsx | 6 +- .../frontend-marios2/src/dataCache/time.ts | 1 + .../src/interfaces/InstallationTypes.tsx | 3 +- typescript/frontend-marios2/src/lang/de.json | 71 +- typescript/frontend-marios2/src/lang/fr.json | 61 +- .../SidebarLayout/Header/Menu/index.tsx | 13 +- .../SidebarLayout/Header/Userbox/index.tsx | 3 +- .../layouts/SidebarLayout/Header/index.tsx | 40 +- .../Sidebar/SidebarMenu/index.tsx | 54 +- .../layouts/SidebarLayout/Sidebar/index.tsx | 60 +- typescript/frontend-marios2/src/routes.json | 28 +- 50 files changed, 1964 insertions(+), 924 deletions(-) delete mode 100644 typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index b9b5d3878..4763556a2 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -456,9 +456,9 @@ public class Controller : ControllerBase } [HttpPost(nameof(ResetPasswordRequest))] - public async Task>> ResetPasswordRequest(String email) + public async Task>> ResetPasswordRequest(String username) { - var user = Db.GetUserByEmail(email); + var user = Db.GetUserByEmail(username); if (user is null) return Unauthorized(); @@ -482,7 +482,7 @@ public class Controller : ControllerBase Db.DeleteUserPassword(user); - return Redirect($"https://monitor.innov.energy/?username={user.Email}&reset=true"); + return Redirect($"https://monnitor.innov.energy/?username={user.Email}&reset=true"); // TODO: move to settings file } } diff --git a/csharp/App/Backend/DataTypes/Methods/User.cs b/csharp/App/Backend/DataTypes/Methods/User.cs index b85020cfe..2467c7d3b 100644 --- a/csharp/App/Backend/DataTypes/Methods/User.cs +++ b/csharp/App/Backend/DataTypes/Methods/User.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using System.Web; using InnovEnergy.App.Backend.Database; using InnovEnergy.Lib.Mailer; using InnovEnergy.Lib.Utils; @@ -218,11 +219,12 @@ public static class UserMethods public static Task SendPasswordResetEmail(this User user, String token) { const String subject = "Reset the password of your InnovEnergy-Account"; - const String resetLink = "https://monitor.innov.energy/api/ResetPassword"; // TODO: move to settings file + const String resetLink = "https://monnitor.innov.energy/api/ResetPassword"; // TODO: move to settings file + var encodedToken = HttpUtility.UrlEncode(token); var body = $"Dear {user.Name}\n" + $"To reset your password " + - $"please open this link:{resetLink}?token={token}"; + $"please open this link:{resetLink}?token={encodedToken}"; return user.SendEmail(subject, body); } diff --git a/csharp/App/Backend/Properties/launchSettings.json b/csharp/App/Backend/Properties/launchSettings.json index 3bef649bb..760aaabab 100644 --- a/csharp/App/Backend/Properties/launchSettings.json +++ b/csharp/App/Backend/Properties/launchSettings.json @@ -7,7 +7,7 @@ "dotnetRunMessages": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7087", + "applicationUrl": "http://localhost:7087", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "HOME":"~/backend" diff --git a/typescript/frontend-marios2/package-lock.json b/typescript/frontend-marios2/package-lock.json index e573f61eb..fc39e5adb 100644 --- a/typescript/frontend-marios2/package-lock.json +++ b/typescript/frontend-marios2/package-lock.json @@ -21,7 +21,7 @@ "chart.js": "^4.4.0", "clsx": "1.1.1", "cytoscape": "^3.26.0", - "date-fns": "2.28.0", + "date-fns": "^2.28.0", "history": "5.3.0", "linq-to-typescript": "^11.0.0", "nprogress": "0.2.0", @@ -38,9 +38,11 @@ "react-icons": "^4.11.0", "react-icons-converter": "^1.1.4", "react-intl": "^6.4.4", + "react-redux": "^8.1.3", "react-router": "6.3.0", "react-router-dom": "6.3.0", "react-scripts": "5.0.1", + "redux": "^4.2.1", "rxjs": "^7.8.1", "simplytyped": "^3.3.0", "stylis": "4.1.1", @@ -4662,6 +4664,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -15155,6 +15162,49 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15334,6 +15384,14 @@ "node": "*" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -17185,6 +17243,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -21400,6 +21466,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -28886,6 +28957,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -29020,6 +29111,14 @@ } } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -30399,6 +30498,12 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/typescript/frontend-marios2/package.json b/typescript/frontend-marios2/package.json index d0c1bf1ae..23ef0f8a9 100644 --- a/typescript/frontend-marios2/package.json +++ b/typescript/frontend-marios2/package.json @@ -17,7 +17,7 @@ "chart.js": "^4.4.0", "clsx": "1.1.1", "cytoscape": "^3.26.0", - "date-fns": "2.28.0", + "date-fns": "^2.28.0", "history": "5.3.0", "linq-to-typescript": "^11.0.0", "nprogress": "0.2.0", @@ -34,9 +34,11 @@ "react-icons": "^4.11.0", "react-icons-converter": "^1.1.4", "react-intl": "^6.4.4", + "react-redux": "^8.1.3", "react-router": "6.3.0", "react-router-dom": "6.3.0", "react-scripts": "5.0.1", + "redux": "^4.2.1", "rxjs": "^7.8.1", "simplytyped": "^3.3.0", "stylis": "4.1.1", diff --git a/typescript/frontend-marios2/src/App.tsx b/typescript/frontend-marios2/src/App.tsx index 605cafc58..817713db7 100644 --- a/typescript/frontend-marios2/src/App.tsx +++ b/typescript/frontend-marios2/src/App.tsx @@ -1,4 +1,4 @@ -import { Navigate, Route, Routes } from 'react-router-dom'; +import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'; import { CssBaseline } from '@mui/material'; import ThemeProvider from './theme/ThemeProvider'; import React, { lazy, Suspense, useContext, useState } from 'react'; @@ -9,15 +9,16 @@ import en from './lang/en.json'; import de from './lang/de.json'; import fr from './lang/fr.json'; import SuspenseLoader from './components/SuspenseLoader'; -import { RouteObject } from 'react-router'; -import BaseLayout from './layouts/BaseLayout'; import SidebarLayout from './layouts/SidebarLayout'; import { TokenContext } from './contexts/tokenContext'; import ResetPassword from './components/ResetPassword'; -import ForgotPassword from './components/ForgotPassword'; import InstallationTabs from './content/dashboards/Installations/index'; import routes from 'src/Resources/routes.json'; import './App.css'; +import ForgotPassword from './components/ForgotPassword'; +import { axiosConfigWithoutToken } from './Resources/axiosConfig'; +import UsersContextProvider from './contexts/UsersContextProvider'; +import InstallationsContextProvider from './contexts/InstallationsContextProvider'; function App() { const context = useContext(UserContext); @@ -25,6 +26,9 @@ function App() { const tokencontext = useContext(TokenContext); const { token, setNewToken, removeToken } = tokencontext; const [forgotPassword, setForgotPassword] = useState(false); + const navigate = useNavigate(); + const searchParams = new URLSearchParams(location.search); + const username = searchParams.get('username'); const [language, setLanguage] = useState('en'); const getTranslations = () => { @@ -62,9 +66,26 @@ function App() { lazy(() => import('src/components/ResetPassword')) ); + const SetNewPassword = Loader( + lazy(() => import('src/components/SetNewPassword')) + ); + const Login = Loader(lazy(() => import('src/components/login'))); const Users = Loader(lazy(() => import('src/content/dashboards/Users'))); + const loginToResetPassword = () => { + axiosConfigWithoutToken + .post('/Login', null, { params: { username, password: '' } }) + .then((response) => { + if (response.data && response.data.token) { + setNewToken(response.data.token); + setUser(response.data.user); + navigate(routes.installations); + } + }) + .catch((error) => {}); + }; + // Status const Status404 = Loader( lazy(() => import('src/content/pages/Status/Status404')) @@ -79,61 +100,25 @@ function App() { lazy(() => import('src/content/pages/Status/Maintenance')) ); - const routesArray: RouteObject[] = [ - { - path: '', - element: , - children: [ - { - path: '/', - element: - }, - { - path: 'status', - children: [ - { - path: '', - element: - }, - { - path: '404', - element: - }, - { - path: '500', - element: - }, - { - path: 'maintenance', - element: - }, - { - path: 'coming-soon', - element: - } - ] - }, - { - path: '*', - element: - } - ] - } - ]; - if (forgotPassword) { - return ( - - - - - ); + if (username) { + loginToResetPassword(); } if (!token) { return ( - + + } + > + }> + } + > + ); } @@ -142,7 +127,7 @@ function App() { return ( - + ); } @@ -156,18 +141,10 @@ function App() { > - {routesArray.map((route, index) => ( - - {route.children && - route.children.map((childRoute, childIndex) => ( - - ))} - - ))} + } + > } + element={ + + + + + + } /> + } /> + }> - }> diff --git a/typescript/frontend-marios2/src/Resources/formatPower.tsx b/typescript/frontend-marios2/src/Resources/formatPower.tsx index 6ac38ca6c..a54dcc960 100644 --- a/typescript/frontend-marios2/src/Resources/formatPower.tsx +++ b/typescript/frontend-marios2/src/Resources/formatPower.tsx @@ -37,7 +37,7 @@ export function findPower(value) { value = Math.abs(value); // Calculate the power of 10 that's greater or equal to the absolute value - let exponent = Math.floor(Math.log10(value)); + const exponent = Math.floor(Math.log10(value)); // Compute the nearest power of 10 const nearestPowerOf10 = Math.pow(10, exponent); diff --git a/typescript/frontend-marios2/src/Resources/routes.json b/typescript/frontend-marios2/src/Resources/routes.json index d33a51dfc..c592e0bfe 100644 --- a/typescript/frontend-marios2/src/Resources/routes.json +++ b/typescript/frontend-marios2/src/Resources/routes.json @@ -1,6 +1,6 @@ { "installation": "installation/", - "liveView": "liveView/", + "live": "live", "users": "/users/", "log": "log/", "installations": "/installations/", @@ -9,6 +9,13 @@ "folder": "folder/", "manageAccess": "manageAccess/", "user": "user/", - "tree": "tree", - "list": "list" + "tree": "tree/", + "list": "list/", + "overview": "overview", + "manage": "manage", + "log": "log", + "information": "information", + "configuration": "configuration", + "login": "/login/", + "forgotPassword": "/forgotPassword/" } diff --git a/typescript/frontend-marios2/src/components/ForgotPassword.tsx b/typescript/frontend-marios2/src/components/ForgotPassword.tsx index 0063d3563..c44a4325c 100644 --- a/typescript/frontend-marios2/src/components/ForgotPassword.tsx +++ b/typescript/frontend-marios2/src/components/ForgotPassword.tsx @@ -16,12 +16,14 @@ import { TokenContext } from 'src/contexts/tokenContext'; import Avatar from '@mui/material/Avatar'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import axiosConfig from 'src/Resources/axiosConfig'; +import { useNavigate } from 'react-router-dom'; +import routes from 'src/Resources/routes.json'; interface ForgotPasswordPromps { resetPassword: () => void; } -function ForgotPassword(props: ForgotPasswordPromps) { +function ForgotPassword() { const [username, setUsername] = useState(''); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); @@ -35,6 +37,7 @@ function ForgotPassword(props: ForgotPasswordPromps) { const { currentUser, setUser, removeUser } = context; const tokencontext = useContext(TokenContext); const { token, setNewToken, removeToken } = tokencontext; + const navigate = useNavigate(); const handleUsernameChange = (e) => { const { name, value } = e.target; @@ -43,7 +46,8 @@ function ForgotPassword(props: ForgotPasswordPromps) { const handleReturn = () => { setOpen(false); - props.resetPassword(); + navigate(routes.login); + //props.resetPassword(); }; const handleSubmit = () => { @@ -72,7 +76,7 @@ function ForgotPassword(props: ForgotPasswordPromps) { - + innovenergy logo @@ -122,6 +126,12 @@ function ForgotPassword(props: ForgotPasswordPromps) { margin="normal" required sx={{ width: 350 }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSubmit(); + } + }} /> {loading && } diff --git a/typescript/frontend-marios2/src/components/ResetPassword.tsx b/typescript/frontend-marios2/src/components/ResetPassword.tsx index 789fed067..2544de6f1 100644 --- a/typescript/frontend-marios2/src/components/ResetPassword.tsx +++ b/typescript/frontend-marios2/src/components/ResetPassword.tsx @@ -73,7 +73,7 @@ function ResetPassword() { - + innovenergy logo diff --git a/typescript/frontend-marios2/src/components/login.tsx b/typescript/frontend-marios2/src/components/login.tsx index 30d144ef9..dbcd203b3 100644 --- a/typescript/frontend-marios2/src/components/login.tsx +++ b/typescript/frontend-marios2/src/components/login.tsx @@ -23,12 +23,9 @@ import { TokenContext } from 'src/contexts/tokenContext'; import { useNavigate } from 'react-router-dom'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; +import routes from 'src/Resources/routes.json'; -interface loginPromps { - onForgotPassword: () => void; -} - -function Login(props: loginPromps) { +function Login() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -43,11 +40,10 @@ function Login(props: loginPromps) { if (!context) { return null; } - const { currentUser, setUser, removeUser } = context; + const { currentUser, setUser, removeUser } = context; const tokencontext = useContext(TokenContext); const { token, setNewToken, removeToken } = tokencontext; - const cookies = new Cookies(); const handleUsernameChange = (event: React.ChangeEvent) => { @@ -61,6 +57,11 @@ function Login(props: loginPromps) { const handleRememberMeChange = () => { setRememberMe(!rememberMe); }; + + const onForgotPassword = () => { + navigate(routes.forgotPassword); + }; + const handleSubmit = () => { setLoading(true); axiosConfigWithoutToken @@ -76,7 +77,7 @@ function Login(props: loginPromps) { cookies.set('rememberedUsername', username, { path: '/' }); cookies.set('rememberedPassword', password, { path: '/' }); } - navigate('/'); + navigate(routes.installations); } }) .catch((error) => { @@ -91,10 +92,10 @@ function Login(props: loginPromps) { return ( <> - + - + innovenergy logo @@ -113,7 +114,6 @@ function Login(props: loginPromps) { boxShadow: 24, p: 6, position: 'absolute', - top: '30%', left: '50%', transform: 'translate(-50%, -50%)' @@ -143,6 +143,12 @@ function Login(props: loginPromps) { margin="normal" required sx={{ width: 350 }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSubmit(); + } + }} /> { + if (e.key === 'Enter') { + e.preventDefault(); + handleSubmit(); + } + }} /> @@ -242,7 +254,7 @@ function Login(props: loginPromps) { sx={{ color: '#111111' }} onClick={(e) => { e.preventDefault(); - props.onForgotPassword(); + onForgotPassword(); }} > Forgot password? diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx index 1d8f5e03e..09c4fcdb6 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -1,6 +1,7 @@ import { TopologyValues } from '../Log/graph.util'; import { Box, CardContent, Container, Grid, TextField } from '@mui/material'; import React from 'react'; +import { FormattedMessage } from 'react-intl'; interface ConfigurationProps { values: TopologyValues; @@ -32,7 +33,12 @@ function Configuration(props: ConfigurationProps) { >
+ } value={props.values.minimumSoC.values[0].value + ' %'} fullWidth /> @@ -40,14 +46,24 @@ function Configuration(props: ConfigurationProps) {
+ } value={props.values.calibrationChargeForced.values[0].value} fullWidth />
+ } value={ ( (props.values.gridSetPoint.values[0].value as number) / @@ -59,7 +75,12 @@ function Configuration(props: ConfigurationProps) {
+ } value={ ( (props.values.installedDcDcPower.values[0] @@ -71,7 +92,12 @@ function Configuration(props: ConfigurationProps) {
+ } value={ ( (props.values.maximumDischargePower.values[0] @@ -83,7 +109,12 @@ function Configuration(props: ConfigurationProps) {
+ } value={props.values.battery.values.length - 4} fullWidth /> diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx index 36e301b70..8ea7c68cb 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx @@ -18,7 +18,6 @@ import CancelIcon from '@mui/icons-material/Cancel'; import { LogContext } from 'src/contexts/LogContextProvider'; import { FormattedMessage } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import routes from 'src/Resources/routes.json'; interface FlatInstallationViewProps { installations: I_Installation[]; @@ -29,7 +28,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { const logContext = useContext(LogContext); const { getStatus } = logContext; const navigate = useNavigate(); - const searchParams = new URLSearchParams(location.search); const installationId = parseInt(searchParams.get('installation')); const [selectedInstallation, setSelectedInstallation] = useState(-1); @@ -37,15 +35,9 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { const handleSelectOneInstallation = (installationID: number): void => { if (selectedInstallation != installationID) { setSelectedInstallation(installationID); - navigate( - routes.installations + - routes.list + - '?installation=' + - installationID.toString(), - { - replace: true - } - ); + navigate(`?installation=${installationID}`, { + replace: true + }); } else { setSelectedInstallation(-1); } @@ -88,8 +80,8 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { @@ -234,6 +226,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { + {props.installations.map((installation) => ( - }, - { - value: 'overview', - label: - }, - , - { - value: 'manage', - label: - }, - { - value: 'log', - label: - }, - { - value: 'information', - label: - }, - - { - value: 'configuration', - label: ( - - ) - } - ]; const theme = useTheme(); - const [currentTab, setCurrentTab] = useState('live'); const [formValues, setFormValues] = useState(props.current_installation); const requiredFields = ['name', 'region', 'location', 'country']; const context = useContext(UserContext); @@ -100,17 +68,16 @@ function Installation(props: singleInstallationProps) { const { installationStatus, handleLogWarningOrError, getStatus } = logContext; const searchParams = new URLSearchParams(location.search); const installationId = parseInt(searchParams.get('installation')); + const currentTab = searchParams.get('tab'); + const [values, setValues] = useState(null); + const [openModalDeleteInstallation, setOpenModalDeleteInstallation] = + useState(false); if (formValues == undefined) { return null; } - const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { - setCurrentTab(value); - setError(false); - }; - const handleChange = (e) => { const { name, value } = e.target; setFormValues({ @@ -127,7 +94,18 @@ function Installation(props: singleInstallationProps) { const handleDelete = (e) => { setLoading(true); setError(false); + setOpenModalDeleteInstallation(true); + }; + + const deleteInstallationModalHandle = (e) => { + setOpenModalDeleteInstallation(false); deleteInstallation(formValues, props.type); + setLoading(false); + }; + + const deleteInstallationModalHandleCancel = (e) => { + setOpenModalDeleteInstallation(false); + setLoading(false); }; const areRequiredFieldsFilled = () => { @@ -155,8 +133,8 @@ function Installation(props: singleInstallationProps) { useEffect(() => { let isMounted = true; setFormValues(props.current_installation); - setErrorLoadingS3Data(false); + let disconnectedStatusResult = []; const fetchDataPeriodically = async () => { const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); @@ -165,9 +143,6 @@ function Installation(props: singleInstallationProps) { try { const res = await fetchData(now, s3Credentials); - if (installationId == 2) { - console.log('Fetched data from unix timestamp ' + now); - } if (!isMounted) { return; } @@ -176,8 +151,22 @@ function Installation(props: singleInstallationProps) { const newErrors: Notification[] = []; if (res === FetchResult.notAvailable || res === FetchResult.tryLater) { - setErrorLoadingS3Data(true); handleLogWarningOrError(props.current_installation.id, -1); + + disconnectedStatusResult.unshift(-1); + disconnectedStatusResult = disconnectedStatusResult.slice(0, 5); + + let i = 0; + //If at least one status value shows an error, then show error + for (i; i < disconnectedStatusResult.length; i++) { + if (disconnectedStatusResult[i] != -1) { + break; + } + } + + if (i === disconnectedStatusResult.length) { + setErrorLoadingS3Data(true); + } } else { setErrorLoadingS3Data(false); setValues( @@ -237,9 +226,15 @@ function Installation(props: singleInstallationProps) { if (newErrors.length > 0) { handleLogWarningOrError(props.current_installation.id, 2); + disconnectedStatusResult.unshift(2); + disconnectedStatusResult = disconnectedStatusResult.slice(0, 5); } else if (newWarnings.length > 0) { + disconnectedStatusResult.unshift(1); + disconnectedStatusResult = disconnectedStatusResult.slice(0, 5); handleLogWarningOrError(props.current_installation.id, 1); } else { + disconnectedStatusResult.unshift(0); + disconnectedStatusResult = disconnectedStatusResult.slice(0, 5); handleLogWarningOrError(props.current_installation.id, 0); } } @@ -259,291 +254,394 @@ function Installation(props: singleInstallationProps) { if (installationId == props.current_installation.id) { return ( - - - + {openModalDeleteInstallation && ( + setOpenModalDeleteInstallation(false)} + aria-labelledby="error-modal" + aria-describedby="error-modal-description" > - {tabs.map((tab) => ( - - ))} - - - - - {currentTab === 'information' && ( - - + + Do you want to delete this installation? + + +
+ + +
+ + + )} - {currentUser.hasWriteAccess && ( - <> -
- -
+ +
+ + + + + {props.current_installation.name} + +
-
- -
- -
- -
- - )} - -
+ + {currentTab === 'information' && ( + + + + + - {currentUser.hasWriteAccess && ( - - )} - {currentUser.hasWriteAccess && ( - - )} - {loading && ( - + + } + name="name" + value={formValues.name} + onChange={handleChange} + fullWidth + required + error={formValues.name === ''} /> - )} - {error && ( - - - setError(false)} - sx={{ marginLeft: '4px' }} - > - - - - )} - {updated && ( - - +
+
+ + } + name="region" + value={formValues.region} + onChange={handleChange} + variant="outlined" + fullWidth + required + error={formValues.name === ''} + /> +
+
+ + } + name="location" + value={formValues.location} + onChange={handleChange} + variant="outlined" + fullWidth + required + error={formValues.name === ''} + /> +
+
+ + } + name="country" + value={formValues.country} + onChange={handleChange} + variant="outlined" + fullWidth + required + error={formValues.name === ''} + /> +
+
+ + } + name="orderNumbers" + value={formValues.orderNumbers} + onChange={handleChange} + variant="outlined" + fullWidth + /> +
+
+ + } + name="installationName" + value={formValues.installationName} + onChange={handleChange} + variant="outlined" + fullWidth + /> +
- setUpdated(false)} // Set error state to false on click - sx={{ marginLeft: '4px' }} - > - - - + {currentUser.hasWriteAccess && ( + <> +
+ +
+ +
+ +
+ +
+ +
+ )} -
- - + +
+ {currentUser.hasWriteAccess && ( + + )} + {currentUser.hasWriteAccess && ( + + )} + + {loading && ( + + )} + {error && ( + + + setError(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} + {updated && ( + + + + setUpdated(false)} // Set error state to false on click + sx={{ marginLeft: '4px' }} + > + + + + )} +
+ + + - - - )} - {currentTab === 'overview' && ( - - )} - {currentTab === 'configuration' && currentUser.hasWriteAccess && ( - - )} - {currentTab === 'manage' && currentUser.hasWriteAccess && ( - - - - )} - {currentTab === 'live' && } - {currentTab === 'log' && ( - - )} - - - + + )} + {currentTab === 'overview' && ( + + )} + {currentTab === 'configuration' && currentUser.hasWriteAccess && ( + + )} + {currentTab === 'manage' && currentUser.hasWriteAccess && ( + + + + )} + {currentTab === 'live' && } + {currentTab === 'log' && ( + + )} + + + + ); } else { return null; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx index 2af85562f..85d1f878c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormControl, Grid, @@ -6,34 +6,32 @@ import { TextField, useTheme } from '@mui/material'; -import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone'; import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView'; -import LogContextProvider from 'src/contexts/LogContextProvider'; +import LogContextProvider from '../../../contexts/LogContextProvider'; +import { I_Installation } from '../../../interfaces/InstallationTypes'; -function InstallationSearch() { +interface installationSearchProps { + installations: I_Installation[]; +} + +function InstallationSearch(props: installationSearchProps) { const theme = useTheme(); const [searchTerm, setSearchTerm] = useState(''); - const { installations, fetchAllInstallations } = - useContext(InstallationsContext); const searchParams = new URLSearchParams(location.search); const installationId = parseInt(searchParams.get('installation')); - useEffect(() => { - fetchAllInstallations(); - }, []); - - const [filteredData, setFilteredData] = useState(installations); + const [filteredData, setFilteredData] = useState(props.installations); useEffect(() => { - const filtered = installations.filter( + const filtered = props.installations.filter( (item) => item.name.toLowerCase().includes(searchTerm.toLowerCase()) || item.location.toLowerCase().includes(searchTerm.toLowerCase()) ); setFilteredData(filtered); - }, [searchTerm, installations]); + }, [searchTerm, props.installations]); return ( <> @@ -41,7 +39,7 @@ function InstallationSearch() { diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx deleted file mode 100644 index 03998cea6..000000000 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Box, Grid, useTheme } from '@mui/material'; -import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider'; -import InstallationSearch from './InstallationSearch'; - -function FlatView() { - const theme = useTheme(); - - return ( - - - - - - - - ); -} - -export default FlatView; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx index 7a892956f..fc33e30eb 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx @@ -1,10 +1,9 @@ -import React, { ChangeEvent, useEffect, useState } from 'react'; +import React, { ChangeEvent, useContext, useEffect, useState } from 'react'; import Footer from 'src/components/Footer'; -import { Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material'; +import { Box, Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material'; import ListIcon from '@mui/icons-material/List'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; -import UsersContextProvider from 'src/contexts/UsersContextProvider'; import { Link, Route, @@ -12,50 +11,220 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import FlatView from './flatView'; import TreeView from '../Tree/treeView'; import routes from 'src/Resources/routes.json'; +import InstallationSearch from './InstallationSearch'; +import { FormattedMessage } from 'react-intl'; +import { UserContext } from '../../../contexts/userContext'; +import { InstallationsContext } from '../../../contexts/InstallationsContextProvider'; +import LogContextProvider from '../../../contexts/LogContextProvider'; +import Installation from './Installation'; function InstallationTabs() { const theme = useTheme(); const location = useLocation(); const navigate = useNavigate(); - const tabs = [ - { - value: 'list', - label: 'Flat view', - icon: - }, - { - value: 'tree', - label: 'Tree view', - icon: - } - ]; + + const searchParams = new URLSearchParams(location.search); + const installationId = parseInt(searchParams.get('installation')); + const [singleInstallationID, setSingleInstallationID] = useState(-1); + const context = useContext(UserContext); + const { currentUser, setUser } = context; const [currentTab, setCurrentTab] = useState('list'); + const { installations, fetchAllInstallations } = + useContext(InstallationsContext); useEffect(() => { - //console.log(location.pathname); - if ( - location.pathname === '/installations' || - location.pathname === '/installations/' - ) { - navigate(routes.installations + routes.list, { + if (installations.length === 0) { + fetchAllInstallations(); + } + + if (installations.length === 1) { + navigate(`list?installation=${installations[0].id}&tab=live`, { replace: true }); - } else if (location.pathname === '/installations/tree') { - setCurrentTab('tree'); + setCurrentTab('live'); + } else { + if ( + location.pathname === '/installations' || + location.pathname === '/installations/' + ) { + navigate(routes.installations + routes.list, { + replace: true + }); + } else if (location.pathname === '/installations/tree/') { + setCurrentTab('tree'); + } else if (location.pathname === '/installations/list/') { + setCurrentTab('list'); + } + + if (installationId) { + navigate(`?installation=${installationId}&tab=live`, { + replace: true + }); + setCurrentTab('live'); + } } - }, [location.pathname, navigate]); + }, [location.pathname, navigate, installationId, installations]); const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { setCurrentTab(value); - navigate(value); }; - return ( - - + const singleInstallationTabs = currentUser.hasWriteAccess + ? [ + { + value: 'live', + label: + }, + { + value: 'overview', + label: + }, + , + { + value: 'manage', + label: ( + + ) + }, + { + value: 'log', + label: + }, + { + value: 'information', + label: ( + + ) + }, + + { + value: 'configuration', + label: ( + + ) + } + ] + : [ + { + value: 'live', + label: + }, + { + value: 'overview', + label: + }, + , + { + value: 'log', + label: + }, + { + value: 'information', + label: ( + + ) + } + ]; + + const tabs = installationId + ? currentUser.hasWriteAccess + ? [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + { + value: 'live', + label: + }, + { + value: 'overview', + label: + }, + , + { + value: 'manage', + label: ( + + ) + }, + { + value: 'log', + label: + }, + { + value: 'information', + label: ( + + ) + }, + + { + value: 'configuration', + label: ( + + ) + } + ] + : [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + + { + value: 'live', + label: + }, + { + value: 'overview', + label: + }, + , + { + value: 'log', + label: + }, + { + value: 'information', + label: ( + + ) + } + ] + : [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + } + ]; + + return installations.length > 1 ? ( + <> + ))} @@ -85,15 +259,79 @@ function InstallationTabs() { spacing={0} > - } /> + + + + + + } + /> } />