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}