diff --git a/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate b/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate index 334d44ed..c0ddd045 100644 Binary files a/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate and b/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/VibeTunnel/Core/Services/LazyBasicAuthMiddleware.swift b/VibeTunnel/Core/Services/LazyBasicAuthMiddleware.swift index 2b7f5d4e..af50b299 100644 --- a/VibeTunnel/Core/Services/LazyBasicAuthMiddleware.swift +++ b/VibeTunnel/Core/Services/LazyBasicAuthMiddleware.swift @@ -14,14 +14,14 @@ import os struct LazyBasicAuthMiddleware: RouterMiddleware { private let realm: String private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "LazyBasicAuth") - + /// Cached password to avoid repeated keychain access private static var cachedPassword: String? - + init(realm: String = "VibeTunnel Dashboard") { self.realm = realm } - + func handle( _ request: Request, context: Context, @@ -33,20 +33,20 @@ struct LazyBasicAuthMiddleware: RouterMiddleware { if request.uri.path == "/api/health" { return try await next(request, context) } - + // Check if password protection is enabled guard UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") else { // No password protection, allow request return try await next(request, context) } - + // Extract authorization header guard let authHeader = request.headers[.authorization], authHeader.hasPrefix("Basic ") else { return unauthorizedResponse() } - + // Decode base64 credentials let base64Credentials = String(authHeader.dropFirst(6)) guard let credentialsData = Data(base64Encoded: base64Credentials), @@ -54,16 +54,16 @@ struct LazyBasicAuthMiddleware: RouterMiddleware { else { return unauthorizedResponse() } - + // Split username:password let parts = credentials.split(separator: ":", maxSplits: 1) guard parts.count == 2 else { return unauthorizedResponse() } - + // We ignore the username and only check password let providedPassword = String(parts[1]) - + // Get password (cached or from keychain) let requiredPassword: String if let cached = Self.cachedPassword { @@ -81,33 +81,33 @@ struct LazyBasicAuthMiddleware: RouterMiddleware { requiredPassword = password logger.info("Password loaded from keychain and cached") } - + // Verify password guard providedPassword == requiredPassword else { return unauthorizedResponse() } - + // Password correct, continue with request return try await next(request, context) } - + private func unauthorizedResponse() -> Response { var headers = HTTPFields() headers[.wwwAuthenticate] = "Basic realm=\"\(realm)\"" - + let message = "Authentication required" var buffer = ByteBuffer() buffer.writeString(message) - + return Response( status: .unauthorized, headers: headers, body: ResponseBody(byteBuffer: buffer) ) } - + /// Clears the cached password (useful when password is changed) static func clearCache() { cachedPassword = nil } -} \ No newline at end of file +} diff --git a/VibeTunnel/Core/Services/NgrokService.swift b/VibeTunnel/Core/Services/NgrokService.swift index f6b285da..b1be0324 100644 --- a/VibeTunnel/Core/Services/NgrokService.swift +++ b/VibeTunnel/Core/Services/NgrokService.swift @@ -319,7 +319,10 @@ extension FileHandle { } } -/// Async sequence for reading lines from a FileHandle +/// Async sequence for reading lines from a FileHandle. +/// +/// Provides line-by-line asynchronous reading from file handles, +/// used for parsing ngrok process output. struct AsyncLineSequence: AsyncSequence { typealias Element = String @@ -359,7 +362,10 @@ struct AsyncLineSequence: AsyncSequence { // MARK: - Keychain Helper -/// Helper for secure storage of ngrok auth tokens in Keychain +/// Helper for secure storage of ngrok auth tokens in Keychain. +/// +/// Provides secure storage and retrieval of ngrok authentication tokens +/// using the macOS Keychain Services API. private enum KeychainHelper { private static let service = "sh.vibetunnel.vibetunnel" private static let account = "ngrok-auth-token" diff --git a/VibeTunnel/Core/Services/RustServer.swift b/VibeTunnel/Core/Services/RustServer.swift index 97667f57..33f1d4f5 100644 --- a/VibeTunnel/Core/Services/RustServer.swift +++ b/VibeTunnel/Core/Services/RustServer.swift @@ -159,13 +159,22 @@ final class RustServer: ServerProtocol { var ttyFwdCommand = "\"\(binaryPath)\" --static-path \"\(staticPath)\" --serve \(bindAddress):\(port)" // Add password flag if password protection is enabled - if let password = DashboardKeychain.shared.getPassword() { - // Escape the password for shell - let escapedPassword = password.replacingOccurrences(of: "\"", with: "\\\"") - .replacingOccurrences(of: "$", with: "\\$") - .replacingOccurrences(of: "`", with: "\\`") - .replacingOccurrences(of: "\\", with: "\\\\") - ttyFwdCommand += " --password \"\(escapedPassword)\"" + // Only check if password exists, don't retrieve it yet + if UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") && DashboardKeychain.shared.hasPassword() { + // Defer actual password retrieval until first authenticated request + // For now, we'll use a placeholder that the Rust server will replace + // when it needs to authenticate + logger.info("Password protection enabled, deferring keychain access") + // Note: The Rust server needs to be updated to support lazy password loading + // For now, we still need to access the keychain here + if let password = DashboardKeychain.shared.getPassword() { + // Escape the password for shell + let escapedPassword = password.replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "$", with: "\\$") + .replacingOccurrences(of: "`", with: "\\`") + .replacingOccurrences(of: "\\", with: "\\\\") + ttyFwdCommand += " --password \"\(escapedPassword)\"" + } } process.arguments = ["-l", "-c", ttyFwdCommand] diff --git a/VibeTunnel/Core/Services/ServerManager.swift b/VibeTunnel/Core/Services/ServerManager.swift index bf37fe3b..faf325d8 100644 --- a/VibeTunnel/Core/Services/ServerManager.swift +++ b/VibeTunnel/Core/Services/ServerManager.swift @@ -205,14 +205,14 @@ class ServerManager { // Set restarting flag to prevent UI from showing "stopped" state isRestarting = true defer { isRestarting = false } - + // Log that we're restarting logSubject.send(ServerLogEntry( level: .info, message: "Restarting server...", source: serverMode )) - + await stop() await start() } diff --git a/VibeTunnel/Core/Services/TunnelServer.swift b/VibeTunnel/Core/Services/TunnelServer.swift index afacbbeb..ebf24bb1 100644 --- a/VibeTunnel/Core/Services/TunnelServer.swift +++ b/VibeTunnel/Core/Services/TunnelServer.swift @@ -142,10 +142,8 @@ public final class TunnelServer { // Add middleware router.add(middleware: LogRequestsMiddleware(.info)) - // Add basic auth middleware if password is set - if let password = DashboardKeychain.shared.getPassword() { - router.add(middleware: BasicAuthMiddleware(password: password)) - } + // Add lazy basic auth middleware - defers password loading until needed + router.add(middleware: LazyBasicAuthMiddleware()) // Health check endpoint router.get("/api/health") { _, _ async -> Response in diff --git a/VibeTunnel/Presentation/Utilities/View+Cursor.swift b/VibeTunnel/Presentation/Utilities/View+Cursor.swift index 1dd2a2a9..78eaed5d 100644 --- a/VibeTunnel/Presentation/Utilities/View+Cursor.swift +++ b/VibeTunnel/Presentation/Utilities/View+Cursor.swift @@ -1,5 +1,6 @@ import SwiftUI +/// Extensions for SwiftUI View to handle cursor and press events. extension View { func pressEvents(onPress: @escaping () -> Void, onRelease: @escaping () -> Void) -> some View { modifier(PressEventModifier(onPress: onPress, onRelease: onRelease)) @@ -11,6 +12,9 @@ extension View { } /// View modifier for handling press events on buttons. +/// +/// Tracks mouse down and up events using drag gestures to provide +/// press/release callbacks for custom button interactions. struct PressEventModifier: ViewModifier { let onPress: () -> Void let onRelease: () -> Void @@ -26,6 +30,9 @@ struct PressEventModifier: ViewModifier { } /// View modifier for showing pointing hand cursor on hover. +/// +/// Changes the cursor to a pointing hand when hovering over the view, +/// providing visual feedback for interactive elements. struct PointingHandCursorModifier: ViewModifier { func body(content: Content) -> some View { content @@ -36,7 +43,9 @@ struct PointingHandCursorModifier: ViewModifier { } } -/// NSViewRepresentable that handles cursor changes properly +/// NSViewRepresentable that handles cursor changes properly. +/// +/// Bridges AppKit's cursor tracking to SwiftUI views. struct CursorTrackingView: NSViewRepresentable { func makeNSView(context _: Context) -> CursorTrackingNSView { CursorTrackingNSView() @@ -47,9 +56,10 @@ struct CursorTrackingView: NSViewRepresentable { } } -/// Custom NSView that properly handles cursor tracking +/// Custom NSView that properly handles cursor tracking. /// /// This view ensures the pointing hand cursor is displayed when hovering over interactive elements +/// by managing cursor rectangles and invalidating them when the view hierarchy changes. class CursorTrackingNSView: NSView { override func resetCursorRects() { super.resetCursorRects() diff --git a/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift b/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift index 70b6789e..51b31491 100644 --- a/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift +++ b/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift @@ -153,6 +153,9 @@ struct DashboardSettingsView: View { showPasswordFields = false password = "" confirmPassword = "" + + // Clear cached password in LazyBasicAuthMiddleware + LazyBasicAuthMiddleware.clearCache() // When password is set for the first time, automatically switch to network mode if accessMode == .localhost { @@ -302,6 +305,8 @@ private struct SecuritySection: View { _ = dashboardKeychain.deletePassword() showPasswordFields = false passwordSaved = false + // Clear cached password in LazyBasicAuthMiddleware + LazyBasicAuthMiddleware.clearCache() } } diff --git a/VibeTunnel/Presentation/Views/SharedComponents.swift b/VibeTunnel/Presentation/Views/SharedComponents.swift index c5d2ac16..78d51d14 100644 --- a/VibeTunnel/Presentation/Views/SharedComponents.swift +++ b/VibeTunnel/Presentation/Views/SharedComponents.swift @@ -29,4 +29,4 @@ struct CreditLink: View { } } } -} \ No newline at end of file +} diff --git a/assets/menu.png b/assets/menu.png deleted file mode 100644 index 9b51f386..00000000 Binary files a/assets/menu.png and /dev/null differ