diff --git a/README.md b/README.md index ce79fe6..cfbe20f 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,16 @@ First visit the Fax Server's address and register a new user account after you c - [Advanced Users] Enabled secure (HTTPS Only) cookies in settings. This should only be done if you only access your server over a HTTPS connection and will restrict logins to HTTPS only. Note if you enable this you will not be able to sign in over an insecure (HTTP) connection to turn if off. - [Advanced Users] Enable 2 factor authentication (TOTP) login, under Users select your user account and activate 2 factor authentication. Note there is no way to reset this if you loose your TOTP secret key. - [Advanced Users] If you will not be receiving Nexmo SMS messages the webhook can be disabled in Fax Server settings. + +### 📱 iOS App Setup + +![Fax Server](https://media.bludesign.biz/fax_client.png) + +- Open Clients in Fax Server and create a client with the above values. +- After creating the client copy the Client ID and Client Secret and fill them into the iOS app along with your servers URL. + +

+ + App Store + +

diff --git a/Resources/Views/admin.leaf b/Resources/Views/admin.leaf index 5f90bda..e416a84 100644 --- a/Resources/Views/admin.leaf +++ b/Resources/Views/admin.leaf @@ -250,6 +250,7 @@ diff --git a/Resources/Views/user.leaf b/Resources/Views/user.leaf index eaccf8c..c573b8c 100644 --- a/Resources/Views/user.leaf +++ b/Resources/Views/user.leaf @@ -113,6 +113,45 @@ +
+
+ Push Devices +
+
+ + + + + + + + + + #raw(tableData) + +
Device NameUpdated AtToken
+ +
+ +
diff --git a/Sources/Server/Middleware/AuthenicationMiddleware.swift b/Sources/Server/Middleware/AuthenicationMiddleware.swift index ce19ea3..f0e5223 100644 --- a/Sources/Server/Middleware/AuthenicationMiddleware.swift +++ b/Sources/Server/Middleware/AuthenicationMiddleware.swift @@ -59,9 +59,10 @@ extension Request { let tokenHash = try Application.makeHash(token) guard var accessToken = try AccessToken.collection.findOne("token" == tokenHash) else { return nil } guard let tokenExpiration = accessToken["tokenExpires"] as? Date else { return nil } + guard let source = accessToken["source"] as? String else { return nil } guard Date() < tokenExpiration else { return nil } guard let userId = accessToken["userId"] as? ObjectId else { return nil } - if tokenExpiration.timeIntervalSinceReferenceDate - 432000 < Date().timeIntervalSinceReferenceDate, let objectId = accessToken.objectId { + if tokenExpiration.timeIntervalSinceReferenceDate - 432000 < Date().timeIntervalSinceReferenceDate, let objectId = accessToken.objectId, source == "cookie" { let expirationDate = Date(timeIntervalSinceNow: AccessToken.cookieExpiresIn) accessToken["tokenExpires"] = expirationDate accessToken["endOfLife"] = expirationDate diff --git a/Sources/Server/Models/AccessToken.swift b/Sources/Server/Models/AccessToken.swift index ed3b1bb..88a5986 100644 --- a/Sources/Server/Models/AccessToken.swift +++ b/Sources/Server/Models/AccessToken.swift @@ -67,6 +67,10 @@ struct AccessToken { throw ServerAbort(.unauthorized, reason: "Refresh token not found") } let scope = try accessToken.extract("scope") as String + let source = try accessToken.extract("source") as String + guard source == "oauth" else { + throw ServerAbort(.unauthorized, reason: "Access token is not valid") + } let token = try String.tokenEncoded() let tokenHash = try Application.makeHash(token) accessToken["tokenExpires"] = Date(timeIntervalSinceNow: AccessToken.expiresIn) diff --git a/Sources/Server/Providers/PushProvider.swift b/Sources/Server/Providers/PushProvider.swift index bb4f355..9b7ccaf 100644 --- a/Sources/Server/Providers/PushProvider.swift +++ b/Sources/Server/Providers/PushProvider.swift @@ -40,7 +40,6 @@ final class PushProvider: Vapor.Provider { static func sendPush(threadId: String, title: String, body: String, userId: ObjectId? = nil) { let payload = Payload(title: title, body: body) - payload.threadId = threadId do { let devices: CollectionSlice if let userId = userId { diff --git a/Sources/Server/Routes/Admin+Routes.swift b/Sources/Server/Routes/Admin+Routes.swift index a208700..95e1330 100644 --- a/Sources/Server/Routes/Admin+Routes.swift +++ b/Sources/Server/Routes/Admin+Routes.swift @@ -87,6 +87,10 @@ extension Admin { // MARK: Update Settings protected.post { request in + if request.data["action"]?.string == "testPush" { + PushProvider.sendPush(threadId: "test", title: "Test Notification", body: "Test Push Notification") + return Response(redirect: "/admin") + } if let domain = try? request.data.extract("domain") as String { guard let url = URL(string: domain), let domain = url.domain else { throw ServerAbort(.badRequest, reason: "Domain format is invalid") diff --git a/Sources/Server/Routes/Device+Routes.swift b/Sources/Server/Routes/Device+Routes.swift index a7fd1b7..01a1214 100644 --- a/Sources/Server/Routes/Device+Routes.swift +++ b/Sources/Server/Routes/Device+Routes.swift @@ -20,8 +20,10 @@ extension Device { protected.post { request in let userId = try request.getUserId() let deviceToken = try request.data.extract("deviceToken") as String + let deviceName = try request.data.extract("deviceName") as String let document: Document = [ "deviceToken": deviceToken, + "deviceName": deviceName, "updatedAt": Date(), "userId": userId ] @@ -29,5 +31,14 @@ extension Device { return Response(jsonStatus: .ok) } + + protected.post("testPush") { request in + let userId = try request.getUserId() + PushProvider.sendPush(threadId: "test", title: "Test Notification", body: "Test Push Notification", userId: userId) + if request.jsonResponse { + return Response(jsonStatus: .ok) + } + return Response(redirect: "/user/\(userId.hexString)") + } } } diff --git a/Sources/Server/Routes/Fax+Routes.swift b/Sources/Server/Routes/Fax+Routes.swift index 8d17583..b755824 100644 --- a/Sources/Server/Routes/Fax+Routes.swift +++ b/Sources/Server/Routes/Fax+Routes.swift @@ -179,7 +179,6 @@ extension Fax { } let token = try String.token() - let fileToken = try String.token() let fromString = request.data["from"]?.string ?? phoneNumber var document: Document = [ @@ -191,7 +190,8 @@ extension Fax { "senderEmail": senderEmail, "status": "started", "dateCreated": Date(), - "accountId": accountId + "accountId": accountId, + "direction": "outbound" ] guard let objectId = try Fax.collection.insert(document) as? ObjectId else { throw ServerAbort(.notFound, reason: "Error creating fax") @@ -199,7 +199,7 @@ extension Fax { let fileDocument: Document = [ "faxObjectId": objectId, - "token": fileToken, + "token": token, "dateCreated": Date(), "data": Data(bytes: bytes) ] @@ -216,7 +216,7 @@ extension Fax { request.body = .data(try Node(node: [ "From": fromString, "To": toString, - "MediaUrl": "\(url)/fax/file/\(objectId.hexString)/\(fileToken)", + "MediaUrl": "\(url)/fax/file/\(objectId.hexString)/\(token)", "StatusCallback": "\(url)/fax/status/\(objectId.hexString)/\(token)" ]).formURLEncodedPlus()) @@ -444,6 +444,7 @@ extension Fax { throw ServerAbort(.notFound, reason: "Account not found") } let authToken = try account.extract("authToken") as String + let token = try String.token() var document: Document = [ "sid": sid, @@ -457,7 +458,8 @@ extension Fax { "pages": request.data["NumPages"]?.int, "mediaUrl": request.data["MediaUrl"]?.string, "errorCode": request.data["ErrorCode"]?.string, - "errorMessage": request.data["ErrorMessage"]?.string + "errorMessage": request.data["ErrorMessage"]?.string, + "token": token ] guard let responseBytes = try drop.client.get("\(Constants.Twilio.faxUrl)/Faxes/\(sid)", [ @@ -491,11 +493,10 @@ extension Fax { guard let bytes = response.body.bytes else { throw ServerAbort(.notFound, reason: "No response body") } - let fileToken = try String.token() let fileDocument: Document = [ "faxObjectId": objectId, - "token": fileToken, + "token": token, "dateCreated": Date(), "data": Data(bytes: bytes) ] diff --git a/Sources/Server/Routes/Message+Routes.swift b/Sources/Server/Routes/Message+Routes.swift index 22843a1..a6e0651 100644 --- a/Sources/Server/Routes/Message+Routes.swift +++ b/Sources/Server/Routes/Message+Routes.swift @@ -142,7 +142,6 @@ extension Message { let mediaUrl: String? if let bytes = request.data["file"]?.bytes, bytes.count > 0 { - let fileToken = try String.token() let mimeType: String switch bytes[0] { case 0xFF: mimeType = "image/jpeg" @@ -153,7 +152,7 @@ extension Message { } let fileDocument: Document = [ "messageObjectId": objectId, - "token": fileToken, + "token": token, "dateCreated": Date(), "mimeType": mimeType, "data": Data(bytes: bytes) @@ -161,7 +160,7 @@ extension Message { guard let fileObjectId = try MessageFile.collection.insert(fileDocument) as? ObjectId else { throw ServerAbort(.notFound, reason: "Error creating message file") } - mediaUrl = "\(url)/message/file/\(fileObjectId.hexString)/\(fileToken)" + mediaUrl = "\(url)/message/file/\(fileObjectId.hexString)/\(token)" } else { mediaUrl = nil } diff --git a/Sources/Server/Routes/User+Routes.swift b/Sources/Server/Routes/User+Routes.swift index 8d9b650..d954183 100644 --- a/Sources/Server/Routes/User+Routes.swift +++ b/Sources/Server/Routes/User+Routes.swift @@ -98,9 +98,41 @@ extension User { if request.jsonResponse { return try document.makeResponse() } else { + let pageInfo = request.pageInfo + let devices = try Device.collection.find("userId" == objectId, sortedBy: ["updatedAt": .ascending], skipping: pageInfo.skip, limitedTo: pageInfo.limit, withBatchSize: pageInfo.limit) + + let link = "/users/\(objectId.hexString)?" + var pages = try (devices.count() / pageInfo.limit) + 1 + let startPage: Int + if pages > 7 { + let firstPage = pageInfo.page - 3 + let lastPage = pageInfo.page + 2 + startPage = max(pageInfo.page - 3 - (lastPage > pages ? lastPage - pages : 0), 0) + pages = min(pages, lastPage - (firstPage < 0 ? firstPage : 0)) + } else { + startPage = 0 + } + var pageData: String = "" + for x in startPage..\(x + 1)") + } + var tableData: String = "" + for device in devices { + let deviceName = try device.extractString("deviceName") + let lastActionDate = try device.extractDate("updatedAt") + let deviceToken = try device.extractString("deviceToken") + let string = "\(deviceName)\(lastActionDate.longString)\(deviceToken)" + tableData.append(string) + } + let email = try document.extract("email") as String let totpActivated = try? document.extract("totpActivated") as Bool return try drop.view.make("user", [ + "tableData": tableData, + "pageData": pageData, + "page": pageInfo.page, + "nextPage": (pageInfo.page + 1 > pages.count ? "#" : "/\(link)page=\(pageInfo.page + 1)"), + "prevPage": (pageInfo.page - 1 <= 0 ? "#" : "\(link)page=\(pageInfo.page - 1)"), "userId": objectId.hexString, "email": email, "totpActivated": totpActivated ?? false