diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..9e3c1ec --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,27 @@ +version: 2 + +jobs: + macos: + macos: + xcode: "9.0" + steps: + - run: brew install vapor/tap/vapor + - checkout + - run: swift build + - run: swift test + + linux: + docker: + - image: swift:4.0 + steps: + - run: apt-get install -yq libssl-dev + - checkout + - run: swift build + - run: swift test + +workflows: + version: 2 + tests: + jobs: + - macos + - linux diff --git a/Package.resolved b/Package.resolved index a31ef76..262b33d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,174 +2,84 @@ "object": { "pins": [ { - "package": "BCrypt", - "repositoryURL": "https://github.com/vapor/bcrypt.git", + "package": "Async", + "repositoryURL": "https://github.com/vapor/async.git", "state": { - "branch": null, - "revision": "3ee4aca16ba6ebfb1ad48cc5fd4dfb163c6d6be8", - "version": "1.1.0" - } - }, - { - "package": "Bits", - "repositoryURL": "https://github.com/vapor/bits.git", - "state": { - "branch": null, - "revision": "c32f5e6ae2007dccd21a92b7e33eba842dd80d2f", - "version": "1.1.0" - } - }, - { - "package": "CTLS", - "repositoryURL": "https://github.com/vapor/ctls.git", - "state": { - "branch": null, - "revision": "fddec6a4643d6e85b6bb6dc54b1b5cdbabd395d2", - "version": "1.1.2" + "branch": "beta", + "revision": "fa7aa97c0476fe6da859e5fea35b3f027fcfc4ff", + "version": null } }, { "package": "Console", "repositoryURL": "https://github.com/vapor/console.git", "state": { - "branch": null, - "revision": "df9eb9a6afd03851abcb3d8204d04c368729776e", - "version": "2.3.0" + "branch": "beta", + "revision": "2e7266b7c8c08839ea548223579f7ece74905331", + "version": null } }, { "package": "Core", "repositoryURL": "https://github.com/vapor/core.git", "state": { - "branch": null, - "revision": "f9f3a585ab0ea5764b46d7a36d9c0d9d508b9c63", - "version": "2.2.0" + "branch": "beta", + "revision": "a848fb34323fa194ba9255df6899dcf9291b00ff", + "version": null } }, { "package": "Crypto", "repositoryURL": "https://github.com/vapor/crypto.git", "state": { - "branch": null, - "revision": "bf4470b9da79024aab79c85de80374f6c29e3864", - "version": "2.1.1" + "branch": "beta", + "revision": "b31cdb4ecc32b8f529a6027c1945f404c4a41c1e", + "version": null } }, { - "package": "Debugging", - "repositoryURL": "https://github.com/vapor/debugging.git", + "package": "DatabaseKit", + "repositoryURL": "https://github.com/vapor/database-kit.git", "state": { - "branch": null, - "revision": "49c5e8f0a7cb5456a8f7c72c6cd9f1553e5885a8", - "version": "1.1.0" + "branch": "beta", + "revision": "19af4c3911167a6e0ccf465003b52dea29717429", + "version": null } }, { "package": "Engine", "repositoryURL": "https://github.com/vapor/engine.git", "state": { - "branch": null, - "revision": "decf702d774ac630dfe0441ff76b4bb68257b77a", - "version": "2.2.1" - } - }, - { - "package": "Fluent", - "repositoryURL": "https://github.com/vapor/fluent.git", - "state": { - "branch": null, - "revision": "2c66e5c6c99dac19554e1cb8957e6de256009efc", - "version": "2.4.2" - } - }, - { - "package": "FluentProvider", - "repositoryURL": "https://github.com/vapor/fluent-provider.git", - "state": { - "branch": null, - "revision": "425d973f7cad7bfa987409b9e4a8659c14c6f0e0", - "version": "1.3.0" - } - }, - { - "package": "JSON", - "repositoryURL": "https://github.com/vapor/json.git", - "state": { - "branch": null, - "revision": "735800d8f2e75ebe3be25559eb6a781f4666dcfc", - "version": "2.2.1" - } - }, - { - "package": "Multipart", - "repositoryURL": "https://github.com/vapor/multipart.git", - "state": { - "branch": null, - "revision": "8e541b2e6fc64a3741eca2aa48ee2c3f23cbe17c", - "version": "2.1.1" - } - }, - { - "package": "Node", - "repositoryURL": "https://github.com/vapor/node.git", - "state": { - "branch": null, - "revision": "642f357d08ec5aa335ae2e3c4633c72da7b5a0c4", - "version": "2.1.1" - } - }, - { - "package": "Random", - "repositoryURL": "https://github.com/vapor/random.git", - "state": { - "branch": null, - "revision": "d7c4397d125caba795d14d956efacfe2a27a63d0", - "version": "1.2.0" - } - }, - { - "package": "Routing", - "repositoryURL": "https://github.com/vapor/routing.git", - "state": { - "branch": null, - "revision": "cb9d78aca2540c1a6b45b0ab43e5b0c50f29d216", - "version": "2.2.0" - } - }, - { - "package": "SQLite", - "repositoryURL": "https://github.com/vapor/sqlite.git", - "state": { - "branch": null, - "revision": "9aceb6a0d7b1698a557647493bd78b030dad468b", - "version": "2.3.1" + "branch": "beta", + "revision": "dce3bbcd3bf9cca676bcbf41a849bc392ea50076", + "version": null } }, { - "package": "Sockets", - "repositoryURL": "https://github.com/vapor/sockets.git", + "package": "Service", + "repositoryURL": "https://github.com/vapor/service.git", "state": { - "branch": null, - "revision": "70d14c0e223257176f5ef69a595f7cad5de7a88b", - "version": "2.2.1" + "branch": "beta", + "revision": "ae18f072da312710506802e557dc552d3c68bd34", + "version": null } }, { - "package": "TLS", - "repositoryURL": "https://github.com/vapor/tls.git", + "package": "TemplateKit", + "repositoryURL": "https://github.com/vapor/template-kit.git", "state": { - "branch": null, - "revision": "6c6eedb6761cddc6b6c87142a27eec13fa1701ec", - "version": "2.1.1" + "branch": "beta", + "revision": "56990033c17006e7d1ae9c78abc3da1654d79fe6", + "version": null } }, { "package": "Vapor", "repositoryURL": "https://github.com/vapor/vapor.git", "state": { - "branch": null, - "revision": "63768e7f56e58dbfa4288e16ad2e4003bfd8dcde", - "version": "2.4.0" + "branch": "beta", + "revision": "24e66402cd89ab039ff95f48dec87406e7865395", + "version": null } } ] diff --git a/Package.swift b/Package.swift index 3f9e587..e57a4e2 100644 --- a/Package.swift +++ b/Package.swift @@ -8,11 +8,10 @@ let package = Package( .library(name: "Imperial", targets: ["Imperial"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", .exact("2.4.0")), - .package(url: "https://github.com/vapor/fluent-provider.git", .exact("1.3.0")) + .package(url: "https://github.com/vapor/vapor.git", .branch("beta")) ], targets: [ - .target(name: "Imperial", dependencies: ["Vapor", "FluentProvider"]), + .target(name: "Imperial", dependencies: ["Vapor"]), .testTarget(name: "ImperialTests", dependencies: ["Imperial"]), ] ) diff --git a/Sources/Imperial/Errors/ImperialError.swift b/Sources/Imperial/Errors/ImperialError.swift index eea11a5..c7fa4de 100644 --- a/Sources/Imperial/Errors/ImperialError.swift +++ b/Sources/Imperial/Errors/ImperialError.swift @@ -5,15 +5,11 @@ public enum ImperialError: Error, CustomStringConvertible { /// - warning: This error is never thrown; rather, the application will fatal error. case missingEnvVar(String) - /// Thrown when no service is registered with a given name. - case noServiceFound(String) - /// Thrown when we attempt to create a `FederatedCreatable` model and there is /// no JSON in the response from the the request to `dataUri`. case missingJSONFromResponse(String) - /// Thrown when a `FederatedCreatable` type has a `serviceKey` that does not match any available endpoints in the service. - case noServiceEndpoint(String) + /// Thrown when `request.fetch` is called with a type that has not been run through `request.create`. case typeNotInitialized(String) @@ -22,9 +18,7 @@ public enum ImperialError: Error, CustomStringConvertible { public var description: String { switch self { case let .missingEnvVar(variable): return "Missing enviroment variable '\(variable)'" - case let .noServiceFound(name): return "No service was found with the name '\(name)'" case let .missingJSONFromResponse(uri): return "Reponse returned from '\(uri)' does not contain JSON" - case let .noServiceEndpoint(endpoint): return "Service does not have available endpoint for key '\(endpoint)'" case let .typeNotInitialized(type): return "No instence of type '\(type)' has been created" } } diff --git a/Sources/Imperial/Errors/ServiceError.swift b/Sources/Imperial/Errors/ServiceError.swift new file mode 100644 index 0000000..dfb6feb --- /dev/null +++ b/Sources/Imperial/Errors/ServiceError.swift @@ -0,0 +1,20 @@ +/// Represents an error that occurs during a service action. +public enum ServiceError: Error, CustomStringConvertible { + + /// Thrown when no service is registered with a given name. + case noServiceFound(String) + + /// Thrown when no `FederatedSewrvice` type is found whgen creating a `Service` from JSON. + case noExistingService(String) + + /// Thrown when a `FederatedCreatable` type has a `serviceKey` that does not match any available endpoints in the service. + case noServiceEndpoint(String) + + public var description: String { + switch self { + case let .noServiceFound(name): return "No service was found with the name '\(name)'" + case let .noExistingService(name): return "No service exists with the name '\(name)'" + case let .noServiceEndpoint(endpoint): return "Service does not have available endpoint for key '\(endpoint)'" + } + } +} diff --git a/Sources/Imperial/Helpers/Request+Imperial.swift b/Sources/Imperial/Helpers/Request+Imperial.swift index 5daa022..0e369a0 100644 --- a/Sources/Imperial/Helpers/Request+Imperial.swift +++ b/Sources/Imperial/Helpers/Request+Imperial.swift @@ -2,6 +2,17 @@ import Vapor extension Request { + func send(method: HTTPMethod = .get, url: String, headers: HTTPHeaders.Literal = [:], body: HTTPBody = HTTPBody(), mediaType: MediaType? = nil)throws -> Future { + let client = try self.make(HTTPClient.self) + var header: HTTPHeaders = HTTPHeaders() + header.append(headers) + var request = HTTPRequest(method: method, uri: URI(url), headers: header, body: body) + request.mediaType = mediaType ?? .urlEncodedForm + return client.send(request).map(to: Response.self, { (res) in + return Response(http: res, using: self.superContainer) + }) + } + /// Creates an instance of a `FederatedCreatable` type from JSON fetched from an OAuth provider's API. /// /// - Parameters: @@ -9,17 +20,18 @@ extension Request { /// - service: The service to get the data from. /// - Returns: An instance of the type passed in. /// - Throws: Errors from trying to get the access token from the request. - func create(_ model: T.Type, with service: Service)throws -> T { - let uri = try service[model.serviceKey] ?? ImperialError.noServiceEndpoint(model.serviceKey) + func create(_ model: T.Type, with service: OAuthService)throws -> Future { + let uri = try service[model.serviceKey] ?? ServiceError.noServiceEndpoint(model.serviceKey) let token = try service.tokenPrefix + self.getAccessToken() - let noJson = ImperialError.missingJSONFromResponse(uri) - - let response = try drop.client.get(uri, [.authorization: token]) - let new = try model.create(with: response.json ?? noJson, for: service) - self.storage["imperial-\(model)"] = new - return new + return try self.send(url: uri, headers: [.authorization: token]).flatMap(to: T.self, { (response) -> Future in + return try model.create(from: response) + }).map(to: T.self, { (instance) -> T in + let session = try self.session() + session.data.storage["imperial-\(model)"] = instance + return instance + }) } /// Gets an instance of a `FederatedCreatable` type that is stored in the request. @@ -30,9 +42,11 @@ extension Request { /// - Throws: /// - `ImperialError.typeNotInitialized`: If there is no value stored in the request for the type passed in. func fetch(_ model: T.Type)throws -> T { - if let new = self.storage["imperial-\(model)"] { - return new as! T + let session = try self.session() + guard let fetched = session.data.storage["imperial-\(model)"], + let typed = fetched as? T else { + throw ImperialError.typeNotInitialized("\(model)") } - throw ImperialError.typeNotInitialized("\(model)") + return typed } } diff --git a/Sources/Imperial/Helpers/Sessions+Imperial.swift b/Sources/Imperial/Helpers/Sessions+Imperial.swift index 5078fdc..dc54f7a 100644 --- a/Sources/Imperial/Helpers/Sessions+Imperial.swift +++ b/Sources/Imperial/Helpers/Sessions+Imperial.swift @@ -1,5 +1,4 @@ import Vapor -import Sessions extension Request { @@ -10,7 +9,7 @@ extension Request { /// - `Abort.unauthorized` if no access token exists. /// - `SessionsError.notConfigured` if session middlware is not configured yet. public func getAccessToken()throws -> String { - return try self.assertSession().getAccessToken() + return try self.session().getAccessToken() } } @@ -21,7 +20,7 @@ extension Session { /// - Returns: The access token stored with the `access_token` key. /// - Throws: `Abort.unauthorized` if no access token exists.m public func getAccessToken()throws -> String { - guard let token = self.data["access_token"]?.string else { + guard let token = self["access_token"] else { throw Abort(.unauthorized, reason: "User currently not authenticated") } return token diff --git a/Sources/Imperial/Middleware/ImperialMiddleware.swift b/Sources/Imperial/Middleware/ImperialMiddleware.swift index a5dff3b..a00519d 100644 --- a/Sources/Imperial/Middleware/ImperialMiddleware.swift +++ b/Sources/Imperial/Middleware/ImperialMiddleware.swift @@ -15,7 +15,7 @@ public class ImperialMiddleware: Middleware { /// Checks that the request contains an access token. If it does, let the request through. If not, redirect the user to the `redirectPath`. /// If the `redirectPath` is `nil`, then throw the error from getting the access token (Abort.unauthorized). - public func respond(to request: Request, chainingTo next: Responder) throws -> Response { + public func respond(to request: Request, chainingTo next: Responder) throws -> Future { do { _ = try request.getAccessToken() return try next.respond(to: request) @@ -23,7 +23,7 @@ public class ImperialMiddleware: Middleware { guard let redirect = redirectPath else { throw error } - return Response(redirect: redirect) + return Future(request.redirect(to: redirect)) } catch let error { throw error } diff --git a/Sources/Imperial/Model/FederatedCreatable.swift b/Sources/Imperial/Model/FederatedCreatable.swift index 1279c57..33e31e3 100644 --- a/Sources/Imperial/Model/FederatedCreatable.swift +++ b/Sources/Imperial/Model/FederatedCreatable.swift @@ -1,16 +1,22 @@ -import JSON +import Vapor /// Defines a type that can be created with federated login data. /// This type is used as a parameter in the `request.fetch` method -public protocol FederatedCreatable { +public protocol FederatedCreatable: Codable { /// The key for the service's endpoint to use when `request.create` is called with the implimenting type. static var serviceKey: String { get } /// Creates an instance of the model with JSON. /// - /// - Parameter json: The JSON in the response from the `dataUri`. + /// - Parameter response: The JSON in the response from the `dataUri`. /// - Returns: An instence of the type that conforms to this protocol. /// - Throws: Any errors that could be thrown inside the method. - static func create(with json: JSON, `for` service: Service)throws -> Self + static func create(from response: Response)throws -> Future +} + +extension FederatedCreatable { + static func create(from response: Response)throws -> Future { + return try response.content.decode(Self.self) + } } diff --git a/Sources/Imperial/Provider.swift b/Sources/Imperial/Provider.swift index 51ae2ed..3dd9c78 100644 --- a/Sources/Imperial/Provider.swift +++ b/Sources/Imperial/Provider.swift @@ -1,39 +1,16 @@ import Vapor -internal fileprivate(set) var drop: Droplet! +internal fileprivate(set) var router: Router! public class Provider: Vapor.Provider { + public static var repositoryName: String = "Imperial" - public func boot(_ droplet: Droplet) throws { - drop = droplet - } + /// Register all services provided by the provider here. + public func register(_ services: inout Services) throws {} - public func boot(_ config: Config) throws { - guard let imperial = config["imperial"]?.object else { - return - } - - if let ghID = imperial["github_client_id"]?.string, - let ghSecret = imperial["github_client_secret"]?.string { - ImperialConfig.gitHubID = ghID - ImperialConfig.gitHubSecret = ghSecret - } - - if let googleID = imperial["google_client_id"]?.string, - let googleSecret = imperial["google_client_secret"]?.string { - ImperialConfig.googleID = googleID - ImperialConfig.googleSecret = googleSecret - } + /// Called after the container has initialized. + public func boot(_ worker: Container) throws { + router = try worker.make(Router.self, for: Container.self) } - - public func beforeRun(_ droplet: Droplet) throws {} - public required init(config: Config) throws {} -} - -internal struct ImperialConfig { - internal fileprivate(set) static var gitHubID: String? - internal fileprivate(set) static var gitHubSecret: String? - internal fileprivate(set) static var googleID: String? - internal fileprivate(set) static var googleSecret: String? } diff --git a/Sources/Imperial/Routing/FederatedServiceRouter.swift b/Sources/Imperial/Routing/FederatedServiceRouter.swift index 5986408..b19c2f0 100644 --- a/Sources/Imperial/Routing/FederatedServiceRouter.swift +++ b/Sources/Imperial/Routing/FederatedServiceRouter.swift @@ -1,5 +1,4 @@ import Vapor -import URI /// Defines a type that implements the routing to get an access token from an OAuth provider. /// See implementations in the `Services/(Google|GitHub)/$0Router.swift` files @@ -10,7 +9,7 @@ public protocol FederatedServiceRouter { /// The callback that is fired after the access token is fetched from the OAuth provider. /// The response that is returned from this callback is also returned from the callback route. - var callbackCompletion: (String) -> (ResponseRepresentable) { get } + var callbackCompletion: (String) -> (Future) { get } /// The scopes to get permission for when getting the access token. /// Usage of this property varies by provider. @@ -32,7 +31,7 @@ public protocol FederatedServiceRouter { /// - callback: The callback URL that the OAuth provider will redirect to after authenticating the user. /// - completion: The completion handler that will be fired at the end of the `callback` route. The access token is passed into it. /// - Throws: Any errors that could occur in the implementation. - init(callback: String, completion: @escaping (String) -> (ResponseRepresentable))throws + init(callback: String, completion: @escaping (String) -> (Future))throws /// Configures the `authenticate` and `callback` routes with the droplet. @@ -48,26 +47,26 @@ public protocol FederatedServiceRouter { /// - Parameter request: The request from the browser. /// - Returns: A response that, by default, redirects the user to `authURL`. /// - Throws: N/A - func authenticate(_ request: Request)throws -> ResponseRepresentable + func authenticate(_ request: Request)throws -> Future /// The route that the OAuth provider calls when the user has benn authenticated. /// /// - Parameter request: The request from the OAuth provider. /// - Returns: A response that should redirect the user back to the app. /// - Throws: An errors that occur in the implementation code. - func callback(_ request: Request)throws -> ResponseRepresentable + func callback(_ request: Request)throws -> Future } extension FederatedServiceRouter { - public func authenticate(_ request: Request)throws -> ResponseRepresentable { - return Response(redirect: authURL) + public func authenticate(_ request: Request)throws -> Future { + return Future(request.redirect(to: authURL)) } public func configureRoutes(withAuthURL authURL: String) throws { - var callbackPath = URIParser().parse(bytes: callbackURL.bytes).path + var callbackPath = URI(callbackURL).path callbackPath = callbackPath != "/" ? callbackPath : callbackURL - drop.get(callbackPath, handler: callback) - drop.get(authURL, handler: authenticate) + router.get(callbackPath.makePathComponent(), use: callback) + router.get(authURL.makePathComponent(), use: authenticate) } } diff --git a/Sources/Imperial/Services/FederatedService.swift b/Sources/Imperial/Services/FederatedService.swift index b77df4e..1c12a83 100644 --- a/Sources/Imperial/Services/FederatedService.swift +++ b/Sources/Imperial/Services/FederatedService.swift @@ -1,4 +1,5 @@ -import HTTP +import Vapor +import Async /** Represents a connection to an OAuth provider to get an access token for authenticating a user. @@ -41,5 +42,5 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(authenticate: String, callback: String, scope: [String], completion: @escaping (String) -> (ResponseRepresentable))throws + init(authenticate: String, callback: String, scope: [String], completion: @escaping (String) -> (Future))throws } diff --git a/Sources/Imperial/Services/GitHub/GitHub.swift b/Sources/Imperial/Services/GitHub/GitHub.swift index 5303e14..9ba9a43 100644 --- a/Sources/Imperial/Services/GitHub/GitHub.swift +++ b/Sources/Imperial/Services/GitHub/GitHub.swift @@ -1,17 +1,17 @@ -import HTTP +import Vapor public class GitHub: FederatedService { public var tokens: FederatedServiceTokens public var router: FederatedServiceRouter @discardableResult - public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (ResponseRepresentable)) throws { + public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (Future)) throws { self.router = try GitHubRouter(callback: callback, completion: completion) self.tokens = self.router.tokens self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate) - Service.register(.github) + OAuthService.register(.github) } } diff --git a/Sources/Imperial/Services/GitHub/GitHubAuth.swift b/Sources/Imperial/Services/GitHub/GitHubAuth.swift index 4bb6cb9..30915ae 100644 --- a/Sources/Imperial/Services/GitHub/GitHubAuth.swift +++ b/Sources/Imperial/Services/GitHub/GitHubAuth.swift @@ -10,22 +10,7 @@ public class GitHubAuth: FederatedServiceTokens { let idError = ImperialError.missingEnvVar(idEnvKey) let secretError = ImperialError.missingEnvVar(secretEnvKey) - do { - guard let id = ImperialConfig.gitHubID else { - throw idError - } - self.clientID = id - } catch { - self.clientID = try Env.get(idEnvKey).value(or: idError) - } - - do { - guard let secret = ImperialConfig.gitHubSecret else { - throw secretError - } - self.clientSecret = secret - } catch { - self.clientSecret = try Env.get(secretEnvKey).value(or: secretError) - } + self.clientID = try Environment.get(idEnvKey) ?? idError + self.clientSecret = try Environment.get(secretEnvKey) ?? secretError } } diff --git a/Sources/Imperial/Services/GitHub/GitHubRouter.swift b/Sources/Imperial/Services/GitHub/GitHubRouter.swift index 5b3531d..8a55a5f 100644 --- a/Sources/Imperial/Services/GitHub/GitHubRouter.swift +++ b/Sources/Imperial/Services/GitHub/GitHubRouter.swift @@ -1,9 +1,9 @@ import Vapor -import Sessions +import Foundation public class GitHubRouter: FederatedServiceRouter { public let tokens: FederatedServiceTokens - public let callbackCompletion: (String) -> (ResponseRepresentable) + public let callbackCompletion: (String) -> (Future) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://github.com/login/oauth/access_token" @@ -13,43 +13,36 @@ public class GitHubRouter: FederatedServiceRouter { "client_id=\(self.tokens.clientID)" } - public required init(callback: String, completion: @escaping (String) -> (ResponseRepresentable)) throws { + public required init(callback: String, completion: @escaping (String) -> (Future)) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion } - public func callback(_ request: Request)throws -> ResponseRepresentable { + public func callback(_ request: Request)throws -> Future { let code: String - if let queryCode: String = try request.query?.get("code") { + if let queryCode: String = try request.query.get(at: "code") { code = queryCode - } else if let error: String = try request.query?.get("error") { + } else if let error: String = try request.query.get(at: "error") { throw Abort(.badRequest, reason: error) } else { throw Abort(.badRequest, reason: "Missing 'code' key in URL query") } - let req = Request(method: .post, uri: accessTokenURL) - req.formURLEncoded = [ - "client_id": .string(self.tokens.clientID), - "client_secret": .string(self.tokens.clientSecret), - "code": .string(code) - ] + let bodyData = NSKeyedArchiver.archivedData(withRootObject: [ + "client_id": self.tokens.clientID, + "client_secret": self.tokens.clientSecret, + "code": code + ]) - let response = try drop.client.respond(to: req) - - guard let body = response.body.bytes else { - throw Abort(.internalServerError, reason: "Unable to get body from access token response") - } - - guard let accessToken: String = try Node(formURLEncoded: body, allowEmptyValues: false).get("access_token") else { - throw Abort(.internalServerError, reason: "Unable to get access token from response body") - } - - let session = try request.assertSession() - try session.data.set("access_token", accessToken) - try session.data.set("access_token_service", Service.github) - - return callbackCompletion(accessToken) + return try request.send(url: accessTokenURL, body: HTTPBody(bodyData)).flatMap(to: String.self, { (response) in + return response.content.get(String.self, at: ["access_token"]) + }).map(to: ResponseEncodable.self, { (accessToken) in + let session = try request.session() + session.data.storage["access_token"] = accessToken + session.data.storage["access_token_service"] = OAuthService.github + + return self.callbackCompletion(accessToken) + }) } } diff --git a/Sources/Imperial/Services/GitHub/Service+GitHub.swift b/Sources/Imperial/Services/GitHub/Service+GitHub.swift index 37d03e6..d681120 100644 --- a/Sources/Imperial/Services/GitHub/Service+GitHub.swift +++ b/Sources/Imperial/Services/GitHub/Service+GitHub.swift @@ -1,7 +1,6 @@ -extension Service { - public static let github = Service.init( +extension OAuthService { + public static let github = OAuthService.init( name: "github", - model: GitHub.self, endpoints: [ "user": "https://api.github.com/user" ] diff --git a/Sources/Imperial/Services/Google/Google.swift b/Sources/Imperial/Services/Google/Google.swift index b5a3d5f..687eb06 100644 --- a/Sources/Imperial/Services/Google/Google.swift +++ b/Sources/Imperial/Services/Google/Google.swift @@ -1,17 +1,17 @@ -import HTTP +import Vapor public class Google: FederatedService { public var tokens: FederatedServiceTokens public var router: FederatedServiceRouter @discardableResult - public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (ResponseRepresentable)) throws { + public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (Future)) throws { self.router = try GoogleRouter(callback: callback, completion: completion) self.tokens = self.router.tokens self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate) - Service.register(.google) + OAuthService.register(.google) } } diff --git a/Sources/Imperial/Services/Google/GoogleAuth.swift b/Sources/Imperial/Services/Google/GoogleAuth.swift index 56ca1fc..ddede76 100644 --- a/Sources/Imperial/Services/Google/GoogleAuth.swift +++ b/Sources/Imperial/Services/Google/GoogleAuth.swift @@ -10,22 +10,7 @@ public class GoogleAuth: FederatedServiceTokens { let idError = ImperialError.missingEnvVar(idEnvKey) let secretError = ImperialError.missingEnvVar(secretEnvKey) - do { - guard let id = ImperialConfig.googleID else { - throw idError - } - self.clientID = id - } catch { - self.clientID = try Env.get(idEnvKey).value(or: idError) - } - - do { - guard let secret = ImperialConfig.googleSecret else { - throw secretError - } - self.clientSecret = secret - } catch { - self.clientSecret = try Env.get(secretEnvKey).value(or: secretError) - } + self.clientID = try Environment.get(idEnvKey) ?? idError + self.clientSecret = try Environment.get(secretEnvKey) ?? secretError } } diff --git a/Sources/Imperial/Services/Google/GoogleRouter.swift b/Sources/Imperial/Services/Google/GoogleRouter.swift index 3c72f7d..388d013 100644 --- a/Sources/Imperial/Services/Google/GoogleRouter.swift +++ b/Sources/Imperial/Services/Google/GoogleRouter.swift @@ -1,10 +1,9 @@ import Vapor -import HTTP -import Sessions +import Foundation public class GoogleRouter: FederatedServiceRouter { public let tokens: FederatedServiceTokens - public let callbackCompletion: (String) -> (ResponseRepresentable) + public let callbackCompletion: (String) -> (Future) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -16,41 +15,38 @@ public class GoogleRouter: FederatedServiceRouter { "response_type=code" } - public required init(callback: String, completion: @escaping (String) -> (ResponseRepresentable)) throws { + public required init(callback: String, completion: @escaping (String) -> (Future)) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion } - public func callback(_ request: Request)throws -> ResponseRepresentable { + public func callback(_ request: Request)throws -> Future { let code: String - if let queryCode: String = try request.query?.get("code") { + if let queryCode: String = try request.query.get(at: "code") { code = queryCode - } else if let error: String = try request.query?.get("error") { + } else if let error: String = try request.query.get(at: "error") { throw Abort(.badRequest, reason: error) } else { throw Abort(.badRequest, reason: "Missing 'code' key in URL query") } - let req = Request(method: .post, uri: accessTokenURL) - req.formURLEncoded = [ - "code": .string(code), - "client_id": .string(self.tokens.clientID), - "client_secret": .string(self.tokens.clientSecret), - "grant_type": .string("authorization_code"), - "redirect_uri": .string(self.callbackURL) - ] - let response = try drop.client.respond(to: req) + let bodyData = NSKeyedArchiver.archivedData(withRootObject: [ + "code": code, + "client_id": self.tokens.clientID, + "client_secret": self.tokens.clientSecret, + "grant_type": "authorization_code", + "redirect_uri": self.callbackURL + ]) - guard let body = response.body.bytes, - let accessToken: String = try JSON(bytes: body).get("access_token") else { - throw Abort(.badRequest, reason: "Missing JSON from response body") - } - - let session = try request.assertSession() - try session.data.set("access_token", accessToken) - try session.data.set("access_token_service", Service.google) - - return callbackCompletion(accessToken) + return try request.send(url: accessTokenURL, body: HTTPBody(bodyData)).flatMap(to: String.self, { (response) in + return response.content.get(String.self, at: ["access_token"]) + }).map(to: ResponseEncodable.self, { (accessToken) in + let session = try request.session() + session.data.storage["access_token"] = accessToken + session.data.storage["access_token_service"] = OAuthService.google + + return self.callbackCompletion(accessToken) + }) } } diff --git a/Sources/Imperial/Services/Google/Service+Google.swift b/Sources/Imperial/Services/Google/Service+Google.swift index f468b4a..625ea9b 100644 --- a/Sources/Imperial/Services/Google/Service+Google.swift +++ b/Sources/Imperial/Services/Google/Service+Google.swift @@ -1,7 +1,6 @@ -extension Service { - public static let google = Service.init( +extension OAuthService { + public static let google = OAuthService.init( name: "google", - model: Google.self, endpoints: [:] ) } diff --git a/Sources/Imperial/Services/Service.swift b/Sources/Imperial/Services/Service/OAuthService.swift similarity index 73% rename from Sources/Imperial/Services/Service.swift rename to Sources/Imperial/Services/Service/OAuthService.swift index 5a970c8..d075dc0 100644 --- a/Sources/Imperial/Services/Service.swift +++ b/Sources/Imperial/Services/Service/OAuthService.swift @@ -1,9 +1,11 @@ +import Vapor + /// The services that are available for use in the application. -/// Services are added and fecthed with the `Service.register` and `.get` static methods. -fileprivate var services: [String: Service] = [:] +/// Services are added and fetched with the `Service.register` and `.get` static methods. +fileprivate var services: [String: OAuthService] = [:] /// Represents a service that interacts with an OAuth provider. -public struct Service { +public struct OAuthService: Codable, Content { /// The name of the service, i.e. "google", "github", etc. public let name: String @@ -14,9 +16,6 @@ public struct Service { /// The endpoints for the provider's API to use for initializing `FederatedCreatable` types public let endpoints: [String: String] - /// The service model that is used for interacting the the named OAuth provider. - public let model: FederatedService.Type - /// Creates an instance of a service. /// This is is usually done by creating an extension and a static property. /// @@ -25,10 +24,9 @@ public struct Service { /// - prefix: The prefix for the access token when it is used in a authoriazation header. /// - uri: The URI used to get data to initialize a `FederatedCreatable` type. /// - model: The model that works with the service. - public init(name: String, prefix: String? = nil, model: FederatedService.Type, endpoints: [String: String]) { + public init(name: String, prefix: String? = nil, endpoints: [String: String]) { self.name = name self.tokenPrefix = prefix ?? "Bearer " - self.model = model self.endpoints = endpoints } @@ -40,7 +38,7 @@ public struct Service { /// Registers a service as available for use. /// /// - Parameter service: The service to register. - internal static func register(_ service: Service) { + internal static func register(_ service: OAuthService) { services[service.name] = service } @@ -49,7 +47,7 @@ public struct Service { /// - Parameter name: The name of the service to fetch. /// - Returns: The service that matches the name passed in. /// - Throws: `ImperialError.noServiceFound` if no service is found with the name passed in. - public static func get(service name: String)throws -> Service { - return try services[name] ?? ImperialError.noServiceFound(name) + public static func get(service name: String)throws -> OAuthService { + return try services[name] ?? ServiceError.noServiceFound(name) } } diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index e2d6ae6..9ae6498 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -2,6 +2,9 @@ import XCTest @testable import Imperial class ImperialTests: XCTestCase { + func testExists() {} + static var allTests: [(String, (ImperialTests) -> () -> ())] = [ + ("testExists", testExists) ] }