using System.Diagnostics; using System.Net; using System.Text.RegularExpressions; using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; using InnovEnergy.App.Backend.Websockets; using InnovEnergy.Lib.Utils; using Microsoft.AspNetCore.Mvc; namespace InnovEnergy.App.Backend; using Token = String; // create JobStatus class to track download battery log job public class JobStatus { public string JobId { get; set; } public string Status { get; set; } public string FileName { get; set; } public DateTime StartTime { get; set; } } [Controller] [Route("api/")] public class Controller : ControllerBase { [HttpPost(nameof(Login))] public ActionResult Login(String username, String? password) { 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!); if (!(user.Password.IsNullOrEmpty() && user.MustResetPassword) && !user.VerifyPassword(password)) { //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)); //TODO The Frontend should check for the MustResetPassword Flag return Db.Create(session) ? session : throw new Exceptions(401,"Session Creation Exception", "Not allowed to log in.", Request.Path.Value!); } [HttpPost(nameof(Logout))] public ActionResult Logout(Token authToken) { var session = Db.GetSession(authToken); return session.Logout() ? Ok() : Unauthorized(); } [HttpGet(nameof(CreateWebSocket))] public async Task CreateWebSocket(Token authToken) { var session = Db.GetSession(authToken)?.User; if (session is null) { Console.WriteLine("------------------------------------Unauthorized user----------------------------------------------"); HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; HttpContext.Abort(); return; } if (!HttpContext.WebSockets.IsWebSocketRequest) { Console.WriteLine("------------------------------------Not a websocket request ----------------------------------------------"); HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; HttpContext.Abort(); return; } var webSocketContext = await HttpContext.WebSockets.AcceptWebSocketAsync(); var webSocket = webSocketContext; //Handle the WebSocket connection await WebsocketManager.HandleWebSocketConnection(webSocket); } [HttpGet(nameof(GetAllErrorsForInstallation))] public ActionResult> GetAllErrorsForInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Db.Errors .Where(error => error.InstallationId == id) .OrderByDescending(error => error.Date) .ThenByDescending(error => error.Time) .ToList(); } [HttpGet(nameof(GetHistoryForInstallation))] public ActionResult> GetHistoryForInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Db.UserActions .Where(action =>action.InstallationId == id) .OrderByDescending(action => action.Timestamp) .ToList(); } [HttpGet(nameof(GetAllWarningsForInstallation))] public ActionResult> GetAllWarningsForInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Db.Warnings .Where(error => error.InstallationId == id) .OrderByDescending(error => error.Date) .ThenByDescending(error => error.Time) .ToList(); } [HttpGet(nameof(GetCsvTimestampsForInstallation))] public ActionResult> GetCsvTimestampsForInstallation(Int64 id, Int32 start, Int32 end, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var sampleSize = 100; var allTimestamps = new List(); static string FindCommonPrefix(string str1, string str2) { int minLength = Math.Min(str1.Length, str2.Length); int i = 0; while (i < minLength && str1[i] == str2[i]) { i++; } return str1.Substring(0, i); } string commonPrefix = FindCommonPrefix(start.ToString(), end.ToString()); Int64 startTimestamp = Int64.Parse(start.ToString().Substring(0,5)); Int64 endTimestamp = Int64.Parse(end.ToString().Substring(0,5)); string configPath = "/home/ubuntu/.s3cfg"; while (startTimestamp <= endTimestamp) { string bucketPath = "s3://"+installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/"+startTimestamp; Console.WriteLine("Fetching data for "+startTimestamp); try { // Set up process start info ProcessStartInfo startInfo = new ProcessStartInfo { FileName = "s3cmd", Arguments = $"--config {configPath} ls {bucketPath}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; // Start the process Process process = new Process { StartInfo = startInfo }; process.Start(); // Read the output string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); // Check for errors if (process.ExitCode != 0) { Console.WriteLine("Error executing command:"); Console.WriteLine(error); } else { // Define a regex pattern to match the filenames without .csv extension var pattern = @"/([^/]+)\.csv$"; var regex = new Regex(pattern); // Process each line of the output foreach (var line in output.Split('\n')) { var match = regex.Match(line); if (match.Success && long.Parse(match.Groups[1].Value) >= start && long.Parse(match.Groups[1].Value) <= end) { allTimestamps.Add(long.Parse(match.Groups[1].Value)); //Console.WriteLine(match.Groups[1].Value); } } } } catch (Exception e) { Console.WriteLine($"Exception: {e.Message}"); } startTimestamp++; } int totalRecords = allTimestamps.Count; if (totalRecords <= sampleSize) { // If the total records are less than or equal to the sample size, return all records Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end); Console.WriteLine("SampledTimestamps = " + allTimestamps.Count); return allTimestamps; } int interval = totalRecords / sampleSize; var sampledTimestamps = new List(); for (int i = 0; i < totalRecords; i += interval) { sampledTimestamps.Add(allTimestamps[i]); } // If we haven't picked enough records (due to rounding), add the latest record to ensure completeness if (sampledTimestamps.Count < sampleSize) { sampledTimestamps.Add(allTimestamps.Last()); } Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end); Console.WriteLine("TotalRecords = "+totalRecords + " interval = "+ interval); Console.WriteLine("SampledTimestamps = " + sampledTimestamps.Count); return sampledTimestamps; } [HttpGet(nameof(GetUserById))] public ActionResult GetUserById(Int64 id, Token authToken) { var session = Db.GetSession(authToken)?.User; if (session == null) return Unauthorized(); var user = Db.GetUserById(id); if (user is null || !session.HasAccessTo(user)) return Unauthorized(); return user .HidePassword() .HideParentIfUserHasNoAccessToParent(session); } [HttpGet(nameof(GetInstallationById))] public ActionResult GetInstallationById(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return installation .FillOrderNumbers() .HideParentIfUserHasNoAccessToParent(user) .HideWriteKeyIfUserIsNotAdmin(user.UserType); } [HttpGet(nameof(GetUsersWithDirectAccessToInstallation))] public ActionResult> GetUsersWithDirectAccessToInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return installation .UsersWithDirectAccess() .Where(u => u.IsDescendantOf(user)) .Select(u => u.HidePassword()) .ToList(); } [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))] public ActionResult> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) 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(); } [HttpGet(nameof(GetUsersWithDirectAccessToFolder))] public ActionResult> GetUsersWithDirectAccessToFolder(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var folder = Db.GetFolderById(id); if (folder is null || !user.HasAccessTo(folder)) return Unauthorized(); return folder .UsersWithDirectAccess() .Where(u => u.IsDescendantOf(user)) .Select(u => u.HidePassword()) .ToList(); } [HttpGet(nameof(GetUsersWithInheritedAccessToFolder))] public ActionResult> GetUsersWithInheritedAccessToFolder(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var folder = Db.GetFolderById(id); if (folder is null || !user.HasAccessTo(folder)) return Unauthorized(); return folder .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(GetFolderById))] public ActionResult GetFolderById(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var folder = Db.GetFolderById(id); if (folder is null || !user.HasAccessTo(folder)) return Unauthorized(); return folder.HideParentIfUserHasNoAccessToParent(user); } [HttpGet(nameof(GetAllDirectChildUsers))] public ActionResult> GetAllDirectChildUsers(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); return user.ChildUsers().Select(u => u.HidePassword()).ToList(); } [HttpGet(nameof(GetAllChildUsers))] public ActionResult> GetAllChildUsers(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); return user .DescendantUsers() .Select(u => u.HidePassword()) .ToList(); } [HttpGet(nameof(GetAllInstallations))] public ActionResult> GetAllInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return user .AccessibleInstallations(product:0) .Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType)) .ToList(); } [HttpGet(nameof(GetAllSalidomoInstallations))] public ActionResult> GetAllSalidomoInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return user .AccessibleInstallations(product:1) .ToList(); } [HttpGet(nameof(GetAllFolders))] public ActionResult> GetAllFolders(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return new(user.AccessibleFolders().HideParentIfUserHasNoAccessToParent(user)); } [HttpGet(nameof(GetAllFoldersAndInstallations))] public ActionResult> GetAllFoldersAndInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); var foldersAndInstallations = user .AccessibleFoldersAndInstallations(product:0) .Do(o => o.FillOrderNumbers()) .Select(o => o.HideParentIfUserHasNoAccessToParent(user)) .OfType(); // Important! JSON serializer must see Objects otherwise // it will just serialize the members of TreeNode %&@#!!! // TODO Filter out write keys return new (foldersAndInstallations); } [HttpPost(nameof(CreateUser))] public async Task> CreateUser([FromBody] User newUser, Token authToken) { var create = Db.GetSession(authToken).Create(newUser); if (create) { var mail_success= await Db.SendNewUserEmail(newUser); if (!mail_success) { Db.GetSession(authToken).Delete(newUser); } return mail_success ? newUser.HidePassword():Unauthorized(); } return Unauthorized() ; } [HttpPost(nameof(CreateInstallation))] public async Task> CreateInstallation([FromBody] Installation installation, Token authToken) { var session = Db.GetSession(authToken); if (! await session.Create(installation)) return Unauthorized(); return installation; } [HttpPost(nameof(CreateFolder))] public ActionResult CreateFolder([FromBody] Folder folder, Token authToken) { var session = Db.GetSession(authToken); if (!session.Create(folder)) return Unauthorized(); return folder.HideParentIfUserHasNoAccessToParent(session!.User); } [HttpPost(nameof(GrantUserAccessToFolder))] public ActionResult GrantUserAccessToFolder(FolderAccess folderAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var folder = Db.GetFolderById(folderAccess.FolderId); var user = Db.GetUserById(folderAccess.UserId); return session.GrantUserAccessTo(user, folder) ? Ok() : Unauthorized(); } [HttpPost(nameof(RevokeUserAccessToFolder))] public ActionResult RevokeUserAccessToFolder(FolderAccess folderAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var folder = Db.GetFolderById(folderAccess.FolderId); var user = Db.GetUserById(folderAccess.UserId); return session.RevokeUserAccessTo(user, folder) ? Ok() : Unauthorized(); } [HttpPost(nameof(GrantUserAccessToInstallation))] public ActionResult GrantUserAccessToInstallation(InstallationAccess installationAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var installation = Db.GetInstallationById(installationAccess.InstallationId); var user = Db.GetUserById(installationAccess.UserId); return session.GrantUserAccessTo(user, installation) ? Ok() : Unauthorized(); } [HttpPost(nameof(RevokeUserAccessToInstallation))] public ActionResult RevokeUserAccessToInstallation(InstallationAccess installationAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var installation = Db.GetInstallationById(installationAccess.InstallationId); var user = Db.GetUserById(installationAccess.UserId); return session.RevokeUserAccessTo(user, installation) ? Ok() : Unauthorized(); } [HttpPut(nameof(UpdateUser))] public ActionResult UpdateUser([FromBody] User updatedUser, Token authToken) { var session = Db.GetSession(authToken); if (!session.Update(updatedUser)) return Unauthorized(); return updatedUser.HidePassword(); } [HttpPut(nameof(UpdatePassword))] public ActionResult UpdatePassword(String newPassword, Token authToken) { var session = Db.GetSession(authToken); return session.UpdatePassword(newPassword) ? Ok() : Unauthorized(); } [HttpPut(nameof(UpdateInstallation))] public ActionResult UpdateInstallation([FromBody] Installation installation, Token authToken) { var session = Db.GetSession(authToken); if (!session.Update(installation)) return Unauthorized(); if (installation.Product == 0) { return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType); } return installation.HideParentIfUserHasNoAccessToParent(session!.User); } [HttpPost(nameof(AcknowledgeError))] public ActionResult AcknowledgeError(Int64 id, Token authToken) { var session = Db.GetSession(authToken); if (session == null) return Unauthorized(); var error=Db.Errors .FirstOrDefault(error => error.Id == id); error.Seen = true; return Db.Update(error) ? Ok() : Unauthorized(); } [HttpPost(nameof(AcknowledgeWarning))] public ActionResult AcknowledgeWarning(Int64 id, Token authToken) { var session = Db.GetSession(authToken); if (session == null) return Unauthorized(); var warning=Db.Warnings .FirstOrDefault(warning => warning.Id == id); warning.Seen = true; return Db.Update(warning) ? Ok() : Unauthorized(); } [HttpPut(nameof(UpdateFolder))] public ActionResult UpdateFolder([FromBody] Folder folder, Token authToken) { var session = Db.GetSession(authToken); if (!session.Update(folder)) return Unauthorized(); return folder.HideParentIfUserHasNoAccessToParent(session!.User); } [HttpPut(nameof(MoveInstallation))] public ActionResult MoveInstallation(Int64 installationId,Int64 parentId, Token authToken) { var session = Db.GetSession(authToken); return session.MoveInstallation(installationId, parentId) ? Ok() : Unauthorized(); } [HttpPost(nameof(UpdateFirmware))] public async Task UpdateFirmware(Int64 batteryNode, Int64 installationId,String version,Token authToken) { var session = Db.GetSession(authToken); var installationToUpdate = Db.GetInstallationById(installationId); if (installationToUpdate != null) { _ = session.RunScriptInBackground(installationToUpdate.VpnIp, batteryNode,version,installationToUpdate.Product); } return Ok(); } private static Dictionary JobStatuses = new Dictionary(); [HttpPost("StartDownloadBatteryLog")] public async Task> StartDownloadBatteryLog(long batteryNode, long installationId, Token authToken) { var session = Db.GetSession(authToken); var installationToDownload = Db.GetInstallationById(installationId); if (installationToDownload != null) { string jobId = Guid.NewGuid().ToString(); _ = Task.Run(async () => { await session.RunDownloadLogScript(installationToDownload.VpnIp, batteryNode, installationToDownload.Product); string fileName = $"{installationToDownload.VpnIp}-node{batteryNode}-{DateTime.Now:dd-MM-yyyy}.bin"; string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}"; if (System.IO.File.Exists(filePath)) { SaveJobStatus(jobId, "Completed", fileName:fileName); } else { SaveJobStatus(jobId, "Failed"); } }); // Store initial job status in in-memory storage SaveJobStatus(jobId, "Processing"); return Ok(jobId); } return NotFound(); } [HttpGet("DownloadBatteryLog")] public async Task DownloadBatteryLog(string jobId) { Console.WriteLine("-----------------------------------Start uploading battery log-----------------------------------"); var jobStatus = JobStatuses.TryGetValue(jobId, out var status) ? status : null; if (jobStatus == null || jobStatus.Status != "Completed" || string.IsNullOrEmpty(jobStatus.FileName)) { return NotFound(); } string fileName = jobStatus.FileName; string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}"; if (!System.IO.File.Exists(filePath)) { return NotFound(); } string contentType = "application/octet-stream"; var memory = new MemoryStream(); await using (var stream = new FileStream(filePath, FileMode.Open)) { await stream.CopyToAsync(memory); } memory.Position = 0; var fileContentResult = new FileContentResult(memory.ToArray(), contentType) { //FileDownloadName = Path.GetFileName(filePath) FileDownloadName = fileName }; Console.WriteLine("-----------------------------------Stop uploading battery log-----------------------------------"); return fileContentResult; } [HttpDelete("DeleteBatteryLog")] public IActionResult DeleteBatteryLog(string fileName) { Console.WriteLine("-----------------------------------Start deleting downloaded battery log-----------------------------------"); string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}"; try { if (System.IO.File.Exists(filePath)) { System.IO.File.Delete(filePath); Console.WriteLine("-----------------------------------Stop deleting downloaded battery log-----------------------------------"); return Ok(); } else { return NotFound("File not found."); } } catch (Exception ex) { return StatusCode(500, $"Internal server error: {ex.Message}"); } } private void SaveJobStatus(string jobId, string status, string fileName = null) { JobStatuses[jobId] = new JobStatus { JobId = jobId, Status = status, FileName = fileName, StartTime = DateTime.UtcNow // Initialize StartTime when saving }; } [HttpGet("GetJobResult")] public ActionResult GetJobResult(string jobId) { if (string.IsNullOrEmpty(jobId)) { return BadRequest(new { status = "Error", message = "Job ID is required." }); } if (!JobStatuses.TryGetValue(jobId, out var jobStatus)) { return NotFound(); } if (jobStatus.Status == "Completed") { return Ok(new { status = "Completed", fileName = jobStatus.FileName }); } else if (jobStatus.Status == "Failed") { return StatusCode(500, new { status = "Failed", message = "Job processing failed." }); } else if (jobStatus.Status == "Processing") { // Check for timeout var startTime = jobStatus.StartTime; var currentTime = DateTime.UtcNow; if ((currentTime - startTime).TotalMinutes > 60)//60 minutes as timeout => Running multiple tasks in parallel on a crowded backend server will increase the time each task takes to complete { return StatusCode(500, new { status = "Failed", message = "Job in back end timeout exceeded." }); } return Ok(new { status = "Processing" }); } else { return BadRequest(new { status = "Unknown", message = "Unknown job status." }); } } [HttpPost(nameof(InsertNewAction))] public async Task>> InsertNewAction([FromBody] UserAction action, Token authToken) { var session = Db.GetSession(authToken); var actionSuccess = await session.InsertUserAction(action); return actionSuccess ? Ok() : Unauthorized(); } [HttpPost(nameof(UpdateAction))] public async Task>> UpdateAction([FromBody] UserAction action, Token authToken) { var session = Db.GetSession(authToken); var actionSuccess = await session.UpdateUserAction(action); return actionSuccess ? Ok() : Unauthorized(); } [HttpPost(nameof(DeleteAction))] public async Task>> DeleteAction(Int64 actionId, Token authToken) { var session = Db.GetSession(authToken); var actionSuccess = await session.DeleteUserAction(actionId); return actionSuccess ? Ok() : Unauthorized(); } [HttpPost(nameof(EditInstallationConfig))] public async Task>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,Token authToken) { var session = Db.GetSession(authToken); // Send configuration changes var success = await session.SendInstallationConfig(installationId, config); // Record configuration change if (success) { // Create a new UserAction object var action = new UserAction { InstallationId = installationId, Timestamp = DateTime.Now, Description = config.GetConfigurationString() }; var actionSuccess = await session.InsertUserAction(action); return actionSuccess?Ok():Unauthorized(); } return Unauthorized(); } [HttpPut(nameof(MoveFolder))] public ActionResult MoveFolder(Int64 folderId,Int64 parentId, Token authToken) { var session = Db.GetSession(authToken); return session.MoveFolder(folderId, parentId) ? Ok() : Unauthorized(); } [HttpDelete(nameof(DeleteUser))] public ActionResult DeleteUser(Int64 userId, Token authToken) { var session = Db.GetSession(authToken); var user = Db.GetUserById(userId); return session.Delete(user) ? Ok() : Unauthorized(); } [HttpDelete(nameof(DeleteInstallation))] public async Task DeleteInstallation(Int64 installationId, Token authToken) { var session = Db.GetSession(authToken); var installation = Db.GetInstallationById(installationId); return await session.Delete(installation) ? Ok() : Unauthorized(); } [HttpDelete(nameof(DeleteFolder))] public ActionResult DeleteFolder(Int64 folderId, Token authToken) { var session = Db.GetSession(authToken); var folder = Db.GetFolderById(folderId); return session.Delete(folder) ? Ok() : Unauthorized(); } [HttpPost(nameof(ResetPasswordRequest))] public async Task>> ResetPasswordRequest(String username) { var user = Db.GetUserByEmail(username); if (user is null) return Unauthorized(); var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user)); var success = Db.Create(session); return success && await 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(); Db.DeleteUserPassword(user); return Redirect($"https://monitor.innov.energy/?username={user.Email}&reset=true"); // TODO: move to settings file } }