From 5c39d6a17cc928d972b8dd06fe564ec300ca9148 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 3 Jun 2022 22:49:04 -0400 Subject: [PATCH] WIP: Auth --- ntfy.xcodeproj/project.pbxproj | 16 ++- .../xcschemes/xcschememanagement.plist | 4 +- ntfy/App/AppMain.swift | 1 - ntfy/Persistence/Store.swift | 31 +++++- ntfy/Persistence/SubscriptionManager.swift | 5 +- ntfy/Persistence/User.swift | 7 ++ .../Model.xcdatamodel/contents | 13 ++- ntfy/Utils/ApiService.swift | 83 +++++++++++--- ntfy/Utils/Helpers.swift | 5 +- ntfy/Views/ContentView.swift | 2 +- ntfy/Views/MainView.swift | 29 +++++ ntfy/Views/NotificationListView.swift | 6 +- ntfy/Views/SettingsView.swift | 87 +++++++++++++++ ntfy/Views/SubscriptionAddView.swift | 105 ++++++++++++++---- ntfy/Views/SubscriptionListView.swift | 2 +- ntfyNSE/NotificationService.swift | 3 +- 16 files changed, 345 insertions(+), 54 deletions(-) create mode 100644 ntfy/Persistence/User.swift create mode 100644 ntfy/Views/MainView.swift create mode 100644 ntfy/Views/SettingsView.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 7d78538..f11323b 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 02024E60283D7CBB0064224A /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02024E5F283D7CBB0064224A /* Extensions.swift */; }; + 9407EDDA284ADE1F00C1C334 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9407EDD9284ADE1F00C1C334 /* User.swift */; }; + 9407EDDB284ADE1F00C1C334 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9407EDD9284ADE1F00C1C334 /* User.swift */; }; 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1C0282F2AA700CDE4DD /* AppMain.swift */; }; 9474F1C3282F2AA700CDE4DD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1C2282F2AA700CDE4DD /* ContentView.swift */; }; 9474F1C5282F2AA800CDE4DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9474F1C4282F2AA800CDE4DD /* Assets.xcassets */; }; @@ -41,6 +43,8 @@ 94A3F7C8283734D900C48E79 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */; }; 94A3F7CA28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; }; 94A3F7CB28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; }; + 94B736D5284AF9B2003D69FB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B736D4284AF9B2003D69FB /* SettingsView.swift */; }; + 94B736D7284AF9BE003D69FB /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B736D6284AF9BE003D69FB /* MainView.swift */; }; 94CD1966283E662900973B93 /* emojis.json in Resources */ = {isa = PBXBuildFile; fileRef = 94CD1965283E662900973B93 /* emojis.json */; }; 94CD1967283E662900973B93 /* emojis.json in Resources */ = {isa = PBXBuildFile; fileRef = 94CD1965283E662900973B93 /* emojis.json */; }; 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; @@ -74,6 +78,7 @@ /* Begin PBXFileReference section */ 02024E5F283D7CBB0064224A /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 9407EDD9284ADE1F00C1C334 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 9474F1BD282F2AA700CDE4DD /* ntfy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ntfy.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9474F1C0282F2AA700CDE4DD /* AppMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 9474F1C2282F2AA700CDE4DD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -102,6 +107,8 @@ 948671492841D0CE0093C7A4 /* ActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionExecutor.swift; sourceTree = ""; }; 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = ""; }; 94A3F7C928386B2100C48E79 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 94B736D4284AF9B2003D69FB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = ""; }; 94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -189,8 +196,10 @@ 02024E5F283D7CBB0064224A /* Extensions.swift */, 9474F1C2282F2AA700CDE4DD /* ContentView.swift */, 9474F1FC2831311A00CDE4DD /* SubscriptionAddView.swift */, - 9474F20728331F3900CDE4DD /* NotificationListView.swift */, 9474F1F12830825600CDE4DD /* SubscriptionListView.swift */, + 94B736D4284AF9B2003D69FB /* SettingsView.swift */, + 94B736D6284AF9BE003D69FB /* MainView.swift */, + 9474F20728331F3900CDE4DD /* NotificationListView.swift */, ); path = Views; sourceTree = ""; @@ -203,6 +212,7 @@ 9474F1F82830835400CDE4DD /* Store.swift */, 9474F20B283321C300CDE4DD /* Notification.swift */, 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */, + 9407EDD9284ADE1F00C1C334 /* User.swift */, ); path = Persistence; sourceTree = ""; @@ -370,7 +380,10 @@ 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */, 94867143283EC9960093C7A4 /* Actions.swift in Sources */, 9474F20F283326C500CDE4DD /* ApiService.swift in Sources */, + 94B736D7284AF9BE003D69FB /* MainView.swift in Sources */, 9474F1F72830830700CDE4DD /* ntfy.xcdatamodeld in Sources */, + 9407EDDA284ADE1F00C1C334 /* User.swift in Sources */, + 94B736D5284AF9B2003D69FB /* SettingsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -378,6 +391,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9407EDDB284ADE1F00C1C334 /* User.swift in Sources */, 9474F2132834755A00CDE4DD /* Notification.swift in Sources */, 94E9196C28353E0100F30170 /* Log.swift in Sources */, 9474F2152834758700CDE4DD /* Helpers.swift in Sources */, diff --git a/ntfy.xcodeproj/xcuserdata/pheckel.xcuserdatad/xcschemes/xcschememanagement.plist b/ntfy.xcodeproj/xcuserdata/pheckel.xcuserdatad/xcschemes/xcschememanagement.plist index cffa0e4..d407d7f 100644 --- a/ntfy.xcodeproj/xcuserdata/pheckel.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ntfy.xcodeproj/xcuserdata/pheckel.xcuserdatad/xcschemes/xcschememanagement.plist @@ -28,12 +28,12 @@ ntfy.xcscheme_^#shared#^_ orderHint - 0 + 1 ntfyNSE.xcscheme_^#shared#^_ orderHint - 1 + 0 diff --git a/ntfy/App/AppMain.swift b/ntfy/App/AppMain.swift index 3bf1265..b753e28 100644 --- a/ntfy/App/AppMain.swift +++ b/ntfy/App/AppMain.swift @@ -1,7 +1,6 @@ import SwiftUI import Firebase -// TODO: Verify whether model version needs to be specified // TODO: Errors are not shown to the user, but instead just logged @main diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index f9a0380..f0caadd 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -74,11 +74,31 @@ class Store: ObservableObject { return try? context.fetch(Subscription.fetchRequest()) } + func getUser(baseUrl: String) -> User? { + let fetchRequest = User.fetchRequest() + let baseUrlPredicate = NSPredicate(format: "baseUrl = %@", baseUrl) + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [baseUrlPredicate]) + return try? context.fetch(fetchRequest).first + } + func delete(subscription: Subscription) { context.delete(subscription) try? context.save() } + func save(userBaseUrl baseUrl: String, username: String, password: String) { + do { + let user = User(context: context) + user.baseUrl = baseUrl + user.username = username + user.password = password + try context.save() + } catch let error { + Log.w(Store.tag, "Cannot store user", error) + rollbackAndRefresh() + } + } + func save(notificationFromMessage message: Message, withSubscription subscription: Subscription) { do { let notification = Notification(context: context) @@ -153,7 +173,7 @@ class Store: ObservableObject { } extension Store { - static let sampleData = [ + static let sampleMessages = [ "stats": [ // TODO: Message with action Message(id: "1", time: 1653048956, event: "message", topic: "stats", message: "In the last 24 hours, hyou had 5,000 users across 13 countries visit your website", title: "Record visitor numbers", priority: 4, tags: ["smile", "server123", "de"], actions: nil), @@ -163,15 +183,20 @@ extension Store { "backups": [], "announcements": [], "alerts": [], - "plaground": [] + "playground": [] ] static var preview: Store = { let store = Store(inMemory: true) store.context.perform { - sampleData.forEach { topic, messages in + // Subscriptions and notifications + sampleMessages.forEach { topic, messages in store.makeSubscription(store.context, topic, messages) } + + // Users + store.save(userBaseUrl: "https://ntfy.sh", username: "testuser", password: "testuser") + store.save(userBaseUrl: "https://ntfy.example.com", username: "phil", password: "phil12") } return store }() diff --git a/ntfy/Persistence/SubscriptionManager.swift b/ntfy/Persistence/SubscriptionManager.swift index ba0ce60..123b271 100644 --- a/ntfy/Persistence/SubscriptionManager.swift +++ b/ntfy/Persistence/SubscriptionManager.swift @@ -37,8 +37,9 @@ struct SubscriptionManager { } func poll(_ subscription: Subscription, completionHandler: @escaping ([Message]) -> Void) { - Log.d(tag, "Polling from \(subscription.urlString())") - ApiService.shared.poll(subscription: subscription) { messages, error in + let user = store.getUser(baseUrl: subscription.baseUrl!)?.toBasicUser() + Log.d(tag, "Polling from \(subscription.urlString()) with user \(user?.username ?? "anonymous")") + ApiService.shared.poll(subscription: subscription, user: user) { messages, error in guard let messages = messages else { Log.e(tag, "Polling failed", error) completionHandler([]) diff --git a/ntfy/Persistence/User.swift b/ntfy/Persistence/User.swift new file mode 100644 index 0000000..ab6656f --- /dev/null +++ b/ntfy/Persistence/User.swift @@ -0,0 +1,7 @@ +import Foundation + +extension User { + func toBasicUser() -> BasicUser { + return BasicUser(username: username ?? "?", password: password ?? "?") + } +} diff --git a/ntfy/Persistence/ntfy.xcdatamodeld/Model.xcdatamodel/contents b/ntfy/Persistence/ntfy.xcdatamodeld/Model.xcdatamodel/contents index 42e9c06..53f6214 100644 --- a/ntfy/Persistence/ntfy.xcdatamodeld/Model.xcdatamodel/contents +++ b/ntfy/Persistence/ntfy.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -28,8 +28,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ntfy/Utils/ApiService.swift b/ntfy/Utils/ApiService.swift index b398c46..b419e44 100644 --- a/ntfy/Utils/ApiService.swift +++ b/ntfy/Utils/ApiService.swift @@ -6,7 +6,7 @@ class ApiService { private let tag = "ApiService" - func poll(subscription: Subscription, completionHandler: @escaping ([Message]?, Error?) -> Void) { + func poll(subscription: Subscription, user: BasicUser?, completionHandler: @escaping ([Message]?, Error?) -> Void) { guard let url = URL(string: subscription.urlString()) else { // FIXME return @@ -14,17 +14,15 @@ class ApiService { let since = subscription.lastNotificationId ?? "all" let urlString = "\(url)/json?poll=1&since=\(since)" - Log.d(tag, "Polling from \(urlString)") - fetchJsonData(urlString: urlString, completionHandler: completionHandler) + Log.d(tag, "Polling from \(urlString) with user \(user?.username ?? "anonymous")") + fetchJsonData(urlString: urlString, user: user, completionHandler: completionHandler) } - func poll(subscription: Subscription, messageId: String, completionHandler: @escaping (Message?, Error?) -> Void) { + func poll(subscription: Subscription, messageId: String, user: BasicUser?, completionHandler: @escaping (Message?, Error?) -> Void) { let url = URL(string: "\(subscription.urlString())/json?poll=1&id=\(messageId)")! - Log.d(tag, "Polling single message from \(url)") + Log.d(tag, "Polling single message from \(url) with user \(user?.username ?? "anonymous")") - var request = URLRequest(url: url) - request.setValue(ApiService.userAgent, forHTTPHeaderField: "User-Agent") - + let request = newRequest(url: url, user: user) URLSession.shared.dataTask(with: request) { (data, response, error) in if let error = error { completionHandler(nil, error) @@ -41,18 +39,18 @@ class ApiService { func publish( subscription: Subscription, + user: BasicUser?, message: String, title: String, priority: Int = 3, tags: [String] = [] ) { guard let url = URL(string: subscription.urlString()) else { return } - var request = URLRequest(url: url) + var request = newRequest(url: url, user: user) Log.d(tag, "Publishing to \(url)") request.httpMethod = "POST" - request.setValue(ApiService.userAgent, forHTTPHeaderField: "User-Agent") request.setValue(title, forHTTPHeaderField: "Title") request.setValue(String(priority), forHTTPHeaderField: "Priority") request.setValue(tags.joined(separator: ","), forHTTPHeaderField: "Tags") @@ -65,12 +63,32 @@ class ApiService { Log.d(self.tag, "Publishing message succeeded", response) }.resume() } + + func checkAuth(baseUrl: String, topic: String, user: BasicUser?, completionHandler: @escaping(AuthCheckResponse?, Error?) -> Void) { + guard let url = URL(string: topicAuthUrl(baseUrl: baseUrl, topic: topic)) else { return } + let request = newRequest(url: url, user: user) + Log.d(tag, "Checking auth for \(url) with user \(user?.username ?? "anonymous")") + URLSession.shared.dataTask(with: request) { (data, response, error) in + if let error = error { + Log.e(self.tag, "Error checking auth: \(error)") + completionHandler(nil, error) + } + if let data = data { + do { + let result = try JSONDecoder().decode(AuthCheckResponse.self, from: data) + Log.d(self.tag, "Auth result: \(result)") + completionHandler(result, nil) + } catch { + Log.e(self.tag, "Error handling auth response: \(error)") + completionHandler(nil, error) + } + } + }.resume() + } - private func fetchJsonData(urlString: String, completionHandler: @escaping ([T]?, Error?) -> ()) { + private func fetchJsonData(urlString: String, user: BasicUser?, completionHandler: @escaping ([T]?, Error?) -> ()) { guard let url = URL(string: urlString) else { return } - var request = URLRequest(url: url) - request.setValue(ApiService.userAgent, forHTTPHeaderField: "User-Agent") - + let request = newRequest(url: url, user: user) URLSession.shared.dataTask(with: request) { (data, response, error) in if let error = error { Log.e(self.tag, "Error fetching data", error) @@ -90,4 +108,41 @@ class ApiService { } }.resume() } + + private func newRequest(url: URL, user: BasicUser?) -> URLRequest { + var request = URLRequest(url: url) + request.setValue(ApiService.userAgent, forHTTPHeaderField: "User-Agent") + if let user = user { + request.setValue(user.toHeader(), forHTTPHeaderField: "Authorization") + } + return request + } +} + +struct BasicUser { + let username: String + let password: String + + func toHeader() -> String { + return "Basic " + String(format: "%@:%@", username, password).data(using: String.Encoding.utf8)!.base64EncodedString() + } +} + +struct AuthCheckResponse: Codable { + let success: Bool? + let code: Int? + let http: Int? + let error: String? + + enum CodingKeys: String, CodingKey { + case success, code, http, error + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.success = try container.decodeIfPresent(Bool.self, forKey: .success) + self.code = try container.decodeIfPresent(Int.self, forKey: .code) + self.http = try container.decodeIfPresent(Int.self, forKey: .http) + self.error = try container.decodeIfPresent(String.self, forKey: .error) + } } diff --git a/ntfy/Utils/Helpers.swift b/ntfy/Utils/Helpers.swift index 34c2377..9973f01 100644 --- a/ntfy/Utils/Helpers.swift +++ b/ntfy/Utils/Helpers.swift @@ -11,6 +11,10 @@ func topicShortUrl(baseUrl: String, topic: String) -> String { .replacingOccurrences(of: "https://", with: "") } +func topicAuthUrl(baseUrl: String, topic: String) -> String { + return "\(baseUrl)/\(topic)/auth" +} + func topicHash(baseUrl: String, topic: String) -> String { let data = Data(topicUrl(baseUrl: baseUrl, topic: topic).utf8) let digest = SHA256.hash(data: data) @@ -41,4 +45,3 @@ func parseNonEmojiTags(_ tags: String?) -> [String] { return parseAllTags(tags) .filter { EmojiManager.shared.getEmojiByAlias(alias: $0) == nil } } - diff --git a/ntfy/Views/ContentView.swift b/ntfy/Views/ContentView.swift index 43a1523..cc008b3 100644 --- a/ntfy/Views/ContentView.swift +++ b/ntfy/Views/ContentView.swift @@ -2,7 +2,7 @@ import SwiftUI struct ContentView: View { var body: some View { - SubscriptionListView() + MainView() } } diff --git a/ntfy/Views/MainView.swift b/ntfy/Views/MainView.swift new file mode 100644 index 0000000..2abf1d4 --- /dev/null +++ b/ntfy/Views/MainView.swift @@ -0,0 +1,29 @@ +import Foundation +import SwiftUI + +struct MainView: View { + var body: some View { + TabView { + SubscriptionListView() + .tabItem { + Image(systemName: "message.fill") + Text("Notifications") + } + SettingsView() + .tabItem { + Image(systemName: "gearshape.fill") + Text("Settings") + } + } + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + let store = Store.preview // Store.previewEmpty + MainView() + .environment(\.managedObjectContext, store.context) + .environmentObject(store) + .environmentObject(AppDelegate()) + } +} diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index 357bd55..124dc95 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -187,8 +187,10 @@ struct NotificationListView: View { let priority = Int.random(in: 1..<6) let tags = Array(possibleTags.shuffled().prefix(Int.random(in: 0..<4))) DispatchQueue.global(qos: .background).async { + let user = store.getUser(baseUrl: subscription.baseUrl!)?.toBasicUser() ApiService.shared.publish( subscription: subscription, + user: user, message: "This is a test notification from the ntfy iOS app. It has a priority of \(priority). If you send another one, it may look different.", title: "Test: You can set a title if you like", priority: priority, @@ -327,8 +329,8 @@ struct NotificationListView_Previews: PreviewProvider { static var previews: some View { let store = Store.preview Group { - let subscriptionWithNotifications = store.makeSubscription(store.context, "stats", Store.sampleData["stats"]!) - let subscriptionWithoutNotifications = store.makeSubscription(store.context, "announcements", Store.sampleData["announcements"]!) + let subscriptionWithNotifications = store.makeSubscription(store.context, "stats", Store.sampleMessages["stats"]!) + let subscriptionWithoutNotifications = store.makeSubscription(store.context, "announcements", Store.sampleMessages["announcements"]!) NotificationListView(subscription: subscriptionWithNotifications) .environment(\.managedObjectContext, store.context) .environmentObject(store) diff --git a/ntfy/Views/SettingsView.swift b/ntfy/Views/SettingsView.swift new file mode 100644 index 0000000..cf8cb58 --- /dev/null +++ b/ntfy/Views/SettingsView.swift @@ -0,0 +1,87 @@ +import Foundation +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject private var store: Store + @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \User.baseUrl, ascending: true)]) var users: FetchedResults + + var body: some View { + NavigationView { + Form { + /*Section(header: Text("General")) { + NavigationLink(destination: UsersView()) { + Text("Manage users") + } + }*/ + Section( + header: Text("Users") + ) { + List { + ForEach(users) { user in + HStack { + Image(systemName: "person.fill") + VStack(alignment: .leading, spacing: 0) { + Text(user.username ?? "?") + + Text(user.baseUrl ?? "?") + .font(.subheadline) + .foregroundColor(.gray) + } + } + } + HStack { + Image(systemName: "plus") + Text("Add user") + } + } + } + Section(header: Text("About")) { + HStack { + Text("Version") + .foregroundColor(.gray) + Spacer() + Text("ntfy 1.1") + } + } + } + .navigationTitle("Settings") + + } + .navigationViewStyle(StackNavigationViewStyle()) + + } +} + +struct UsersView: View { + @EnvironmentObject private var store: Store + @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \User.baseUrl, ascending: true)]) var users: FetchedResults + + var body: some View { + List { + ForEach(users) { user in + Text(user.username ?? "") + } + } + .listStyle(PlainListStyle()) + .navigationTitle("Manage users") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + //self.showingAddDialog = true + } label: { + Image(systemName: "plus") + } + } + } + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + let store = Store.preview // Store.previewEmpty + SettingsView() + .environment(\.managedObjectContext, store.context) + .environmentObject(store) + .environmentObject(AppDelegate()) + } +} diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index c67fe35..f39d7a0 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -10,33 +10,35 @@ struct SubscriptionAddView: View { @State private var useAnother: Bool = false @State private var baseUrl: String = "" + @State private var showLogin: Bool = false + @State private var username: String = "" + @State private var password: String = "" + private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) } var body: some View { NavigationView { - VStack { - Form { - Section( - footer: - Text("Topics are not password protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications") - ) { - TextField("Topic name, e.g. phil_alerts", text: $topic) + Form { + Section( + footer: + Text("Topics are not password protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications") + ) { + TextField("Topic name, e.g. phil_alerts", text: $topic) + .disableAutocapitalization() + .disableAutocorrection(true) + } + Section( + footer: + (useAnother) ? Text("Support for self-hosted servers is currently limited. To ensure instant delivery, be sure to set upstream-base-url in your server's config, otherwise messages may arrive with significant delay. Auth is not yet supported.") : Text("") + ) { + Toggle("Use another server", isOn: $useAnother) + if useAnother { + TextField("Server URL, e.g. https://ntfy.example.com", text: $baseUrl) .disableAutocapitalization() .disableAutocorrection(true) } - Section( - footer: - (useAnother) ? Text("Support for self-hosted servers is currently limited. To ensure instant delivery, be sure to set upstream-base-url in your server's config, otherwise messages may arrive with significant delay. Auth is not yet supported.") : Text("") - ) { - Toggle("Use another server", isOn: $useAnother) - if useAnother { - TextField("Server URL, e.g. https://ntfy.example.com", text: $baseUrl) - .disableAutocapitalization() - .disableAutocorrection(true) - } - } } } .navigationTitle("Add subscription") @@ -48,12 +50,46 @@ struct SubscriptionAddView: View { } } ToolbarItem(placement: .navigationBarTrailing) { - Button(action: subscribeAction) { + Button(action: subscribeOrShowLoginAction) { Text("Subscribe") } .disabled(!isValid()) } } + .background(Group { + NavigationLink( + destination: loginView, + isActive: $showLogin + ) { + EmptyView() + } + }) + } + } + + private var loginView: some View { + Form { + Section( + footer: + Text("This topic requires that you login with username and password. The user will be stored on your device, and will be re-used for other topics.") + ) { + TextField("Username", text: $username) + .disableAutocapitalization() + .disableAutocorrection(true) + TextField("Password", text: $password) + .disableAutocapitalization() + .disableAutocorrection(true) + } + } + .navigationTitle("Login required") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: subscribeWithUserAction) { + Text("Subscribe") + } + .disabled(!isValid()) + } } } @@ -72,11 +108,33 @@ struct SubscriptionAddView: View { return true } - private func subscribeAction() { - DispatchQueue.global(qos: .background).async { - subscriptionManager.subscribe(baseUrl: selectedBaseUrl, topic: sanitizedTopic) + private func subscribeOrShowLoginAction() { + let user = store.getUser(baseUrl: selectedBaseUrl)?.toBasicUser() + ApiService.shared.checkAuth(baseUrl: selectedBaseUrl, topic: topic, user: user) { (response, error) in + if response?.success == true { + DispatchQueue.global(qos: .background).async { + subscriptionManager.subscribe(baseUrl: selectedBaseUrl, topic: sanitizedTopic) + } + isShowing = false + } else { + showLogin = true + } + } + } + + private func subscribeWithUserAction() { + let user = BasicUser(username: username, password: password) + ApiService.shared.checkAuth(baseUrl: selectedBaseUrl, topic: topic, user: user) { (response, error) in + if response?.success == true { + DispatchQueue.global(qos: .background).async { + store.save(userBaseUrl: selectedBaseUrl, username: username, password: password) + subscriptionManager.subscribe(baseUrl: selectedBaseUrl, topic: sanitizedTopic) + } + isShowing = false + } else { + showLogin = true + } } - isShowing = false } private func cancelAction() { @@ -88,7 +146,6 @@ struct SubscriptionAddView: View { } } - struct SubscriptionAddView_Previews: PreviewProvider { @State static var isShowing = true diff --git a/ntfy/Views/SubscriptionListView.swift b/ntfy/Views/SubscriptionListView.swift index 1716737..537b727 100644 --- a/ntfy/Views/SubscriptionListView.swift +++ b/ntfy/Views/SubscriptionListView.swift @@ -168,7 +168,7 @@ struct SubscriptionItemRowView: View { } } -struct SubscriptionsListView_Previews: PreviewProvider { +struct SubscriptionListView_Previews: PreviewProvider { static var previews: some View { let store = Store.preview // Store.previewEmpty SubscriptionListView() diff --git a/ntfyNSE/NotificationService.swift b/ntfyNSE/NotificationService.swift index 0a53655..84e25d8 100644 --- a/ntfyNSE/NotificationService.swift +++ b/ntfyNSE/NotificationService.swift @@ -79,8 +79,9 @@ class NotificationService: UNNotificationServiceExtension { } // Poll original server + let user = store?.getUser(baseUrl: baseUrl)?.toBasicUser() let semaphore = DispatchSemaphore(value: 0) - ApiService.shared.poll(subscription: subscription, messageId: pollId) { message, error in + ApiService.shared.poll(subscription: subscription, messageId: pollId, user: user) { message, error in guard let message = message else { Log.w(self.tag, "Error fetching message", error) contentHandler(request.content)