WIP: Auth

This commit is contained in:
Philipp Heckel 2022-06-03 22:49:04 -04:00
parent 0bb8d22f04
commit 5c39d6a17c
16 changed files with 345 additions and 54 deletions

View file

@ -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 = "<group>"; };
9407EDD9284ADE1F00C1C334 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
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 = "<group>"; };
9474F1C2282F2AA700CDE4DD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -102,6 +107,8 @@
948671492841D0CE0093C7A4 /* ActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionExecutor.swift; sourceTree = "<group>"; };
94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
94A3F7C928386B2100C48E79 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
94B736D4284AF9B2003D69FB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = "<group>"; };
94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -203,6 +212,7 @@
9474F1F82830835400CDE4DD /* Store.swift */,
9474F20B283321C300CDE4DD /* Notification.swift */,
94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */,
9407EDD9284ADE1F00C1C334 /* User.swift */,
);
path = Persistence;
sourceTree = "<group>";
@ -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 */,

View file

@ -28,12 +28,12 @@
<key>ntfy.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
<key>ntfyNSE.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
</dict>
</dict>

View file

@ -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

View file

@ -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
}()

View file

@ -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([])

View file

@ -0,0 +1,7 @@
import Foundation
extension User {
func toBasicUser() -> BasicUser {
return BasicUser(username: username ?? "?", password: password ?? "?")
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E258" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="actions" optional="YES" attributeType="String"/>
<attribute name="click" optional="YES" attributeType="String"/>
@ -28,8 +28,19 @@
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="User" representedClassName="User" syncable="YES" codeGenerationType="class">
<attribute name="baseUrl" attributeType="String"/>
<attribute name="password" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="baseUrl"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Notification" positionX="-54" positionY="9" width="128" height="164"/>
<element name="Subscription" positionX="-262.4760131835938" positionY="11.46405029296875" width="128" height="89"/>
<element name="User" positionX="-162" positionY="81" width="128" height="74"/>
</elements>
</model>

View file

@ -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<T: Decodable>(urlString: String, completionHandler: @escaping ([T]?, Error?) -> ()) {
private func fetchJsonData<T: Decodable>(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)
}
}

View file

@ -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 }
}

View file

@ -2,7 +2,7 @@ import SwiftUI
struct ContentView: View {
var body: some View {
SubscriptionListView()
MainView()
}
}

29
ntfy/Views/MainView.swift Normal file
View file

@ -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())
}
}

View file

@ -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)

View file

@ -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<User>
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<User>
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())
}
}

View file

@ -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

View file

@ -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()

View file

@ -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)