new auth logic

This commit is contained in:
Peter Steinberger 2025-06-24 01:47:37 +02:00
parent 62c6052faf
commit 288a3197d2
19 changed files with 595 additions and 923 deletions

View file

@ -133,18 +133,22 @@ final class BunServer {
// Build the vibetunnel command with all arguments
var vibetunnelArgs = "--port \(port) --bind \(bindAddress)"
// Add password flag if password protection is enabled
if UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") && DashboardKeychain.shared.hasPassword() {
logger.info("Password protection enabled, retrieving from keychain")
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: "\\\\")
// Use password-only mode for better UX - users can enter any username
vibetunnelArgs += " --password \"\(escapedPassword)\""
}
// Add authentication flags based on configuration
let authMode = UserDefaults.standard.string(forKey: "authenticationMode") ?? "os"
logger.info("Configuring authentication mode: \(authMode)")
switch authMode {
case "none":
vibetunnelArgs += " --no-auth"
case "ssh":
vibetunnelArgs += " --enable-ssh-keys --disallow-user-password"
case "both":
vibetunnelArgs += " --enable-ssh-keys"
case "os":
fallthrough
default:
// OS authentication is the default, no special flags needed
break
}
// Create wrapper to run vibetunnel with a parent death signal

View file

@ -1,43 +1,35 @@
import SwiftUI
/// Fourth page explaining dashboard security and access protection.
/// Fourth page explaining dashboard security and authentication.
///
/// This view allows users to set up password protection for their dashboard
/// when accessing it over the network. It provides secure password entry
/// with confirmation and validation.
/// This view explains how the dashboard is protected using the system's
/// built-in authentication. Users don't need to set up a password as
/// authentication uses their macOS username and password by default.
///
/// ## Topics
///
/// ### Overview
/// The dashboard protection page includes:
/// - Password and confirmation fields
/// - Password validation (minimum 6 characters)
/// - Secure storage in keychain
/// - Automatic network mode switching when password is set
/// - Option to skip password protection
/// - Explanation of OS-based authentication
/// - Information about SSH key authentication option
/// - Link to settings for authentication configuration
///
/// ### Security
/// - Passwords are stored securely in the system keychain
/// - Network access is automatically enabled when a password is set
/// - Dashboard remains localhost-only without password
/// - Uses macOS system authentication (PAM) by default
/// - SSH key authentication available as an alternative
/// - No separate password setup required
struct ProtectDashboardPageView: View {
@State private var password = ""
@State private var confirmPassword = ""
@State private var showError = false
@State private var errorMessage = ""
@State private var isPasswordSet = false
private let dashboardKeychain = DashboardKeychain.shared
@State private var showingSettings = false
var body: some View {
VStack(spacing: 30) {
VStack(spacing: 16) {
Text("Protect Your Dashboard")
Text("Dashboard Security")
.font(.largeTitle)
.fontWeight(.semibold)
Text(
"If you want to access your dashboard over the network, set a password now.\nOtherwise, it will only be accessible via localhost."
"Your dashboard is protected using your macOS username and password.\nNo additional setup is required."
)
.font(.body)
.foregroundColor(.secondary)
@ -45,51 +37,61 @@ struct ProtectDashboardPageView: View {
.frame(maxWidth: 480)
.fixedSize(horizontal: false, vertical: true)
// Password fields
VStack(spacing: 12) {
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.onChange(of: password) { _, _ in
// Reset password saved state when user starts typing
if isPasswordSet {
isPasswordSet = false
}
}
// Authentication info
VStack(spacing: 20) {
// Security icon and explanation
HStack(spacing: 12) {
Image(systemName: "lock.shield.fill")
.font(.system(size: 48))
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
SecureField("Confirm Password", text: $confirmPassword)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.onChange(of: confirmPassword) { _, _ in
// Reset password saved state when user starts typing
if isPasswordSet {
isPasswordSet = false
}
}
if showError {
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
}
if isPasswordSet {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Password saved securely")
VStack(alignment: .leading, spacing: 8) {
Text("Secure by Default")
.font(.headline)
Text("Access requires your macOS credentials")
.font(.body)
.foregroundColor(.secondary)
}
.font(.caption)
} else {
Button("Set Password") {
setPassword()
}
.buttonStyle(.bordered)
.disabled(password.isEmpty)
.frame(maxWidth: 400)
// Authentication methods
VStack(alignment: .leading, spacing: 12) {
Label {
VStack(alignment: .leading, spacing: 4) {
Text("macOS Authentication")
.font(.callout)
.fontWeight(.medium)
Text("Uses your system username and password")
.font(.caption)
.foregroundColor(.secondary)
}
} icon: {
Image(systemName: "person.badge.shield.checkmark.fill")
.foregroundColor(.green)
}
Text("Leave empty to skip password protection")
Label {
VStack(alignment: .leading, spacing: 4) {
Text("SSH Key Authentication")
.font(.callout)
.fontWeight(.medium)
Text("Available as an alternative in Settings")
.font(.caption)
.foregroundColor(.secondary)
}
} icon: {
Image(systemName: "key.fill")
.foregroundColor(.blue)
}
}
.padding()
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
.frame(maxWidth: 400)
Text("You can configure authentication methods later in Settings")
.font(.caption)
.foregroundColor(.secondary)
}
@ -98,42 +100,6 @@ struct ProtectDashboardPageView: View {
}
.padding()
}
private func setPassword() {
showError = false
guard !password.isEmpty else {
return
}
guard password == confirmPassword else {
errorMessage = "Passwords do not match"
showError = true
return
}
guard password.count >= 6 else {
errorMessage = "Password must be at least 6 characters"
showError = true
return
}
if dashboardKeychain.setPassword(password) {
isPasswordSet = true
UserDefaults.standard.set(true, forKey: "dashboardPasswordEnabled")
// When password is set for the first time, automatically switch to network mode
let currentMode = DashboardAccessMode(rawValue: UserDefaults.standard
.string(forKey: "dashboardAccessMode") ?? ""
) ?? .localhost
if currentMode == .localhost {
UserDefaults.standard.set(DashboardAccessMode.network.rawValue, forKey: "dashboardAccessMode")
}
} else {
errorMessage = "Failed to save password to keychain"
showError = true
}
}
}
// MARK: - Preview