krz/rune

an ios client for njalla

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

v1.0.0: Rune/API/Models.swift · raw

  1import Foundation
  2
  3struct EmptyResult: Codable {}
  4
  5struct WalletBalance: Codable, Equatable {
  6    let balance: Int
  7}
  8
  9struct DomainListResponse: Codable {
 10    let domains: [Domain]
 11}
 12
 13struct RecordListResponse: Codable {
 14    let records: [DNSRecord]
 15}
 16
 17struct TokenListResponse: Codable {
 18    let tokens: [APIToken]
 19}
 20
 21struct Domain: Codable, Identifiable, Hashable {
 22    var id: String { name }
 23
 24    let name: String
 25    let status: String?
 26    let expiry: String?
 27    let autorenew: Bool?
 28    let mailforwarding: Bool?
 29    let dnssec: Bool?
 30    let lock: Bool?
 31    let nameservers: [String]?
 32}
 33
 34struct DNSRecord: Codable, Identifiable, Hashable {
 35    let id: String
 36    let domain: String
 37    let type: String
 38    let name: String
 39    let content: String?
 40    let ttl: Int?
 41    let prio: Int?
 42    let weight: Int?
 43    let port: Int?
 44    let target: String?
 45    let sshAlgorithm: Int?
 46    let sshType: Int?
 47
 48    enum CodingKeys: String, CodingKey {
 49        case id
 50        case domain
 51        case type
 52        case name
 53        case content
 54        case ttl
 55        case prio
 56        case weight
 57        case port
 58        case target
 59        case sshAlgorithm = "ssh_algorithm"
 60        case sshType = "ssh_type"
 61    }
 62
 63    init(
 64        id: String,
 65        domain: String,
 66        type: String,
 67        name: String,
 68        content: String?,
 69        ttl: Int?,
 70        prio: Int?,
 71        weight: Int?,
 72        port: Int?,
 73        target: String?,
 74        sshAlgorithm: Int?,
 75        sshType: Int?
 76    ) {
 77        self.id = id
 78        self.domain = domain
 79        self.type = type
 80        self.name = name
 81        self.content = content
 82        self.ttl = ttl
 83        self.prio = prio
 84        self.weight = weight
 85        self.port = port
 86        self.target = target
 87        self.sshAlgorithm = sshAlgorithm
 88        self.sshType = sshType
 89    }
 90
 91    init(from decoder: Decoder) throws {
 92        let container = try decoder.container(keyedBy: CodingKeys.self)
 93
 94        id = try container.decodeLossyString(forKey: .id)
 95        domain = try container.decodeIfPresent(String.self, forKey: .domain) ?? ""
 96        type = try container.decode(String.self, forKey: .type)
 97        name = try container.decode(String.self, forKey: .name)
 98        content = try container.decodeIfPresent(String.self, forKey: .content)
 99        ttl = try container.decodeLossyIntIfPresent(forKey: .ttl)
100        prio = try container.decodeLossyIntIfPresent(forKey: .prio)
101        weight = try container.decodeLossyIntIfPresent(forKey: .weight)
102        port = try container.decodeLossyIntIfPresent(forKey: .port)
103        target = try container.decodeIfPresent(String.self, forKey: .target)
104        sshAlgorithm = try container.decodeLossyIntIfPresent(forKey: .sshAlgorithm)
105        sshType = try container.decodeLossyIntIfPresent(forKey: .sshType)
106    }
107
108    func withDomain(_ domain: String) -> DNSRecord {
109        DNSRecord(
110            id: id,
111            domain: domain,
112            type: type,
113            name: name,
114            content: content,
115            ttl: ttl,
116            prio: prio,
117            weight: weight,
118            port: port,
119            target: target,
120            sshAlgorithm: sshAlgorithm,
121            sshType: sshType
122        )
123    }
124}
125
126struct APIToken: Codable, Identifiable, Hashable {
127    var id: String { key }
128
129    let key: String
130    let comment: String?
131    let from: [String]?
132    let allowedDomains: [String]?
133    let allowedServers: [String]?
134    let allowedMethods: [String]?
135    let allowedPrefixes: [String]?
136    let allowedTypes: [String]?
137
138    enum CodingKeys: String, CodingKey {
139        case key
140        case comment
141        case from
142        case allowedDomains = "allowed_domains"
143        case allowedServers = "allowed_servers"
144        case allowedMethods = "allowed_methods"
145        case allowedPrefixes = "allowed_prefixes"
146        case allowedTypes = "allowed_types"
147    }
148}
149
150enum DNSRecordType: String, CaseIterable, Identifiable, Codable {
151    case a = "A"
152    case aaaa = "AAAA"
153    case aname = "ANAME"
154    case caa = "CAA"
155    case cname = "CNAME"
156    case ds = "DS"
157    case dynamic = "Dynamic"
158    case https = "HTTPS"
159    case mx = "MX"
160    case naptr = "NAPTR"
161    case ns = "NS"
162    case ptr = "PTR"
163    case srv = "SRV"
164    case sshfp = "SSHFP"
165    case svcb = "SVCB"
166    case tlsa = "TLSA"
167    case txt = "TXT"
168
169    var id: String { rawValue }
170
171    var usesContent: Bool {
172        switch self {
173        case .dynamic, .https, .svcb:
174            return false
175        default:
176            return true
177        }
178    }
179
180    var usesTTL: Bool {
181        usesContent
182    }
183
184    var usesPriority: Bool {
185        switch self {
186        case .https, .mx, .srv, .svcb:
187            return true
188        default:
189            return false
190        }
191    }
192
193    var usesWeight: Bool {
194        self == .srv
195    }
196
197    var usesPort: Bool {
198        self == .srv
199    }
200
201    var usesTarget: Bool {
202        self == .https || self == .svcb
203    }
204
205    var usesSSHFields: Bool {
206        self == .sshfp
207    }
208}
209
210struct DNSRecordDraft: Equatable {
211    var type: DNSRecordType = .a
212    var name = ""
213    var content = ""
214    var ttl = ""
215    var prio = ""
216    var weight = ""
217    var port = ""
218    var target = ""
219    var sshAlgorithm = ""
220    var sshType = ""
221
222    init() {}
223
224    init(record: DNSRecord) {
225        type = DNSRecordType(rawValue: record.type) ?? .a
226        name = record.name
227        content = record.content ?? ""
228        ttl = record.ttl.map(String.init) ?? ""
229        prio = record.prio.map(String.init) ?? ""
230        weight = record.weight.map(String.init) ?? ""
231        port = record.port.map(String.init) ?? ""
232        target = record.target ?? ""
233        sshAlgorithm = record.sshAlgorithm.map(String.init) ?? ""
234        sshType = record.sshType.map(String.init) ?? ""
235    }
236
237    mutating func resetTypeSpecificFields() {
238        content = ""
239        ttl = ""
240        prio = ""
241        weight = ""
242        port = ""
243        target = ""
244        sshAlgorithm = ""
245        sshType = ""
246    }
247
248    var trimmedName: String {
249        name.trimmingCharacters(in: .whitespacesAndNewlines)
250    }
251
252    var canSubmit: Bool {
253        !trimmedName.isEmpty
254    }
255
256    func params(domain: String, id: String? = nil) -> [String: Any] {
257        var params: [String: Any] = [
258            "domain": domain,
259            "type": type.rawValue,
260            "name": trimmedName
261        ]
262
263        if let id {
264            params["id"] = id
265        }
266
267        if type.usesContent {
268            params["content"] = content.trimmingCharacters(in: .whitespacesAndNewlines)
269        }
270
271        if type.usesTTL, let value = Int(ttl.trimmingCharacters(in: .whitespacesAndNewlines)) {
272            params["ttl"] = value
273        }
274
275        if type.usesPriority, let value = Int(prio.trimmingCharacters(in: .whitespacesAndNewlines)) {
276            params["prio"] = value
277        }
278
279        if type.usesWeight, let value = Int(weight.trimmingCharacters(in: .whitespacesAndNewlines)) {
280            params["weight"] = value
281        }
282
283        if type.usesPort, let value = Int(port.trimmingCharacters(in: .whitespacesAndNewlines)) {
284            params["port"] = value
285        }
286
287        if type.usesTarget {
288            params["target"] = target.trimmingCharacters(in: .whitespacesAndNewlines)
289        }
290
291        if type.usesSSHFields {
292            if let value = Int(sshAlgorithm.trimmingCharacters(in: .whitespacesAndNewlines)) {
293                params["ssh_algorithm"] = value
294            }
295            if let value = Int(sshType.trimmingCharacters(in: .whitespacesAndNewlines)) {
296                params["ssh_type"] = value
297            }
298        }
299
300        return params
301    }
302}
303
304struct DomainUpdateRequest {
305    var autorenew: Bool
306    var mailforwarding: Bool
307    var dnssec: Bool
308    var lock: Bool
309    var nameservers: [String]
310
311    var params: [String: Any] {
312        [
313            "autorenew": autorenew,
314            "mailforwarding": mailforwarding,
315            "dnssec": dnssec,
316            "lock": lock,
317            "nameservers": nameservers
318        ]
319    }
320}
321
322struct TokenCreateRequest {
323    var comment: String
324    var from: [String]
325    var allowedMethods: [String]
326
327    var params: [String: Any] {
328        var params: [String: Any] = [:]
329
330        let trimmedComment = comment.trimmingCharacters(in: .whitespacesAndNewlines)
331        if !trimmedComment.isEmpty {
332            params["comment"] = trimmedComment
333        }
334
335        if !from.isEmpty {
336            params["from"] = from
337        }
338
339        if !allowedMethods.isEmpty {
340            params["allowed_methods"] = allowedMethods
341        }
342
343        return params
344    }
345}
346
347extension String {
348    func formattedExpiry() -> String {
349        let parser = ISO8601DateFormatter()
350        guard let date = parser.date(from: self) else { return self }
351        let formatter = DateFormatter()
352        formatter.dateStyle = .medium
353        formatter.timeStyle = .none
354        return formatter.string(from: date)
355    }
356}
357
358private extension KeyedDecodingContainer where K == DNSRecord.CodingKeys {
359    func decodeLossyString(forKey key: K) throws -> String {
360        if let stringValue = try decodeIfPresent(String.self, forKey: key) {
361            return stringValue
362        }
363
364        if let intValue = try decodeIfPresent(Int.self, forKey: key) {
365            return String(intValue)
366        }
367
368        throw DecodingError.keyNotFound(
369            key,
370            DecodingError.Context(codingPath: codingPath, debugDescription: "Missing required value for \(key.stringValue).")
371        )
372    }
373
374    func decodeLossyIntIfPresent(forKey key: K) throws -> Int? {
375        if let intValue = try decodeIfPresent(Int.self, forKey: key) {
376            return intValue
377        }
378
379        if let stringValue = try decodeIfPresent(String.self, forKey: key) {
380            return Int(stringValue)
381        }
382
383        return nil
384    }
385}