krz/rune

an ios client for njalla

clone: git clone https://gitbay.org/krz/rune.git

v1.0.0: Rune/Views/Tokens/TokenAddView.swift · raw

  1import SwiftUI
  2
  3struct TokenAddView: View {
  4    @ObservedObject var viewModel: TokenViewModel
  5    let client: NjallaClient
  6
  7    @Environment(\.dismiss) private var dismiss
  8
  9    @State private var comment = ""
 10    @State private var fromText = ""
 11    @State private var allowedMethodsText = ""
 12    @State private var ipValidationMessage: String?
 13    @State private var methodValidationMessage: String?
 14
 15    var body: some View {
 16        Form {
 17            Section("Token") {
 18                TextField("Comment", text: $comment)
 19            }
 20
 21            Section {
 22                TextEditor(text: $fromText)
 23                    .frame(minHeight: 100)
 24                if let ipValidationMessage {
 25                    Text(ipValidationMessage)
 26                        .font(.caption)
 27                        .foregroundStyle(.red)
 28                }
 29            } header: {
 30                Text("IP Restrictions")
 31            } footer: {
 32                Text("Enter one IPv4, IPv6, or CIDR range per line.")
 33            }
 34
 35            Section {
 36                TextEditor(text: $allowedMethodsText)
 37                    .frame(minHeight: 100)
 38                if let methodValidationMessage {
 39                    Text(methodValidationMessage)
 40                        .font(.caption)
 41                        .foregroundStyle(.red)
 42                }
 43            } header: {
 44                Text("Allowed Methods")
 45            } footer: {
 46                Text("Enter one API method per line, for example `list-domains`.")
 47            }
 48
 49            Section {
 50                Button("Save") {
 51                    Task {
 52                        await save()
 53                    }
 54                }
 55                .disabled(viewModel.isSaving)
 56            }
 57        }
 58        .navigationTitle("Add Token")
 59        .navigationBarTitleDisplayMode(.inline)
 60        .toolbar {
 61            ToolbarItem(placement: .cancellationAction) {
 62                Button("Cancel", role: .cancel) {
 63                    dismiss()
 64                }
 65            }
 66        }
 67        .onChange(of: fromText) { _, _ in
 68            ipValidationMessage = nil
 69        }
 70        .onChange(of: allowedMethodsText) { _, _ in
 71            methodValidationMessage = nil
 72        }
 73        .alert("API Error", isPresented: errorBinding) {
 74            Button("OK", role: .cancel) {}
 75        } message: {
 76            Text(viewModel.errorMessage ?? "")
 77        }
 78    }
 79
 80    private func save() async {
 81        guard validateInput() else {
 82            return
 83        }
 84
 85        let request = TokenCreateRequest(
 86            comment: comment,
 87            from: lines(fromText),
 88            allowedMethods: lines(allowedMethodsText)
 89        )
 90
 91        if await viewModel.addToken(request: request, client: client) {
 92            dismiss()
 93        }
 94    }
 95
 96    private func lines(_ value: String) -> [String] {
 97        value
 98            .split(whereSeparator: \.isNewline)
 99            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
100            .filter { !$0.isEmpty }
101    }
102
103    private func validateInput() -> Bool {
104        let ipEntries = lines(fromText)
105        let methodEntries = lines(allowedMethodsText)
106
107        ipValidationMessage = ipEntries.allSatisfy(isValidIPOrCIDR(_:))
108            ? nil
109            : "One or more entries are not valid IP addresses or CIDR ranges."
110
111        methodValidationMessage = methodEntries.allSatisfy(isValidMethodName(_:))
112            ? nil
113            : "One or more method names appear invalid. Use format: list-domains"
114
115        return ipValidationMessage == nil && methodValidationMessage == nil
116    }
117
118    private func isValidMethodName(_ value: String) -> Bool {
119        value.range(of: "^[a-z-]+$", options: .regularExpression) != nil
120    }
121
122    private func isValidIPOrCIDR(_ value: String) -> Bool {
123        let pattern = #"^((\d{1,3}\.){3}\d{1,3})(/(3[0-2]|[12]?\d))?$|^([0-9A-Fa-f:]+)(/\d{1,3})?$"#
124        return value.range(of: pattern, options: .regularExpression) != nil
125    }
126
127    private var errorBinding: Binding<Bool> {
128        Binding(
129            get: { viewModel.errorMessage != nil },
130            set: { newValue in
131                if !newValue {
132                    viewModel.errorMessage = nil
133                }
134            }
135        )
136    }
137}