Remove user auth, refresh subscription view when adding new sub

This commit is contained in:
Philipp Heckel 2022-05-07 14:31:39 -04:00
parent 22fb9748b7
commit 676626998c
13 changed files with 39 additions and 254 deletions

View file

@ -12,9 +12,11 @@ struct AppMain: App {
// Set App Delegate
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var subscriptions = NtfySubscriptionList()
var body: some Scene {
WindowGroup {
ContentView()
ContentView(subscriptions: subscriptions)
}
}
}

View file

@ -70,9 +70,9 @@ class NtfySubscription: ObservableObject, Identifiable {
return notifications.first
}
func fetchNewNotifications(user: NtfyUser?, completionHandler: ( ([NtfyNotification]?, Error?) -> Void)?) {
func fetchNewNotifications(completionHandler: ( ([NtfyNotification]?, Error?) -> Void)?) {
var newNotifications = [NtfyNotification]()
ApiService.shared.poll(subscription: self, user: user) { (notifications, error) in
ApiService.shared.poll(subscription: self) { (notifications, error) in
if let notifications = notifications {
for notification in notifications {
if (notification.save() != nil) {
@ -96,4 +96,8 @@ class NtfySubscriptionList: ObservableObject {
init() {
self.subscriptions = Database.current.getSubscriptions()
}
func refresh() {
self.subscriptions = Database.current.getSubscriptions()
}
}

View file

@ -1,20 +0,0 @@
//
// NtfyUser.swift
// ntfy.sh
//
// Created by Andrew Cope on 2/20/22.
//
import Foundation
class NtfyUser: Identifiable {
var baseUrl: String
var username: String
var password: String
init(baseUrl: String, username: String, password: String) {
self.baseUrl = baseUrl
self.username = username
self.password = password
}
}

View file

@ -10,22 +10,17 @@ import Foundation
class ApiService: NSObject {
static let shared = ApiService()
func poll(subscription: NtfySubscription, user: NtfyUser?, completionHandler: @escaping ([NtfyNotification]?, Error?) -> Void) {
func poll(subscription: NtfySubscription, completionHandler: @escaping ([NtfyNotification]?, Error?) -> Void) {
let lastNotificationTime = subscription.lastNotification()?.timestamp ?? 0
let sinceString = lastNotificationTime > 0 ? String(lastNotificationTime) : "all";
let urlString = "\(subscription.urlString())/json?poll=1&since=\(sinceString)"
fetchJsonData(urlString: urlString, user: user, completionHandler: completionHandler)
fetchJsonData(urlString: urlString, completionHandler: completionHandler)
}
func publish(subscription: NtfySubscription, message: String, title: String, priority: Int = 3, tags: [String] = [], user: NtfyUser?, completionHandler: @escaping (NtfyNotification?, Error?) -> Void) {
func publish(subscription: NtfySubscription, message: String, title: String, priority: Int = 3, tags: [String] = [], completionHandler: @escaping (NtfyNotification?, Error?) -> Void) {
guard let url = URL(string: subscription.urlString()) else { return }
var request = URLRequest(url: url)
if let user = user {
let credentials = Credentials.Basic(username: user.username, password: user.password)
request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization")
}
request.httpMethod = "POST"
request.setValue(title, forHTTPHeaderField: "Title")
request.setValue(String(priority), forHTTPHeaderField: "Priority")
@ -38,13 +33,9 @@ class ApiService: NSObject {
}.resume()
}
func checkAuth(baseUrl: String, topic: String, user: NtfyUser?, completionHandler: @escaping(AuthCheckResponse?, Error?) -> Void) {
func checkAuth(baseUrl: String, topic: String, completionHandler: @escaping(AuthCheckResponse?, Error?) -> Void) {
guard let url = URL(string: "\(baseUrl)/\(topic)/auth") else { return }
var request = URLRequest(url: url)
if let user = user {
let credentials = Credentials.Basic(username: user.username, password: user.password)
request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization")
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Error checking auth: \(error)")
@ -63,14 +54,9 @@ class ApiService: NSObject {
}.resume()
}
private func fetchJsonData<T: Decodable>(urlString: String, user: NtfyUser?, completionHandler: @escaping ([T]?, Error?) -> ()) {
private func fetchJsonData<T: Decodable>(urlString: String, completionHandler: @escaping ([T]?, Error?) -> ()) {
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url: url)
if let user = user {
let credentials = Credentials.Basic(username: user.username, password: user.password)
request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization")
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {

View file

@ -1,14 +0,0 @@
//
// Credentials.swift
// ntfy.sh
//
// Created by Andrew Cope on 2/20/22.
//
import Foundation
enum Credentials {
static func Basic(username: String, password: String) -> String {
return String(format: "%@:%@", username, password).data(using: String.Encoding.utf8)!.base64EncodedString()
}
}

View file

@ -47,12 +47,6 @@ class Database {
let attachment_url = Expression<String>("url")
let attachment_content_url = Expression<String>("contentUrl")
// Users Table
let users = Table("Users")
let user_base_url = Expression<String>("baseUrl")
let user_username = Expression<String>("username")
let user_password = Expression<String>("password")
// Initialize
init() {
do {
@ -60,7 +54,7 @@ class Database {
// Get the App Group path, which is accessed by both the app and the notification service extension
if let path = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.ntfy") {
// Connect to the database
db = try Connection("\(path.path)/ntfy.sh.sqlite3")
db = try Connection("\(path.path)/ntfy.sqlite3")
// Initialize Subscriptions table
try db?.run(subscriptions.create(ifNotExists: true) { table in
@ -91,14 +85,6 @@ class Database {
table.column(attachment_url)
table.column(attachment_content_url)
})
// Initialize Users Table
try db?.run(users.create(ifNotExists: true) { table in
table.column(user_base_url)
table.column(user_username)
table.column(user_password)
table.primaryKey(user_base_url)
})
}
} catch {
print(error.localizedDescription)
@ -308,45 +294,4 @@ class Database {
print("Error updating attachment: \(error)")
}
}
func addUser(user: NtfyUser) {
do {
try db?.run(users.insert(
user_base_url <- user.baseUrl,
user_username <- user.username,
user_password <- user.password
))
} catch {
print(error)
}
}
func deleteUser(user: NtfyUser) {
do {
let line = users.filter(user_base_url == user.baseUrl && user_username == user.username)
try db?.run(line.delete())
} catch {
print(error)
}
}
func findUsers(baseUrl: String) -> [NtfyUser] {
var ntfyUsers = [NtfyUser]()
do {
let query = users.filter(user_base_url == baseUrl)
if let result = try db?.prepare(query) {
for line in result {
let user = NtfyUser(
baseUrl: try line.get(user_base_url),
username: try line.get(user_username),
password: try line.get(user_password)
)
ntfyUsers.append(user)
}
}
} catch {
print(error)
}
return ntfyUsers
}
}

View file

@ -8,18 +8,16 @@
import SwiftUI
struct AddSubscriptionView: View {
@ObservedObject var subscriptions: NtfySubscriptionList
@State private var topic: String = ""
@State private var baseUrl: String = Configuration.appBaseUrl
@State private var showLogin: Bool = false
@State private var username: String = ""
@State private var password: String = ""
@State private var showAlert = false
@State private var activeAlert: AddSubscriptionView.ActiveAlert = .invalidTopic
@State private var authFailureError = ""
@Binding var currentView: CurrentView
var body: some View {
NavigationView {
Form {
@ -30,16 +28,6 @@ struct AddSubscriptionView: View {
TextField("Topic name, e.g. server_alerts", text: $topic)
.textInputAutocapitalization(.never)
}
if showLogin {
Section(
header: Text("Login")
) {
TextField("Username", text: $username)
.textInputAutocapitalization(.none)
.disableAutocorrection(true)
SecureField("Password", text: $password)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -61,7 +49,7 @@ struct AddSubscriptionView: View {
Validation function:
1. Topic is not empty
2. Topic matches regex? Should match Firebase topic regex
Authentication function:
1. Get baseUrl
2. Get user for baseUrl
@ -69,14 +57,14 @@ struct AddSubscriptionView: View {
3. If authorized, continue to subscribe
4. Else if user != null, access not allowed to topic but user exists
5. Else (user is null), access not allowed, show login view
Login function:
1. Login user / pass view
2. api.checkAuth(baseUrl, topic, user -> user / pass)
3. If authorized, save user to database, continue to subscribe
4. Else access not allowed, show login view again
Subscribe function:
1. Create subscription
2. Add subscription to database
@ -84,32 +72,18 @@ struct AddSubscriptionView: View {
4. Fetch cached messages
5. Switch to SubscriptionDetail view
*/
var user = Database.current.findUsers(baseUrl: baseUrl).first
if showLogin {
print("Authorization via UI forms")
if (user != nil) {
user!.username = username
user!.password = password
} else {
user = NtfyUser(baseUrl: baseUrl, username: username, password: password)
}
}
ApiService.shared.checkAuth(baseUrl: baseUrl, topic: sanitizedTopic, user: user) { authResponse, error in
ApiService.shared.checkAuth(baseUrl: baseUrl, topic: sanitizedTopic) { authResponse, error in
if let authResponse = authResponse {
if let success = authResponse.success, success {
if user != nil {
Database.current.addUser(user: user!)
}
let subscription = NtfySubscription(id: Int64(arc4random()), baseUrl: baseUrl, topic: sanitizedTopic)
subscription.save()
if baseUrl == Configuration.appBaseUrl {
subscription.subscribe(to: sanitizedTopic)
}
subscription.fetchNewNotifications(user: user, completionHandler: nil)
subscriptions.refresh()
subscription.fetchNewNotifications( completionHandler: nil)
currentView = .subscriptionList
showLogin = false
} else {
showLogin = true
showAlert = true
activeAlert = .requiresAuth
}
@ -163,11 +137,11 @@ struct AddSubscriptionView: View {
}
}
}
private func sanitizeTopic(topic: String) -> String {
return topic.trimmingCharacters(in: [" "])
}
private func isTopicValid(topic: String) -> Bool {
return !topic.isEmpty && (topic.range(of: "^[-_A-Za-z0-9]{1,64}$", options: .regularExpression, range: nil, locale: nil) != nil)
}

View file

@ -8,22 +8,19 @@
import SwiftUI
struct ContentView: View {
@State var addingSubscription = false
@State var managingUsers = false
@ObservedObject var subscriptions: NtfySubscriptionList
@State var currentView = CurrentView.subscriptionList
var body: some View {
switch (currentView) {
case .managingUsers:
UserManagementView(currentView: $currentView)
case .addingSubscription:
AddSubscriptionView(currentView: $currentView)
AddSubscriptionView(subscriptions: subscriptions, currentView: $currentView)
case .subscriptionList:
SubscriptionsList(currentView: $currentView)
SubscriptionsList(subscriptions: subscriptions, currentView: $currentView)
}
}
}
enum CurrentView {
case addingSubscription, managingUsers, subscriptionList
case addingSubscription, subscriptionList
}

View file

@ -25,7 +25,6 @@ struct SubscriptionDetail: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
let user = Database.current.findUsers(baseUrl: subscription.baseUrl).first
NavigationView {
List(selection: $selection) {
ForEach(subscription.notifications, id: \.self) { notification in
@ -56,8 +55,7 @@ struct SubscriptionDetail: View {
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,
tags: tags,
user: user
tags: tags
) { _,_ in
print("Success")
}
@ -132,7 +130,7 @@ struct SubscriptionDetail: View {
}
})
.refreshable {
subscription.fetchNewNotifications(user: user, completionHandler: nil)
subscription.fetchNewNotifications(completionHandler: nil)
}
.onAppear {
subscription.loadNotifications()
@ -175,8 +173,8 @@ class SubscriptionDetailViewModel: ObservableObject {
notifications = Database.current.getNotifications(subscription: subscription)
}
func fetchNewNotifications(subscription: NtfySubscription, user: NtfyUser?) {
subscription.fetchNewNotifications(user: user) { (_, _) in
func fetchNewNotifications(subscription: NtfySubscription) {
subscription.fetchNewNotifications { (_, _) in
self.loadNotifications(subscription: subscription)
}
}

View file

@ -8,8 +8,7 @@
import SwiftUI
struct SubscriptionsList: View {
@StateObject var subscriptions = NtfySubscriptionList()
@ObservedObject var subscriptions: NtfySubscriptionList
@Binding var currentView: CurrentView
var body: some View {
@ -47,13 +46,6 @@ struct SubscriptionsList: View {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
currentView = .managingUsers
}) {
Text("Users")
}
}
}
.overlay(Group {
if subscriptions.subscriptions.isEmpty {
@ -65,7 +57,7 @@ struct SubscriptionsList: View {
}
.navigationViewStyle(StackNavigationViewStyle())
.onAppear {
self.subscriptions.objectWillChange.send()
subscriptions.objectWillChange.send()
}
}
}

View file

@ -1,63 +0,0 @@
//
// UserManagementView.swift
// ntfy.sh
//
// Created by Andrew Cope on 2/27/22.
//
import Foundation
import SwiftUI
struct UserManagementView: View {
@ObservedObject var viewModel = UserManagementViewModel()
@Binding var currentView: CurrentView
var body: some View {
NavigationView {
List {
Section(header: Text(Configuration.appBaseUrl)) {
ForEach(viewModel.users) { user in
Text(user.username)
.swipeActions {
Button(role: .destructive) {
viewModel.deleteUser(user: user)
} label: {
Label("Delete", systemImage: "trash.circle")
}
}
}
}
}
.listStyle(GroupedListStyle())
.navigationTitle("Manage Users")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
currentView = .subscriptionList
}) {
Text("Topics")
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.onAppear {
viewModel.loadUsers()
}
}
}
class UserManagementViewModel: ObservableObject {
@Published var users = [NtfyUser]()
func loadUsers() {
self.users = Database.current.findUsers(baseUrl: Configuration.appBaseUrl)
}
func deleteUser(user: NtfyUser) {
Database.current.deleteUser(user: user)
self.loadUsers()
}
}

View file

@ -25,15 +25,10 @@
8079FF4C27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */; };
8079FF4D27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */; };
8079FF4F27C29D3300FB3D18 /* NotificationAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF4E27C29D3300FB3D18 /* NotificationAttachmentView.swift */; };
8079FF5227C2C38700FB3D18 /* NtfyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF5127C2C38700FB3D18 /* NtfyUser.swift */; };
8079FF5327C2C38700FB3D18 /* NtfyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF5127C2C38700FB3D18 /* NtfyUser.swift */; };
8079FF5527C2C49200FB3D18 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF5427C2C49200FB3D18 /* Credentials.swift */; };
8079FF5627C2C49200FB3D18 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF5427C2C49200FB3D18 /* Credentials.swift */; };
80856C7F27BDE0A7008AC8B8 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80856C7E27BDE0A7008AC8B8 /* ApiService.swift */; };
80856C8027BDE0A7008AC8B8 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80856C7E27BDE0A7008AC8B8 /* ApiService.swift */; };
8086EB242794630800C3628A /* AddSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8086EB232794630800C3628A /* AddSubscriptionView.swift */; };
8086EB2627946FCE00C3628A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8086EB2527946FCE00C3628A /* ContentView.swift */; };
808833B427CC32010098927E /* UserManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808833B327CC32010098927E /* UserManagementView.swift */; };
80A313F52793B1CF00F1A639 /* NtfySubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A313F42793B1CF00F1A639 /* NtfySubscription.swift */; };
80A313F72793B56800F1A639 /* NtfyNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A313F62793B56800F1A639 /* NtfyNotification.swift */; };
80A313F92793C0D800F1A639 /* SubscriptionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A313F82793C0D800F1A639 /* SubscriptionRow.swift */; };
@ -88,13 +83,10 @@
80386EA0279363A2009B0480 /* ntfy.sh.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ntfy.sh.entitlements; sourceTree = "<group>"; };
8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfyAttachment.swift; sourceTree = "<group>"; };
8079FF4E27C29D3300FB3D18 /* NotificationAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAttachmentView.swift; sourceTree = "<group>"; };
8079FF5127C2C38700FB3D18 /* NtfyUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfyUser.swift; sourceTree = "<group>"; };
8079FF5427C2C49200FB3D18 /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
8081A7F427B4CB67004A8986 /* FEATURE_PARITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FEATURE_PARITY.md; sourceTree = "<group>"; };
80856C7E27BDE0A7008AC8B8 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = "<group>"; };
8086EB232794630800C3628A /* AddSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSubscriptionView.swift; sourceTree = "<group>"; };
8086EB2527946FCE00C3628A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
808833B327CC32010098927E /* UserManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManagementView.swift; sourceTree = "<group>"; };
80910E1127C420B50074B05A /* GETTING_STARTED.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = GETTING_STARTED.md; sourceTree = "<group>"; };
80910E1227C422140074B05A /* TECHNICAL_LIMITATIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TECHNICAL_LIMITATIONS.md; sourceTree = "<group>"; };
80A313F42793B1CF00F1A639 /* NtfySubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfySubscription.swift; sourceTree = "<group>"; };
@ -147,7 +139,6 @@
80E8CED627B56DB200FDC5E0 /* EmojiManager.swift */,
80ED61BD27BCA36A00FCEA36 /* Configuration.swift */,
80856C7E27BDE0A7008AC8B8 /* ApiService.swift */,
8079FF5427C2C49200FB3D18 /* Credentials.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -232,7 +223,6 @@
8086EB232794630800C3628A /* AddSubscriptionView.swift */,
8086EB2527946FCE00C3628A /* ContentView.swift */,
8079FF4E27C29D3300FB3D18 /* NotificationAttachmentView.swift */,
808833B327CC32010098927E /* UserManagementView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -261,7 +251,6 @@
80A313F42793B1CF00F1A639 /* NtfySubscription.swift */,
80A313F62793B56800F1A639 /* NtfyNotification.swift */,
8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */,
8079FF5127C2C38700FB3D18 /* NtfyUser.swift */,
);
path = Models;
sourceTree = "<group>";
@ -404,13 +393,11 @@
files = (
802D626C27B1A37700DDD3AF /* Database.swift in Sources */,
8079FF4D27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */,
8079FF5627C2C49200FB3D18 /* Credentials.swift in Sources */,
80ED61BF27BCA36A00FCEA36 /* Configuration.swift in Sources */,
802D626D27B1A37900DDD3AF /* NtfyNotification.swift in Sources */,
800FA49627B19CA0005D05B9 /* NotificationService.swift in Sources */,
80E8CED827B56DB200FDC5E0 /* EmojiManager.swift in Sources */,
80856C8027BDE0A7008AC8B8 /* ApiService.swift in Sources */,
8079FF5327C2C38700FB3D18 /* NtfyUser.swift in Sources */,
802D626E27B1A37C00DDD3AF /* NtfySubscription.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -426,13 +413,10 @@
8015C3DC2793AB1500E6F001 /* Database.swift in Sources */,
80E8CED727B56DB200FDC5E0 /* EmojiManager.swift in Sources */,
8086EB242794630800C3628A /* AddSubscriptionView.swift in Sources */,
8079FF5227C2C38700FB3D18 /* NtfyUser.swift in Sources */,
808833B427CC32010098927E /* UserManagementView.swift in Sources */,
80A313FD2793C42000F1A639 /* NotificationRow.swift in Sources */,
80A313FB2793C2EA00F1A639 /* SubscriptionDetail.swift in Sources */,
8079FF4C27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */,
80A313F92793C0D800F1A639 /* SubscriptionRow.swift in Sources */,
8079FF5527C2C49200FB3D18 /* Credentials.swift in Sources */,
80386E802793585B009B0480 /* AppMain.swift in Sources */,
8079FF4F27C29D3300FB3D18 /* NotificationAttachmentView.swift in Sources */,
80A313F72793B56800F1A639 /* NtfyNotification.swift in Sources */,