mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix new security settings
This commit is contained in:
parent
bcad09585d
commit
52bded09fa
1 changed files with 132 additions and 96 deletions
|
|
@ -82,7 +82,7 @@ struct DashboardSettingsView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
.frame(minWidth: 600)
|
.frame(minWidth: 500, idealWidth: 600)
|
||||||
.navigationTitle("Dashboard")
|
.navigationTitle("Dashboard")
|
||||||
.onAppear {
|
.onAppear {
|
||||||
onAppearSetup()
|
onAppearSetup()
|
||||||
|
|
@ -258,10 +258,10 @@ private struct SecuritySection: View {
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .none: "No Authentication"
|
case .none: "None"
|
||||||
case .osAuth: "macOS Authentication"
|
case .osAuth: "macOS"
|
||||||
case .sshKeys: "SSH Keys Only"
|
case .sshKeys: "SSH Keys"
|
||||||
case .both: "macOS + SSH Keys"
|
case .both: "Both"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,52 +280,58 @@ private struct SecuritySection: View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
// Authentication mode picker
|
// Authentication mode picker
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Authentication Method")
|
HStack {
|
||||||
.font(.callout)
|
Text("Authentication Method")
|
||||||
.fontWeight(.medium)
|
.font(.callout)
|
||||||
|
Spacer()
|
||||||
Picker("", selection: $authMode) {
|
Picker("", selection: $authMode) {
|
||||||
ForEach(AuthenticationMode.allCases, id: \.self) { mode in
|
ForEach(AuthenticationMode.allCases, id: \.self) { mode in
|
||||||
Text(mode.displayName)
|
Text(mode.displayName)
|
||||||
.tag(mode)
|
.tag(mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.labelsHidden()
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.menu)
|
||||||
.onChange(of: authMode) { _, newValue in
|
.frame(minWidth: 120, idealWidth: 150)
|
||||||
// Save the authentication mode
|
.fixedSize()
|
||||||
UserDefaults.standard.set(newValue.rawValue, forKey: "authenticationMode")
|
.onChange(of: authMode) { _, newValue in
|
||||||
|
// Save the authentication mode
|
||||||
|
UserDefaults.standard.set(newValue.rawValue, forKey: "authenticationMode")
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
logger.info("Authentication mode changed to: \(newValue.rawValue)")
|
logger.info("Authentication mode changed to: \(newValue.rawValue)")
|
||||||
await serverManager.restart()
|
await serverManager.restart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(authMode.description)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional info based on selected mode
|
// Additional info based on selected mode
|
||||||
if authMode == .osAuth || authMode == .both {
|
if authMode == .osAuth || authMode == .both {
|
||||||
HStack {
|
HStack(alignment: .center, spacing: 6) {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
Text("Uses your macOS username: \(NSUserName())")
|
Text("Uses your macOS username: \(NSUserName())")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if authMode == .sshKeys || authMode == .both {
|
if authMode == .sshKeys || authMode == .both {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
HStack(alignment: .center, spacing: 6) {
|
||||||
HStack {
|
Image(systemName: "key.fill")
|
||||||
Image(systemName: "key.fill")
|
.foregroundColor(.blue)
|
||||||
.foregroundColor(.blue)
|
.font(.system(size: 12))
|
||||||
Text("SSH keys from ~/.ssh/authorized_keys")
|
.frame(width: 16, height: 16)
|
||||||
.font(.caption)
|
Text("SSH keys from ~/.ssh/authorized_keys")
|
||||||
}
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
Button("Open SSH Keys Folder") {
|
Spacer()
|
||||||
|
Button("Open folder") {
|
||||||
let sshPath = NSHomeDirectory() + "/.ssh"
|
let sshPath = NSHomeDirectory() + "/.ssh"
|
||||||
if FileManager.default.fileExists(atPath: sshPath) {
|
if FileManager.default.fileExists(atPath: sshPath) {
|
||||||
NSWorkspace.shared.open(URL(fileURLWithPath: sshPath))
|
NSWorkspace.shared.open(URL(fileURLWithPath: sshPath))
|
||||||
|
|
@ -340,6 +346,7 @@ private struct SecuritySection: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,8 +356,8 @@ private struct SecuritySection: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Localhost connections are always accessible without authentication.")
|
Text("Localhost connections are always accessible without authentication.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -368,19 +375,21 @@ private struct ServerConfigurationSection: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
AccessModeView(
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
accessMode: accessMode,
|
AccessModeView(
|
||||||
accessModeString: $accessModeString,
|
accessMode: accessMode,
|
||||||
serverPort: serverPort,
|
accessModeString: $accessModeString,
|
||||||
localIPAddress: localIPAddress,
|
serverPort: serverPort,
|
||||||
restartServerWithNewBindAddress: restartServerWithNewBindAddress
|
localIPAddress: localIPAddress,
|
||||||
)
|
restartServerWithNewBindAddress: restartServerWithNewBindAddress
|
||||||
|
)
|
||||||
|
|
||||||
PortConfigurationView(
|
PortConfigurationView(
|
||||||
serverPort: $serverPort,
|
serverPort: $serverPort,
|
||||||
restartServerWithNewPort: restartServerWithNewPort,
|
restartServerWithNewPort: restartServerWithNewPort,
|
||||||
serverManager: serverManager
|
serverManager: serverManager
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Server Configuration")
|
Text("Server Configuration")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
@ -415,17 +424,14 @@ private struct AccessModeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if accessMode == .network {
|
if accessMode == .network {
|
||||||
HStack {
|
if let ip = localIPAddress {
|
||||||
Image(systemName: "info.circle")
|
Text("Dashboard available at http://\(ip):\(serverPort)")
|
||||||
.foregroundColor(.blue)
|
.font(.caption)
|
||||||
if let ip = localIPAddress {
|
.foregroundStyle(.secondary)
|
||||||
Text("Dashboard available at http://\(ip):\(serverPort)")
|
} else {
|
||||||
.font(.caption)
|
Text("Fetching local IP address...")
|
||||||
} else {
|
.font(.caption)
|
||||||
Text("Fetching local IP address...")
|
.foregroundStyle(.secondary)
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -443,37 +449,68 @@ private struct PortConfigurationView: View {
|
||||||
@State private var portError: String?
|
@State private var portError: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Port")
|
HStack {
|
||||||
.font(.callout)
|
Text("Port")
|
||||||
Spacer()
|
.font(.callout)
|
||||||
TextField("Port", text: $pendingPort)
|
Spacer()
|
||||||
.textFieldStyle(.roundedBorder)
|
HStack(spacing: 4) {
|
||||||
.frame(width: 80)
|
TextField("", text: $pendingPort)
|
||||||
.focused($isPortFieldFocused)
|
.textFieldStyle(.roundedBorder)
|
||||||
.onSubmit {
|
.frame(width: 80)
|
||||||
validateAndUpdatePort()
|
.multilineTextAlignment(.center)
|
||||||
}
|
.focused($isPortFieldFocused)
|
||||||
.onAppear {
|
.onSubmit {
|
||||||
pendingPort = serverPort
|
validateAndUpdatePort()
|
||||||
}
|
}
|
||||||
.onChange(of: pendingPort) { _, newValue in
|
.onAppear {
|
||||||
// Clear error when user types
|
pendingPort = serverPort
|
||||||
portError = nil
|
}
|
||||||
// Limit to 5 digits
|
.onChange(of: pendingPort) { _, newValue in
|
||||||
if newValue.count > 5 {
|
// Clear error when user types
|
||||||
pendingPort = String(newValue.prefix(5))
|
portError = nil
|
||||||
|
// Limit to 5 digits
|
||||||
|
if newValue.count > 5 {
|
||||||
|
pendingPort = String(newValue.prefix(5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Button(action: {
|
||||||
|
if let port = Int(pendingPort), port < 65535 {
|
||||||
|
pendingPort = String(port + 1)
|
||||||
|
validateAndUpdatePort()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: "chevron.up")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.frame(width: 16, height: 11)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
if let port = Int(pendingPort), port > 1024 {
|
||||||
|
pendingPort = String(port - 1)
|
||||||
|
validateAndUpdatePort()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.frame(width: 16, height: 11)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let error = portError {
|
if let error = portError {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
Image(systemName: "exclamationmark.triangle")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
Text(error)
|
Text(error)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -566,8 +603,10 @@ private struct NgrokIntegrationSection: View {
|
||||||
// Link to ngrok dashboard
|
// Link to ngrok dashboard
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "link")
|
Image(systemName: "link")
|
||||||
Link("Create free ngrok account", destination: URL(string: "https://dashboard.ngrok.com/signup")!)
|
if let url = URL(string: "https://dashboard.ngrok.com/signup") {
|
||||||
.font(.caption)
|
Link("Create free ngrok account", destination: url)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
|
|
@ -576,7 +615,6 @@ private struct NgrokIntegrationSection: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("ngrok creates secure tunnels to your dashboard from anywhere.")
|
Text("ngrok creates secure tunnels to your dashboard from anywhere.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -683,10 +721,10 @@ private struct PublicURLView: View {
|
||||||
showCopiedFeedback = false
|
showCopiedFeedback = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}, label: {
|
||||||
Image(systemName: showCopiedFeedback ? "checkmark" : "doc.on.doc")
|
Image(systemName: showCopiedFeedback ? "checkmark" : "doc.on.doc")
|
||||||
.foregroundColor(showCopiedFeedback ? .green : .accentColor)
|
.foregroundColor(showCopiedFeedback ? .green : .accentColor)
|
||||||
}
|
})
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.help("Copy URL")
|
.help("Copy URL")
|
||||||
}
|
}
|
||||||
|
|
@ -712,9 +750,7 @@ private struct ErrorView: View {
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct DashboardSettingsView_Previews: PreviewProvider {
|
#Preview("Dashboard Settings") {
|
||||||
static var previews: some View {
|
DashboardSettingsView()
|
||||||
DashboardSettingsView()
|
.frame(width: 500, height: 800)
|
||||||
.frame(width: 600, height: 800)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue