Explorar el Código

Merge pull request #450 from CodeByMiniOrg/block-same-totp

Block sending loop commands with same TOTP code
Marion Barker hace 11 meses
padre
commit
fe90560570

+ 6 - 2
LoopFollow.xcodeproj/project.pbxproj

@@ -10,9 +10,10 @@
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
 		654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */; };
 		654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */; };
-		6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
 		654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */; };
 		6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134192E1DC27900BDBE08 /* OverridePresetData.swift */; };
+		6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
+		6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
 		DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
 		DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; };
 		DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; };
@@ -393,9 +394,10 @@
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleQRCodeScannerView.swift; sourceTree = "<group>"; };
 		654132E92E19F24800BDBE08 /* TOTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGenerator.swift; sourceTree = "<group>"; };
-		6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
 		654134172E1DC09700BDBE08 /* OverridePresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetsView.swift; sourceTree = "<group>"; };
 		654134192E1DC27900BDBE08 /* OverridePresetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetData.swift; sourceTree = "<group>"; };
+		6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
+		6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = "<group>"; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = "<group>"; };
 		DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = "<group>"; };
@@ -1191,6 +1193,7 @@
 		DDEF503E2D479B8A00884336 /* LoopAPNS */ = {
 			isa = PBXGroup;
 			children = (
+				6584B1002E4A263900135D4D /* TOTPService.swift */,
 				DDEF503F2D479B8A00884336 /* LoopAPNSService.swift */,
 				DDEF50412D479BAA00884336 /* LoopAPNSCarbsView.swift */,
 				DDEF50422D479BBA00884336 /* LoopAPNSBolusView.swift */,
@@ -1936,6 +1939,7 @@
 				DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */,
 				DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */,
 				DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */,
+				6584B1012E4A263900135D4D /* TOTPService.swift in Sources */,
 				DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
 				DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
 				DD4878172C7B75350048F05C /* BolusView.swift in Sources */,

+ 49 - 2
LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift

@@ -23,6 +23,11 @@ struct LoopAPNSBolusView: View {
     private let otpPeriod: TimeInterval = 30
     private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
 
+    // Computed property to check if TOTP should be blocked
+    private var isTOTPBlocked: Bool {
+        TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
+    }
+
     enum AlertType {
         case success
         case error
@@ -115,9 +120,29 @@ struct LoopAPNSBolusView: View {
                                 Text("Send Insulin")
                             }
                         }
-                        .disabled(insulinAmount.doubleValue(for: .internationalUnit()) <= 0 || isLoading)
+                        .disabled(insulinAmount.doubleValue(for: .internationalUnit()) <= 0 || isLoading || isTOTPBlocked)
                         .frame(maxWidth: .infinity)
                     }
+
+                    // TOTP Blocking Warning Section
+                    if isTOTPBlocked {
+                        Section {
+                            VStack(alignment: .leading, spacing: 8) {
+                                HStack {
+                                    Image(systemName: "exclamationmark.triangle.fill")
+                                        .foregroundColor(.orange)
+                                    Text("TOTP Code Already Used")
+                                        .font(.headline)
+                                        .foregroundColor(.orange)
+                                }
+                                Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
+                                    .font(.caption)
+                                    .foregroundColor(.secondary)
+                                    .multilineTextAlignment(.leading)
+                            }
+                            .padding(.vertical, 4)
+                        }
+                    }
                     Section(header: Text("Security")) {
                         VStack(alignment: .leading) {
                             Text("Current OTP Code")
@@ -158,10 +183,30 @@ struct LoopAPNSBolusView: View {
                 loadRecommendedBolus()
                 // Reset timer state so it shows '-' until first tick
                 otpTimeRemaining = nil
+                // Don't reset TOTP usage flag here - let the timer handle it
+
+                // Validate TOTP state when view appears
+                _ = isTOTPBlocked
             }
             .onReceive(otpTimer) { _ in
                 let now = Date().timeIntervalSince1970
-                otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
+                let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
+
+                // Check if we've moved to a new TOTP period (when time remaining increases)
+                if let currentOtpTimeRemaining = otpTimeRemaining,
+                   newOtpTimeRemaining > currentOtpTimeRemaining
+                {
+                    // New TOTP code generated, reset the usage flag
+                    TOTPService.shared.resetTOTPUsage()
+                }
+
+                // Also check if we're at the very beginning of a new period (when time remaining is close to 30)
+                if newOtpTimeRemaining >= 29 {
+                    // We're at the start of a new TOTP period, reset the usage flag
+                    TOTPService.shared.resetTOTPUsage()
+                }
+
+                otpTimeRemaining = newOtpTimeRemaining
 
                 // Check if recommended bolus calculation is older than 5 minutes (but less than 12 minutes)
                 if let lastLoopTime = lastLoopTime {
@@ -332,6 +377,8 @@ struct LoopAPNSBolusView: View {
                 DispatchQueue.main.async {
                     isLoading = false
                     if success {
+                        // Mark TOTP code as used
+                        TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
                         alertMessage = "Insulin sent successfully!"
                         alertType = .success
                         LogManager.shared.log(

+ 49 - 2
LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift

@@ -22,6 +22,11 @@ struct LoopAPNSCarbsView: View {
     @FocusState private var carbsFieldIsFocused: Bool
     @FocusState private var absorptionFieldIsFocused: Bool
 
+    // Computed property to check if TOTP should be blocked
+    private var isTOTPBlocked: Bool {
+        TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
+    }
+
     enum AlertType {
         case success
         case error
@@ -168,10 +173,30 @@ struct LoopAPNSCarbsView: View {
                                 Text("Send Carbs")
                             }
                         }
-                        .disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading)
+                        .disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading || isTOTPBlocked)
                         .frame(maxWidth: .infinity)
                     }
 
+                    // TOTP Blocking Warning Section
+                    if isTOTPBlocked {
+                        Section {
+                            VStack(alignment: .leading, spacing: 8) {
+                                HStack {
+                                    Image(systemName: "exclamationmark.triangle.fill")
+                                        .foregroundColor(.orange)
+                                    Text("TOTP Code Already Used")
+                                        .font(.headline)
+                                        .foregroundColor(.orange)
+                                }
+                                Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
+                                    .font(.caption)
+                                    .foregroundColor(.secondary)
+                                    .multilineTextAlignment(.leading)
+                            }
+                            .padding(.vertical, 4)
+                        }
+                    }
+
                     Section(header: Text("Security")) {
                         VStack(alignment: .leading) {
                             Text("Current OTP Code")
@@ -221,10 +246,30 @@ struct LoopAPNSCarbsView: View {
                 }
                 // Reset timer state so it shows '-' until first tick
                 otpTimeRemaining = nil
+                // Don't reset TOTP usage flag here - let the timer handle it
+
+                // Validate TOTP state when view appears
+                _ = isTOTPBlocked
             }
             .onReceive(otpTimer) { _ in
                 let now = Date().timeIntervalSince1970
-                otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
+                let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
+
+                // Check if we've moved to a new TOTP period (when time remaining increases)
+                if let currentOtpTimeRemaining = otpTimeRemaining,
+                   newOtpTimeRemaining > currentOtpTimeRemaining
+                {
+                    // New TOTP code generated, reset the usage flag
+                    TOTPService.shared.resetTOTPUsage()
+                }
+
+                // Also check if we're at the very beginning of a new period (when time remaining is close to 30)
+                if newOtpTimeRemaining >= 29 {
+                    // We're at the start of a new TOTP period, reset the usage flag
+                    TOTPService.shared.resetTOTPUsage()
+                }
+
+                otpTimeRemaining = newOtpTimeRemaining
             }
             .alert(isPresented: $showAlert) {
                 switch alertType {
@@ -345,6 +390,8 @@ struct LoopAPNSCarbsView: View {
                 DispatchQueue.main.async {
                     isLoading = false
                     if success {
+                        // Mark TOTP code as used
+                        TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
                         let timeFormatter = DateFormatter()
                         timeFormatter.timeStyle = .short
                         alertMessage = "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!"

+ 37 - 0
LoopFollow/Remote/LoopAPNS/TOTPService.swift

@@ -0,0 +1,37 @@
+// LoopFollow
+// TOTPService.swift
+// Created by codebymini.
+
+import Foundation
+
+/// Service class for managing TOTP code usage and blocking logic
+class TOTPService {
+    static let shared = TOTPService()
+
+    private init() {}
+
+    /// Checks if the current TOTP code is blocked (already used)
+    /// - Parameter qrCodeURL: The QR code URL to extract the current TOTP from
+    /// - Returns: True if the TOTP is blocked, false otherwise
+    func isTOTPBlocked(qrCodeURL: String) -> Bool {
+        guard let currentTOTP = TOTPGenerator.extractOTPFromURL(qrCodeURL) else {
+            return false
+        }
+
+        // Check if the current TOTP code equals the last sent TOTP code
+        return currentTOTP == Observable.shared.lastSentTOTP.value
+    }
+
+    /// Marks the current TOTP code as used
+    /// - Parameter qrCodeURL: The QR code URL to extract the current TOTP from
+    func markTOTPAsUsed(qrCodeURL: String) {
+        if let currentTOTP = TOTPGenerator.extractOTPFromURL(qrCodeURL) {
+            Observable.shared.lastSentTOTP.set(currentTOTP)
+        }
+    }
+
+    /// Resets the TOTP usage tracking (called when a new TOTP period starts)
+    func resetTOTPUsage() {
+        Observable.shared.lastSentTOTP.set(nil)
+    }
+}

+ 4 - 0
LoopFollow/Storage/Observable.swift

@@ -35,5 +35,9 @@ class Observable {
 
     var settingsPath = ObservableValue<NavigationPath>(default: NavigationPath())
 
+    // MARK: - Loop APNS TOTP Tracking
+
+    var lastSentTOTP = ObservableValue<String?>(default: nil)
+
     private init() {}
 }