From f82190afc17c2ea610b6ce4f18453123fd0aa714 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Wed, 4 Mar 2026 12:46:18 +0100 Subject: [PATCH] improved delete installation logic with S3 bucket purge and delete and reminder for manual check --- .../App/Backend/DataTypes/Methods/ExoCmd.cs | 89 +++++++++++++++---- .../App/Backend/DataTypes/Methods/Session.cs | 29 +++--- csharp/App/Backend/Database/Delete.cs | 22 ++++- csharp/Lib/S3Utils/S3.cs | 44 ++++++++- .../dashboards/Information/Information.tsx | 34 ++++++- .../Information/InformationSalidomo.tsx | 34 ++++++- .../Information/InformationSodistoreHome.tsx | 34 ++++++- typescript/frontend-marios2/src/lang/de.json | 3 + typescript/frontend-marios2/src/lang/en.json | 3 + typescript/frontend-marios2/src/lang/fr.json | 3 + typescript/frontend-marios2/src/lang/it.json | 3 + 11 files changed, 256 insertions(+), 42 deletions(-) diff --git a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs index ab3526cfe..8deb34bcf 100644 --- a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs +++ b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs @@ -264,42 +264,70 @@ public static class ExoCmd public static async Task RevokeReadKey(this Installation installation) { - //Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature - var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}"; var method = $"access-key/{installation.S3Key}"; - + var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60; var authheader = "credential="+S3Credentials.Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime); - + var client = new HttpClient(); - + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader); - - var response = await client.DeleteAsync(url); - return response.IsSuccessStatusCode; + + try + { + var response = await client.DeleteAsync(url); + + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"Successfully revoked read key for installation {installation.Id}."); + return true; + } + + Console.WriteLine($"Failed to revoke read key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"Error occurred while revoking read key: {ex.Message}"); + return false; + } } public static async Task RevokeWriteKey(this Installation installation) { - //Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature - var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3WriteKey}"; var method = $"access-key/{installation.S3WriteKey}"; - + var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60; var authheader = "credential="+S3Credentials.Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime); - + var client = new HttpClient(); - + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader); - - var response = await client.DeleteAsync(url); - return response.IsSuccessStatusCode; + + try + { + var response = await client.DeleteAsync(url); + + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"Successfully revoked write key for installation {installation.Id}."); + return true; + } + + Console.WriteLine($"Failed to revoke write key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"Error occurred while revoking write key: {ex.Message}"); + return false; + } } public static async Task<(String, String)> CreateWriteKey(this Installation installation) @@ -373,10 +401,35 @@ public static class ExoCmd return await s3Region.PutBucket(installation.BucketName()) != null; } - public static async Task DeleteBucket(this Installation installation) + public static async Task PurgeAndDeleteBucket(this Installation installation) { var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!); - return await s3Region.DeleteBucket(installation.BucketName()) ; + var bucket = s3Region.Bucket(installation.BucketName()); + + try + { + var purged = await bucket.PurgeBucket(); + if (!purged) + { + Console.WriteLine($"Failed to purge bucket {installation.BucketName()} for installation {installation.Id}."); + return false; + } + + var deleted = await s3Region.DeleteBucket(installation.BucketName()); + if (!deleted) + { + Console.WriteLine($"Failed to delete bucket {installation.BucketName()} for installation {installation.Id}."); + return false; + } + + Console.WriteLine($"Successfully purged and deleted bucket {installation.BucketName()} for installation {installation.Id}."); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error occurred while purging/deleting bucket {installation.BucketName()}: {ex.Message}"); + return false; + } } diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index be56b6ead..5a7c4e95d 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -324,23 +324,22 @@ public static class SessionMethods { var user = session?.User; - if (user is not null - && installation is not null - && user.UserType != 0) - { - - return - Db.Delete(installation) - && await installation.RevokeReadKey() - && await installation.RevokeWriteKey() - && await installation.RemoveReadRole() - && await installation.RemoveWriteRole() - && await installation.DeleteBucket(); - - } + if (user is null || installation is null || user.UserType == 0) + return false; - return false; + // Try all Exoscale operations independently (don't short-circuit) + var readKeyOk = await installation.RevokeReadKey(); + var writeKeyOk = await installation.RevokeWriteKey(); + var readRoleOk = await installation.RemoveReadRole(); + var writeRoleOk = await installation.RemoveWriteRole(); + var bucketOk = await installation.PurgeAndDeleteBucket(); + if (!readKeyOk || !writeKeyOk || !readRoleOk || !writeRoleOk || !bucketOk) + Console.WriteLine($"[Delete] Partial Exoscale cleanup for installation {installation.Id}: " + + $"readKey={readKeyOk}, writeKey={writeKeyOk}, readRole={readRoleOk}, writeRole={writeRoleOk}, bucket={bucketOk}"); + + // Always delete from DB (best-effort — admin wants it gone) + return Db.Delete(installation); } public static Boolean Create(this Session? session, User newUser) diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index 438fa995c..c4fbe17c8 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -102,10 +102,28 @@ public static partial class Db InstallationAccess.Delete(i => i.InstallationId == installation.Id); if (installation.Product == (int)ProductType.Salimax) { - //For Salimax, delete the OrderNumber2Installation entries associated with this installation id. OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id); } - + + // Clean up AI insight cache entries linked to this installation's reports + var weeklyIds = WeeklyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList(); + var monthlyIds = MonthlyReports.Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList(); + var yearlyIds = YearlyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList(); + + foreach (var id in weeklyIds) AiInsightCaches.Delete(c => c.ReportType == "weekly" && c.ReportId == id); + foreach (var id in monthlyIds) AiInsightCaches.Delete(c => c.ReportType == "monthly" && c.ReportId == id); + foreach (var id in yearlyIds) AiInsightCaches.Delete(c => c.ReportType == "yearly" && c.ReportId == id); + + // Clean up energy records, report summaries, errors, warnings, and user actions + DailyRecords .Delete(r => r.InstallationId == installation.Id); + HourlyRecords .Delete(r => r.InstallationId == installation.Id); + WeeklyReports .Delete(r => r.InstallationId == installation.Id); + MonthlyReports.Delete(r => r.InstallationId == installation.Id); + YearlyReports .Delete(r => r.InstallationId == installation.Id); + Errors .Delete(e => e.InstallationId == installation.Id); + Warnings .Delete(w => w.InstallationId == installation.Id); + UserActions .Delete(a => a.InstallationId == installation.Id); + return Installations.Delete(i => i.Id == installation.Id) > 0; } } diff --git a/csharp/Lib/S3Utils/S3.cs b/csharp/Lib/S3Utils/S3.cs index ac07cbc97..3d7ef9aec 100644 --- a/csharp/Lib/S3Utils/S3.cs +++ b/csharp/Lib/S3Utils/S3.cs @@ -201,10 +201,52 @@ public static class S3 return response.HttpStatusCode == HttpStatusCode.OK; } + public static async Task PurgeBucket(this S3Bucket bucket) + { + var client = bucket.Region.GetS3Client(); + var totalDeleted = 0; + + Console.WriteLine($"[PurgeBucket] Starting purge of bucket {bucket.Name}"); + + while (true) + { + var listResponse = await client.ListObjectsV2Async(new ListObjectsV2Request + { + BucketName = bucket.Name, + MaxKeys = 1000 + }); + + if (listResponse.S3Objects.Count == 0) + { + Console.WriteLine($"[PurgeBucket] Completed purge of bucket {bucket.Name}: {totalDeleted} objects deleted"); + return true; + } + + var deleteResponse = await client.DeleteObjectsAsync(new DeleteObjectsRequest + { + BucketName = bucket.Name, + Objects = listResponse.S3Objects + .Select(o => new KeyVersion { Key = o.Key }) + .ToList() + }); + + if (deleteResponse.HttpStatusCode != HttpStatusCode.OK) + { + Console.WriteLine($"[PurgeBucket] Failed at batch after {totalDeleted} objects deleted from bucket {bucket.Name}"); + return false; + } + + totalDeleted += listResponse.S3Objects.Count; + + if (totalDeleted % 10000 == 0) + Console.WriteLine($"[PurgeBucket] Progress: {totalDeleted} objects deleted from bucket {bucket.Name}"); + } + } + public static async Task DeleteBucket(this S3Region region, String bucketName) { var request = new DeleteBucketRequest { BucketName = bucketName }; - + var response = await region .GetS3Client() .DeleteBucketAsync(request); diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx index acfe3220a..f5b5975a7 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx @@ -128,7 +128,7 @@ function Information(props: InformationProps) { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - width: 350, + width: 450, bgcolor: 'background.paper', borderRadius: 4, boxShadow: 24, @@ -143,7 +143,37 @@ function Information(props: InformationProps) { gutterBottom sx={{ fontWeight: 'bold' }} > - Do you want to delete this installation? + + + + + {props.s3Credentials.s3Bucket} + + + +
- Do you want to delete this installation? + + + + + {props.s3Credentials.s3Bucket} + + + +
- Do you want to delete this installation? + + + + + {props.s3Credentials.s3Bucket} + + + +