From 5ead56105c218caae9ded8c5d5ba58aeca1fb093 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 11:38:48 +0200 Subject: [PATCH 01/16] Added User component and User Auth Modifier for authorization --- .../Corvus/Authentication/CorvusUser.swift | 15 --- .../Endpoints/Modifiers/AuthModifier.swift | 14 +-- .../Modifiers/UserAuthModifier.swift | 90 +++++++++++++++ Sources/Corvus/Endpoints/User.swift | 103 ++++++++++++++++++ 4 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift create mode 100644 Sources/Corvus/Endpoints/User.swift diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index aa269a9..d06bf9f 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -107,18 +107,3 @@ extension CorvusUser { ) } } - -/// An extension to validate if a given `CorvusUser` is equal to the current -/// `CorvusUser`, used for `AuthEndpoint.auth()`. -extension CorvusUser { - - /// Validates if a given user is equal to the current user. - /// - /// - Parameter requestUser: The user from the request that is to be - /// validated. - /// - Returns: True if the request's user matches the current user, false if - /// not. - public func validate(_ requestUser: CorvusUser) -> Bool { - requestUser.id == self.id - } -} diff --git a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift index 38d78cf..a8be6c6 100644 --- a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift @@ -4,7 +4,7 @@ import Fluent /// A class that wraps a component which utilizes an `.auth()` modifier. That /// allows Corvus to chain modifiers, as it gets treated as any other struct /// conforming to `AuthEndpoint`. -public final class AuthModifier: AuthEndpoint { +public final class AuthModifier: AuthEndpoint { /// The return type for the `.handler()` modifier. public typealias Element = Q.Element @@ -17,7 +17,7 @@ public final class AuthModifier: AuthEndpoint { /// authenticated. public typealias UserKeyPath = KeyPath< Q.QuerySubject, - Q.QuerySubject.Parent + Q.QuerySubject.Parent > /// The `ReadEndpoint` the `.auth()` modifier is attached to. @@ -74,11 +74,11 @@ public final class AuthModifier: AuthEndpoint { throw Abort(.notFound) } - guard let authorized = req.auth.get(CorvusUser.self) else { + guard let authorized = req.auth.get(T.self) else { throw Abort(.unauthorized) } - return authorized.validate(user) + return authorized.id == user.id } return authorized.flatMap { authorized in @@ -105,9 +105,9 @@ extension AuthEndpoint { /// - Parameter user: A `KeyPath` to the related user property. /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` /// to the user. - public func auth( - _ user: AuthModifier.UserKeyPath - ) -> AuthModifier { + public func auth( + _ user: AuthModifier.UserKeyPath + ) -> AuthModifier { AuthModifier(self, user: user) } } diff --git a/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift new file mode 100644 index 0000000..1d3e9f1 --- /dev/null +++ b/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift @@ -0,0 +1,90 @@ +import Vapor +import Fluent + +/// A class that wraps a component which utilizes an `.auth()` modifier. That +/// allows Corvus to chain modifiers, as it gets treated as any other struct +/// conforming to `AuthEndpoint`. +public final class UserAuthModifier: AuthEndpoint +where Q.QuerySubject: ModelUser { + + /// The return type for the `.handler()` modifier. + public typealias Element = Q.Element + + /// The return value of the `.handler()`, so the type being operated on in + /// the current component. + public typealias QuerySubject = Q.QuerySubject + + /// The `AuthEndpoint` the `.userAuth()` modifier is attached to. + public let queryEndpoint: Q + + /// The HTTP method of the wrapped method. + public let operationType: OperationType + + /// Initializes the modifier with its underlying `QueryEndpoint`. + /// + /// - Parameters: + /// - queryEndpoint: The `QueryEndpoint` which the modifer is attached + /// to. + /// - operationType: The HTTP method of the wrapped component. + public init(_ queryEndpoint: Q) { + self.queryEndpoint = queryEndpoint + self.operationType = queryEndpoint.operationType + } + + /// Returns the `queryEndpoint`'s query. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: A `QueryBuilder`, which represents a `Fluent` query defined + /// by the `queryEndpoint`. + public func query(_ req: Request) throws -> QueryBuilder { + try queryEndpoint.query(req) + } + + /// A method which checks if the `CorvusUser` supplied in the `Request` is + /// equal to the user belonging to the particular `QuerySubject`. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: An `EventLoopFuture` containing an eagerloaded value as + /// defined by `Element`. If authentication fails or a user is not found, + /// HTTP `.unauthorized` and `.notFound` are thrown respectively. + public func handler(_ req: Request) throws -> EventLoopFuture { + let users = try query(req).all() + + let authorized: EventLoopFuture<[Bool]> = users + .mapEachThrowing { user throws -> Bool in + + guard let authorized = req.auth.get(QuerySubject.self) else { + throw Abort(.unauthorized) + } + + return authorized.id == user.id + } + + return authorized.flatMap { authorized in + guard authorized.allSatisfy({ $0 }) else { + return req.eventLoop.makeFailedFuture(Abort(.unauthorized)) + } + + do { + return try self.queryEndpoint.handler(req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } + } +} + +/// An extension that adds the `.auth()` modifier to components conforming to +/// `AuthEndpoint`. +extension AuthEndpoint where Self.QuerySubject: ModelUser{ + + /// A modifier used to make sure components only authorize requests where + /// the supplied `CorvusUser` is actually related to the `QuerySubject`. + /// + /// - Parameter user: A `KeyPath` to the related user property. + /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` + /// to the user. + internal func userAuth() -> UserAuthModifier { + UserAuthModifier(self) + } +} diff --git a/Sources/Corvus/Endpoints/User.swift b/Sources/Corvus/Endpoints/User.swift new file mode 100644 index 0000000..5655a0b --- /dev/null +++ b/Sources/Corvus/Endpoints/User.swift @@ -0,0 +1,103 @@ +import Vapor +import Fluent + +/// A class that contains Create, Read, Update and Delete functionality for a +/// generic type `T` conforming to `CorvusModel` grouped under a given path. +public final class User: Endpoint { + + /// The route path to the parameters. + let pathComponents: [PathComponent] + + /// A property to generate route parameter placeholders. + let parameter = Parameter() + + /// Indicates wether soft delete should be included or not. + let useSoftDelete: Bool + + /// Initializes the component with one or more route path components. + /// + /// - Parameter pathComponents: One or more `PathComponents` identifying the + /// path to the operations defined by the `CRUD` component. + /// - Parameter softDelete: Enable/Disable soft deletion of Models. + public init(_ pathComponents: PathComponent..., softDelete: Bool = true) { + self.pathComponents = pathComponents + self.useSoftDelete = softDelete + } + + /// The `content` of the `CRUD`, containing Create, Read, Update and Delete + /// functionality grouped under one. + public var content: Endpoint { + if useSoftDelete { + return contentWithSoftDelete + } + + return Group(pathComponents) { + Custom(type: .post) { req in + let requestContent = try req.content.decode(T.self) + return requestContent + .save(on: req.db) + .flatMapThrowing { + guard let username = requestContent[ + keyPath: T.usernameKey + ] as? T else + { + throw Abort(.internalServerError) + } + return username + } + } + + BasicAuthGroup { + ReadAll().userAuth() + Group(parameter.id) { + ReadOne(parameter.id).userAuth() + Update(parameter.id).userAuth() + Delete(parameter.id).userAuth() + } + } + } + } + + /// The `content` of the `CRUD`, containing Create, Read, Update, Delete and + /// SoftDelete functionality grouped under one. + public var contentWithSoftDelete: Endpoint { + Group(pathComponents) { + Custom(type: .post) { req in + let requestContent = try req.content.decode(T.self) + return requestContent + .save(on: req.db) + .flatMapThrowing { + guard let username = requestContent[ + keyPath: T.usernameKey + ] as? T else + { + throw Abort(.internalServerError) + } + return username + } + } + + BasicAuthGroup { + ReadAll().userAuth() + + Group(parameter.id) { + ReadOne(parameter.id).userAuth() + Update(parameter.id).userAuth() + SoftDelete(parameter.id).userAuth() + } + + Group("trash") { + ReadAll(.trashed).userAuth() + Group(parameter.id) { + ReadOne(parameter.id, .trashed).userAuth() + Delete(parameter.id).userAuth() + + Group("restore") { + Restore(parameter.id).userAuth() + } + } + } + } + } + } +} From f4a6d9565cf4204a1dd0e4eee0bee19988f21c70 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 14:54:03 +0200 Subject: [PATCH 02/16] Changed Login component to support Custom Token and Users * Added wrapper protocol for ModelUser and ModelUserToken --- .../Authentication/CorvusModelUser.swift | 4 + .../Authentication/CorvusModelUserToken.swift | 73 +++++++++++++++++++ .../Corvus/Authentication/CorvusToken.swift | 8 +- .../Corvus/Authentication/CorvusUser.swift | 2 +- .../Endpoints/Groups/BearerAuthGroup.swift | 2 +- Sources/Corvus/Endpoints/Login.swift | 13 ++-- Tests/CorvusTests/AuthenticationTests.swift | 6 +- 7 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 Sources/Corvus/Authentication/CorvusModelUser.swift create mode 100644 Sources/Corvus/Authentication/CorvusModelUserToken.swift diff --git a/Sources/Corvus/Authentication/CorvusModelUser.swift b/Sources/Corvus/Authentication/CorvusModelUser.swift new file mode 100644 index 0000000..bfa685e --- /dev/null +++ b/Sources/Corvus/Authentication/CorvusModelUser.swift @@ -0,0 +1,4 @@ +import Fluent +import Vapor + +public protocol CorvusModelUser: ModelUser {} diff --git a/Sources/Corvus/Authentication/CorvusModelUserToken.swift b/Sources/Corvus/Authentication/CorvusModelUserToken.swift new file mode 100644 index 0000000..d74d648 --- /dev/null +++ b/Sources/Corvus/Authentication/CorvusModelUserToken.swift @@ -0,0 +1,73 @@ +import Fluent +import Vapor + +public protocol CorvusModelUserToken: CorvusModel { + + associatedtype User: CorvusModel & Authenticatable + var value: String { get set } + var user: User { get set } + var isValid: Bool { get } +} + +extension CorvusModelUserToken { + + internal init(id: Self.IDValue? = nil, value: String, userId: User.IDValue) { + self.init() + self.value = value + _$user.id = userId + } +} + +extension CorvusModelUserToken { + + public static func authenticator( + database: DatabaseID? = nil + ) -> CorvusModelUserTokenAuthenticator { + CorvusModelUserTokenAuthenticator(database: database) + } + + var _$value: Field { + guard let mirror = Mirror(reflecting: self).descendant("_value"), + let token = mirror as? Field else { + fatalError("value property must be declared using @Field") + } + + return token + } + + var _$user: Parent { + guard let mirror = Mirror(reflecting: self).descendant("_user"), + let user = mirror as? Parent else { + fatalError("user property must be declared using @Parent") + } + + return user + } +} + +public struct CorvusModelUserTokenAuthenticator: BearerAuthenticator +{ + public typealias User = T.User + public let database: DatabaseID? + + public func authenticate( + bearer: BearerAuthorization, + for request: Request + ) -> EventLoopFuture { + let db = request.db(self.database) + return T.query(on: db) + .filter(\T._$value == bearer.token) + .first() + .flatMap + { token -> EventLoopFuture in + guard let token = token else { + return request.eventLoop.makeSucceededFuture(nil) + } + guard token.isValid else { + return token.delete(on: db).map { nil } + } + return token._$user.get(on: db) + .map { $0 } + } + } +} diff --git a/Sources/Corvus/Authentication/CorvusToken.swift b/Sources/Corvus/Authentication/CorvusToken.swift index 589a004..55752ff 100644 --- a/Sources/Corvus/Authentication/CorvusToken.swift +++ b/Sources/Corvus/Authentication/CorvusToken.swift @@ -69,13 +69,7 @@ public struct CreateCorvusToken: Migration { /// An extension to conform to the `ModelUserToken` protocol, which provides /// functionality to authenticate a token. -extension CorvusToken: ModelUserToken { - - /// Makes the path to a token's value publicly accessible. - public static let valueKey = \CorvusToken.$value - - /// Makes the path to a token's user publicly accessible. - public static let userKey = \CorvusToken.$user +extension CorvusToken: CorvusModelUserToken { /// Prevents tokens from being deleted after authentication. public var isValid: Bool { diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index d06bf9f..9766007 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -76,7 +76,7 @@ public struct CreateCorvusUser: Migration { /// An extension to conform to the `ModelUser` protocol, which provides /// functionality to authenticate a user with username and password. -extension CorvusUser: ModelUser { +extension CorvusUser: CorvusModelUser { /// Provides a path to the user's username (or in Corvus, the email). public static let usernameKey = \CorvusUser.$email diff --git a/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift b/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift index 9976e9a..823c524 100644 --- a/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift +++ b/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift @@ -3,7 +3,7 @@ import Fluent /// A special type of `Group` that protects its `content` with bearer token /// authentication for a generic `ModelUserToken`. -public struct BearerAuthGroup: Endpoint { +public struct BearerAuthGroup: Endpoint { /// An array of `PathComponent` describing the path that the /// `BearerAuthGroup` extends. diff --git a/Sources/Corvus/Endpoints/Login.swift b/Sources/Corvus/Endpoints/Login.swift index fc7f5dd..62533e5 100644 --- a/Sources/Corvus/Endpoints/Login.swift +++ b/Sources/Corvus/Endpoints/Login.swift @@ -1,9 +1,11 @@ import Vapor +import Fluent /// A class that provides functionality to log in a user with username and /// password credentials sent in a HTTP POST `Request` and save a token for /// that user. -public final class Login: Endpoint { +public final class Login: Endpoint +where T.User: ModelUser { /// The route for the login functionality let path: PathComponent @@ -20,9 +22,10 @@ public final class Login: Endpoint { /// /// - Parameter req: An incoming HTTP `Request`. /// - Returns: An `EventLoopFuture` containing the created `CorvusToken`. - public func handler(_ req: Request) throws -> EventLoopFuture { - let user = try req.auth.require(CorvusUser.self) - let token = try user.generateToken() + public func handler(_ req: Request) throws -> EventLoopFuture { + let user = try req.auth.require(T.User.self) + let token = T.init(value: [UInt8].random(count: 16).base64, userId: try user.requireID()) + return token.save(on: req.db).map { token } } @@ -33,7 +36,7 @@ public final class Login: Endpoint { /// about the HTTP route leading to the current component. public func register(to routes: RoutesBuilder) { let guardedRoutesBuilder = routes.grouped( - CorvusUser.authenticator().middleware() + T.User.authenticator().middleware() ) guardedRoutesBuilder.post(path, use: handler) diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index 9940609..d168906 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -133,7 +133,7 @@ final class AuthenticationTests: XCTestCase { Group("api") { CRUD("users", softDelete: false) - Login("login") + Login("login") BearerAuthGroup("accounts") { Create() @@ -205,7 +205,7 @@ final class AuthenticationTests: XCTestCase { Group("api") { CRUD("users", softDelete: false) - Login("login") + Login("login") BearerAuthGroup("accounts") { Create() @@ -247,7 +247,7 @@ final class AuthenticationTests: XCTestCase { Group("api") { CRUD("users", softDelete: false) - Login("login") + Login("login") BearerAuthGroup("accounts") { Create() From 739a133c009f5641e3e009555a317788209ea4a3 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 15:57:29 +0200 Subject: [PATCH 03/16] Changed to custom CorvusUserModel protocol, added hashing of password for CorvusUser --- .../Authentication/CorvusModelUser.swift | 67 ++++++++++++++++++- .../Authentication/CorvusModelUserToken.swift | 2 +- .../Corvus/Authentication/CorvusUser.swift | 26 ++----- .../Endpoints/Groups/BasicAuthGroup.swift | 2 +- Sources/Corvus/Endpoints/Login.swift | 2 +- .../Endpoints/Modifiers/AuthModifier.swift | 4 +- .../Modifiers/UserAuthModifier.swift | 4 +- Sources/Corvus/Endpoints/User.swift | 28 ++------ Tests/CorvusTests/AuthenticationTests.swift | 29 ++++---- 9 files changed, 98 insertions(+), 66 deletions(-) diff --git a/Sources/Corvus/Authentication/CorvusModelUser.swift b/Sources/Corvus/Authentication/CorvusModelUser.swift index bfa685e..bb36b7c 100644 --- a/Sources/Corvus/Authentication/CorvusModelUser.swift +++ b/Sources/Corvus/Authentication/CorvusModelUser.swift @@ -1,4 +1,69 @@ import Fluent import Vapor -public protocol CorvusModelUser: ModelUser {} +public protocol CorvusModelUser: CorvusModel, Authenticatable { + + var name: String { get set } + var passwordHash: String { get set } + func verify(password: String) throws -> Bool +} + +extension CorvusModelUser { + + internal init(id: Self.IDValue? = nil, password: String, name: String) throws { + self.init() + self.name = name + self.passwordHash = try Bcrypt.hash(password) + } +} + +extension CorvusModelUser { + public static func authenticator( + database: DatabaseID? = nil + ) -> CorvusModelUserAuthenticator { + CorvusModelUserAuthenticator(database: database) + } + + var _$name: Field { + guard let mirror = Mirror(reflecting: self).descendant("_name"), + let username = mirror as? Field else { + fatalError("name property must be declared using @Field") + } + + return username + } + + var _$passwordHash: Field { + guard let mirror = Mirror(reflecting: self).descendant("_passwordHash"), + let passwordHash = mirror as? Field else { + fatalError("passwordHash property must be declared using @Field") + } + + return passwordHash + } +} + +public struct CorvusModelUserAuthenticator: BasicAuthenticator + where User: CorvusModelUser +{ + public let database: DatabaseID? + + public func authenticate( + basic: BasicAuthorization, + for request: Request + ) -> EventLoopFuture { + User.query(on: request.db(self.database)) + .filter(\._$name == basic.username) + .first() + .flatMapThrowing + { + guard let user = $0 else { + return nil + } + guard try user.verify(password: basic.password) else { + return nil + } + return user + } + } +} diff --git a/Sources/Corvus/Authentication/CorvusModelUserToken.swift b/Sources/Corvus/Authentication/CorvusModelUserToken.swift index d74d648..2bfff83 100644 --- a/Sources/Corvus/Authentication/CorvusModelUserToken.swift +++ b/Sources/Corvus/Authentication/CorvusModelUserToken.swift @@ -56,7 +56,7 @@ public struct CorvusModelUserTokenAuthenticator: Bearer ) -> EventLoopFuture { let db = request.db(self.database) return T.query(on: db) - .filter(\T._$value == bearer.token) + .filter(\._$value == bearer.token) .first() .flatMap { token -> EventLoopFuture in diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index 9766007..3749b46 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -15,14 +15,9 @@ public final class CorvusUser: CorvusModel { @Field(key: "name") public var name: String - /// The email of the user, which is used as the username during - /// authentication. - @Field(key: "email") - public var email: String - /// The password of the user, used during authentication. - @Field(key: "password") - public var password: String + @Field(key: "password_hash") + public var passwordHash: String /// Timestamp for soft deletion. @Timestamp(key: "deleted_at", on: .delete) @@ -41,13 +36,11 @@ public final class CorvusUser: CorvusModel { public init( id: UUID? = nil, name: String, - email: String, - password: String + passwordHash: String ) { self.id = id self.name = name - self.email = email - self.password = password + self.passwordHash = passwordHash } } @@ -62,8 +55,7 @@ public struct CreateCorvusUser: Migration { database.schema(CorvusUser.schema) .id() .field("name", .string, .required) - .field("email", .string, .required) - .field("password", .string, .required) + .field("password_hash", .string, .required) .field("deleted_at", .date) .create() } @@ -78,19 +70,13 @@ public struct CreateCorvusUser: Migration { /// functionality to authenticate a user with username and password. extension CorvusUser: CorvusModelUser { - /// Provides a path to the user's username (or in Corvus, the email). - public static let usernameKey = \CorvusUser.$email - - /// Provides a path to the user's password. - public static let passwordHashKey = \CorvusUser.$password - /// Verifies a given string by checking if it matches a user's password. /// /// - Parameter password: The password to verify. /// - Returns: True if the provided password matches the user's, false if /// not. public func verify(password: String) throws -> Bool { - password == self.password + try Bcrypt.verify(password, created: self.passwordHash) } } diff --git a/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift b/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift index f04f34b..a27a092 100644 --- a/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift +++ b/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift @@ -3,7 +3,7 @@ import Fluent /// A special type of `Group` that protects its `content` with basic /// authentication for a generic `ModelUser`. -public struct BasicAuthGroup: Endpoint { +public struct BasicAuthGroup: Endpoint { /// An array of `PathComponent` describing the path that the /// `BasicAuthGroup` extends. diff --git a/Sources/Corvus/Endpoints/Login.swift b/Sources/Corvus/Endpoints/Login.swift index 62533e5..43b2cd5 100644 --- a/Sources/Corvus/Endpoints/Login.swift +++ b/Sources/Corvus/Endpoints/Login.swift @@ -5,7 +5,7 @@ import Fluent /// password credentials sent in a HTTP POST `Request` and save a token for /// that user. public final class Login: Endpoint -where T.User: ModelUser { +where T.User: CorvusModelUser { /// The route for the login functionality let path: PathComponent diff --git a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift index a8be6c6..f119535 100644 --- a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift @@ -4,7 +4,7 @@ import Fluent /// A class that wraps a component which utilizes an `.auth()` modifier. That /// allows Corvus to chain modifiers, as it gets treated as any other struct /// conforming to `AuthEndpoint`. -public final class AuthModifier: AuthEndpoint { +public final class AuthModifier: AuthEndpoint { /// The return type for the `.handler()` modifier. public typealias Element = Q.Element @@ -105,7 +105,7 @@ extension AuthEndpoint { /// - Parameter user: A `KeyPath` to the related user property. /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` /// to the user. - public func auth( + public func auth( _ user: AuthModifier.UserKeyPath ) -> AuthModifier { AuthModifier(self, user: user) diff --git a/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift index 1d3e9f1..9149492 100644 --- a/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift @@ -5,7 +5,7 @@ import Fluent /// allows Corvus to chain modifiers, as it gets treated as any other struct /// conforming to `AuthEndpoint`. public final class UserAuthModifier: AuthEndpoint -where Q.QuerySubject: ModelUser { +where Q.QuerySubject: CorvusModelUser { /// The return type for the `.handler()` modifier. public typealias Element = Q.Element @@ -76,7 +76,7 @@ where Q.QuerySubject: ModelUser { /// An extension that adds the `.auth()` modifier to components conforming to /// `AuthEndpoint`. -extension AuthEndpoint where Self.QuerySubject: ModelUser{ +extension AuthEndpoint where Self.QuerySubject: CorvusModelUser { /// A modifier used to make sure components only authorize requests where /// the supplied `CorvusUser` is actually related to the `QuerySubject`. diff --git a/Sources/Corvus/Endpoints/User.swift b/Sources/Corvus/Endpoints/User.swift index 5655a0b..0f55c46 100644 --- a/Sources/Corvus/Endpoints/User.swift +++ b/Sources/Corvus/Endpoints/User.swift @@ -3,7 +3,7 @@ import Fluent /// A class that contains Create, Read, Update and Delete functionality for a /// generic type `T` conforming to `CorvusModel` grouped under a given path. -public final class User: Endpoint { +public final class User: Endpoint { /// The route path to the parameters. let pathComponents: [PathComponent] @@ -32,19 +32,12 @@ public final class User: Endpoint { } return Group(pathComponents) { - Custom(type: .post) { req in + Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) + let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) return requestContent .save(on: req.db) - .flatMapThrowing { - guard let username = requestContent[ - keyPath: T.usernameKey - ] as? T else - { - throw Abort(.internalServerError) - } - return username - } + .flatMapThrowing { user.name } } BasicAuthGroup { @@ -62,19 +55,12 @@ public final class User: Endpoint { /// SoftDelete functionality grouped under one. public var contentWithSoftDelete: Endpoint { Group(pathComponents) { - Custom(type: .post) { req in + Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) + let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) return requestContent .save(on: req.db) - .flatMapThrowing { - guard let username = requestContent[ - keyPath: T.usernameKey - ] as? T else - { - throw Abort(.internalServerError) - } - return username - } + .flatMapThrowing { user.name } } BasicAuthGroup { diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index d168906..9549f98 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -1,6 +1,7 @@ import Corvus import Fluent import FluentSQLiteDriver +import Vapor import XCTVapor import Foundation @@ -32,14 +33,13 @@ final class AuthenticationTests: XCTestCase { try app.autoMigrate().wait() try app.register(collection: basicAuthenticatorTest) - let basic = "berzan@corvus.com:pass" + let basic = "berzan:pass" .data(using: .utf8)! .base64EncodedString() let user = CorvusUser( name: "berzan", - email: "berzan@corvus.com", - password: "pass" + passwordHash: try Bcrypt.hash("pass") ) let account = Account(name: "Berzan") @@ -63,7 +63,6 @@ final class AuthenticationTests: XCTestCase { body: account.encode() ) { res in print(res.body.string) - XCTAssertEqual(res.status, .ok) XCTAssertEqualJSON( res.body.string, account @@ -100,12 +99,11 @@ final class AuthenticationTests: XCTestCase { let user = CorvusUser( name: "berzan", - email: "berzan@corvus.com", - password: "pass" + passwordHash: try Bcrypt.hash("pass") ) let account = Account(name: "berzan") - let basic = "berzan@corvus.com:wrong" + let basic = "berzan:wrong" .data(using: .utf8)! .base64EncodedString() @@ -158,12 +156,11 @@ final class AuthenticationTests: XCTestCase { let user = CorvusUser( name: "berzan", - email: "berzan@corvus.com", - password: "pass" + passwordHash: try Bcrypt.hash("pass") ) let account = Account(name: "berzan") - let basic = "berzan@corvus.com:pass" + let basic = "berzan:pass" .data(using: .utf8)! .base64EncodedString() @@ -276,30 +273,28 @@ final class AuthenticationTests: XCTestCase { let user1 = CorvusUser( name: "berzan", - email: "berzan@corvus.com", - password: "pass" + passwordHash: try Bcrypt.hash("pass") ) let user2 = CorvusUser( name: "paul", - email: "paul@corvus.com", - password: "pass" + passwordHash: try Bcrypt.hash("pass") ) var account: SecureAccount! - let basic1 = "berzan@corvus.com:pass" + let basic1 = "berzan:pass" .data(using: .utf8)! .base64EncodedString() - let basic2 = "paul@corvus.com:pass" + let basic2 = "paul:pass" .data(using: .utf8)! .base64EncodedString() var token1: CorvusToken! var token2: CorvusToken! var accountRes: SecureAccount! - + try app.testable() .test( .POST, From c7b50ba3f3bfa1f9620f4d33fb0f47eb52e22062 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 15:58:56 +0200 Subject: [PATCH 04/16] Added question comment for Paul --- Sources/Corvus/Endpoints/User.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Corvus/Endpoints/User.swift b/Sources/Corvus/Endpoints/User.swift index 0f55c46..8ccd771 100644 --- a/Sources/Corvus/Endpoints/User.swift +++ b/Sources/Corvus/Endpoints/User.swift @@ -34,7 +34,7 @@ public final class User: Endpoint { return Group(pathComponents) { Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) - let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) + let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) // This works because initializers of CorvusModelUser and CorvusUser are equal, what happens if not? return requestContent .save(on: req.db) .flatMapThrowing { user.name } From 127c2dc6fc91c8dcb2a974bbb46f42b76757e653 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 17:33:32 +0200 Subject: [PATCH 05/16] Fixed correct return type for User component --- Sources/Corvus/Endpoints/User.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Corvus/Endpoints/User.swift b/Sources/Corvus/Endpoints/User.swift index 8ccd771..40b60b4 100644 --- a/Sources/Corvus/Endpoints/User.swift +++ b/Sources/Corvus/Endpoints/User.swift @@ -35,7 +35,7 @@ public final class User: Endpoint { Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) // This works because initializers of CorvusModelUser and CorvusUser are equal, what happens if not? - return requestContent + return user .save(on: req.db) .flatMapThrowing { user.name } } @@ -58,7 +58,7 @@ public final class User: Endpoint { Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) - return requestContent + return user .save(on: req.db) .flatMapThrowing { user.name } } From a1d893e886c8e31920498c5214ad82db97baa481 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 21:06:56 +0200 Subject: [PATCH 06/16] Added master branch to release-drafter --- .github/release-drafter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a6ef3b9..e7535d2 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,3 +1,4 @@ +branches: [master] name-template: '$NEXT_PATCH_VERSION 🌈' tag-template: '$NEXT_PATCH_VERSION' categories: From 1dbc448a86f61ace303615aaa4f0ce184fcee01d Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Mon, 6 Apr 2020 21:52:24 +0200 Subject: [PATCH 07/16] Added thread sanitization and test discovery to workflow test command --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d71ab3..f124167 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,4 +22,4 @@ jobs: - name: Build run: swift build - name: Test - run: swift test + run: swift test --enable-test-discovery --sanitize=thread From dc5bd13123b6980e6cef574ebc15f15dba3f3215 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Tue, 7 Apr 2020 11:28:13 +0200 Subject: [PATCH 08/16] Added test for auth modifier with custom Token and User --- .../Corvus/Authentication/CorvusToken.swift | 6 +- .../Corvus/Authentication/CorvusUser.swift | 20 +-- Tests/CorvusTests/AuthenticationTests.swift | 133 ++++++++++++++++++ Tests/CorvusTests/Models/CustomAccount.swift | 63 +++++++++ Tests/CorvusTests/Models/CustomToken.swift | 60 ++++++++ .../Models/CustomTransaction.swift | 70 +++++++++ Tests/CorvusTests/Models/CustomUser.swift | 80 +++++++++++ 7 files changed, 412 insertions(+), 20 deletions(-) create mode 100644 Tests/CorvusTests/Models/CustomAccount.swift create mode 100644 Tests/CorvusTests/Models/CustomToken.swift create mode 100644 Tests/CorvusTests/Models/CustomTransaction.swift create mode 100644 Tests/CorvusTests/Models/CustomUser.swift diff --git a/Sources/Corvus/Authentication/CorvusToken.swift b/Sources/Corvus/Authentication/CorvusToken.swift index 55752ff..cc5bef9 100644 --- a/Sources/Corvus/Authentication/CorvusToken.swift +++ b/Sources/Corvus/Authentication/CorvusToken.swift @@ -2,10 +2,10 @@ import Vapor import Fluent /// A default implementation of a bearer token. -public final class CorvusToken: CorvusModel { +public final class CorvusToken: CorvusModelUserToken { /// The corresponding database schema. - public static let schema = "tokens" + public static let schema = "corvus_tokens" /// The unique identifier of the model in the database. @ID @@ -69,7 +69,7 @@ public struct CreateCorvusToken: Migration { /// An extension to conform to the `ModelUserToken` protocol, which provides /// functionality to authenticate a token. -extension CorvusToken: CorvusModelUserToken { +extension CorvusToken { /// Prevents tokens from being deleted after authentication. public var isValid: Bool { diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index 3749b46..5688d83 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -2,10 +2,10 @@ import Vapor import Fluent /// A default implementation of a user for basic authentication. -public final class CorvusUser: CorvusModel { +public final class CorvusUser: CorvusModelUser { /// The corresponding database schema. - public static let schema = "users" + public static let schema = "corvus_users" /// The unique identifier of the model in the database. @ID @@ -68,7 +68,7 @@ public struct CreateCorvusUser: Migration { /// An extension to conform to the `ModelUser` protocol, which provides /// functionality to authenticate a user with username and password. -extension CorvusUser: CorvusModelUser { +extension CorvusUser { /// Verifies a given string by checking if it matches a user's password. /// @@ -79,17 +79,3 @@ extension CorvusUser: CorvusModelUser { try Bcrypt.verify(password, created: self.passwordHash) } } - -/// An extension to generate a `CorvusToken` for a given user. -extension CorvusUser { - - /// A method that generates a unique token for a given user. - /// - /// - Returns: The generated token. - public func generateToken() throws -> CorvusToken { - try .init( - value: [UInt8].random(count: 16).base64, - userID: self.requireID() - ) - } -} diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index 9549f98..5e4659c 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -363,4 +363,137 @@ final class AuthenticationTests: XCTestCase { XCTAssertEqualJSON(res.body.string, account) } } + + func testAuthModifierCustom() throws { + final class AuthModifierTest: RestApi { + + let testParameter = Parameter() + + var content: Endpoint { + Group("api") { + CRUD("users", softDelete: false) + + Login("login") + + BearerAuthGroup("accounts") { + Create() + Group(testParameter.id) { + ReadOne(testParameter.id) + .auth(\.$user) + } + } + } + } + } + + let app = Application(.testing) + defer { app.shutdown() } + let authModifierTest = AuthModifierTest() + + app.databases.use(.sqlite(.memory), as: .test, isDefault: true) + app.middleware.use(CustomToken.authenticator().middleware()) + app.migrations.add(CreateCustomAccount()) + app.migrations.add(CreateCustomUser()) + app.migrations.add(CreateCustomToken()) + + try app.autoMigrate().wait() + + try app.register(collection: authModifierTest) + + let user1 = CustomUser( + name: "berzan", + surname: "yildiz", + email: "berzan@corvus.com", + passwordHash: try Bcrypt.hash("pass") + ) + + let user2 = CustomUser( + name: "paul", + surname: "schmiedmayer", + email: "paul@corvus.com", + passwordHash: try Bcrypt.hash("pass") + ) + + var account: CustomAccount! + + let basic1 = "berzan:pass" + .data(using: .utf8)! + .base64EncodedString() + + let basic2 = "paul:pass" + .data(using: .utf8)! + .base64EncodedString() + + var token1: CustomToken! + var token2: CustomToken! + var accountRes: CustomAccount! + + try app.testable() + .test( + .POST, + "/api/users", + headers: ["content-type": "application/json"], + body: user1.encode(), + afterResponse: { res in + let userRes = try res.content.decode(CustomUser.self) + account = CustomAccount( + name: "berzan", + userID: userRes.id! + ) + } + ) + .test( + .POST, + "/api/users", + headers: ["content-type": "application/json"], + body: user2.encode() + ) + .test( + .POST, + "/api/login", + headers: ["Authorization": "Basic \(basic1)"] + ) { res in + token1 = try res.content.decode(CustomToken.self) + XCTAssertTrue(true) + } + .test( + .POST, + "/api/login", + headers: ["Authorization": "Basic \(basic2)"] + ) { res in + token2 = try res.content.decode(CustomToken.self) + XCTAssertTrue(true) + } + .test( + .POST, + "/api/accounts", + headers: [ + "content-type": "application/json", + "Authorization": "Bearer \(token1.value)" + ], + body: account.encode() + ) { res in + accountRes = try res.content.decode(CustomAccount.self) + XCTAssertTrue(true) + } + .test( + .GET, + "/api/accounts/\(accountRes.id!)", + headers: [ + "Authorization": "Bearer \(token2.value)" + ] + ) { res in + XCTAssertEqual(res.status, .unauthorized) + } + .test( + .GET, + "/api/accounts/\(accountRes.id!)", + headers: [ + "Authorization": "Bearer \(token1.value)" + ] + ) { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqualJSON(res.body.string, account) + } + } } diff --git a/Tests/CorvusTests/Models/CustomAccount.swift b/Tests/CorvusTests/Models/CustomAccount.swift new file mode 100644 index 0000000..a60888a --- /dev/null +++ b/Tests/CorvusTests/Models/CustomAccount.swift @@ -0,0 +1,63 @@ +import Corvus +import Fluent +import Foundation + +final class CustomAccount: CorvusModel { + + static let schema = "custom_accounts" + + @ID + var id: UUID? { + didSet { + $id.exists = true + } + } + + @Field(key: "name") + var name: String + + @Parent(key: "user_id") + var user: CustomUser + + @Children(for: \.$account) + var transactions: [CustomTransaction] + + init(id: UUID? = nil, name: String, userID: CustomUser.IDValue) { + self.id = id + self.name = name + self.$user.id = userID + } + + init() {} +} + +struct CreateCustomAccount: Migration { + + func prepare(on database: Database) -> EventLoopFuture { + return database.schema(CustomAccount.schema) + .id() + .field("name", .string, .required) + .field( + "user_id", + .uuid, + .references(CustomUser.schema, "id") + ) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + return database.schema(CustomAccount.schema).delete() + } +} + +extension CustomAccount: Equatable { + static func == (lhs: CustomAccount, rhs: CustomAccount) -> Bool { + var result = lhs.name == rhs.name + + if let lhsId = lhs.id, let rhsId = rhs.id { + result = result && lhsId == rhsId + } + + return result + } +} diff --git a/Tests/CorvusTests/Models/CustomToken.swift b/Tests/CorvusTests/Models/CustomToken.swift new file mode 100644 index 0000000..074b3dd --- /dev/null +++ b/Tests/CorvusTests/Models/CustomToken.swift @@ -0,0 +1,60 @@ +import Corvus +import Foundation +import Fluent + +public final class CustomToken: CorvusModelUserToken { + + public static let schema = "custom_tokens" + + @ID + public var id: UUID? + + @Field(key: "value") + public var value: String + + @Parent(key: "user_id") + public var user: CustomUser + + @Timestamp(key: "deleted_at", on: .delete) + var deletedAt: Date? + + public init() { } + + + public init(id: UUID? = nil, value: String, userID: CustomUser.IDValue) { + self.id = id + self.value = value + self.$user.id = userID + } +} + +public struct CreateCustomToken: Migration { + + public init() {} + + public func prepare(on database: Database) -> EventLoopFuture { + database.schema(CustomToken.schema) + .id() + .field("value", .string, .required) + .field( + "user_id", + .uuid, + .required, + .references(CustomUser.schema, .id) + ) + .field("deleted_at", .date) + .unique(on: "value") + .create() + } + + public func revert(on database: Database) -> EventLoopFuture { + database.schema(CustomToken.schema).delete() + } +} + +extension CustomToken { + + public var isValid: Bool { + true + } +} diff --git a/Tests/CorvusTests/Models/CustomTransaction.swift b/Tests/CorvusTests/Models/CustomTransaction.swift new file mode 100644 index 0000000..ce3a843 --- /dev/null +++ b/Tests/CorvusTests/Models/CustomTransaction.swift @@ -0,0 +1,70 @@ +import Corvus +import Fluent +import Foundation + +final class CustomTransaction: CorvusModel { + + static let schema = "custom_transactions" + + @ID(key: .id) + var id: UUID? + + @Field(key: "amount") + var amount: Double + + @Field(key: "currency") + var currency: String + + @Field(key: "date") + var date: Date + + @Parent(key: "account_id") + var account: CustomAccount + + init( + id: UUID? = nil, + amount: Double, + currency: String, + date: Date, + accountID: CustomAccount.IDValue + ) { + self.id = id + self.amount = amount + self.currency = currency + self.date = date + self.$account.id = accountID + } + + init() {} +} + +struct CreateCustomTransaction: Migration { + + func prepare(on database: Database) -> EventLoopFuture { + return database.schema(CustomTransaction.schema) + .id() + .field("amount", .double, .required) + .field("currency", .string, .required) + .field("date", .datetime, .required) + .field("account_id", .uuid, .references(CustomAccount.schema, .id)) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + return database.schema(CustomAccount.schema).delete() + } +} + +extension CustomTransaction: Equatable { + static func == (lhs: CustomTransaction, rhs: CustomTransaction) -> Bool { + var result = lhs.amount == rhs.amount + && lhs.currency == rhs.currency + && lhs.$account.id == rhs.$account.id + + if let lhsID = lhs.id, let rhsID = rhs.id { + result = result && lhsID == rhsID + } + + return result + } +} diff --git a/Tests/CorvusTests/Models/CustomUser.swift b/Tests/CorvusTests/Models/CustomUser.swift new file mode 100644 index 0000000..70d892c --- /dev/null +++ b/Tests/CorvusTests/Models/CustomUser.swift @@ -0,0 +1,80 @@ +import Corvus +import Fluent +import Vapor +import Foundation + +public final class CustomUser: CorvusModel { + + public static let schema = "custom_users" + + @ID + public var id: UUID? + + @Field(key: "name") + public var name: String + + @Field(key: "surname") + public var surname: String + + @Field(key: "email") + public var email: String + + @Field(key: "password_hash") + public var passwordHash: String + + @Timestamp(key: "deleted_at", on: .delete) + var deletedAt: Date? + + public init() { } + + public init( + id: UUID? = nil, + name: String, + surname: String, + email: String, + passwordHash: String + ) { + self.id = id + self.name = name + self.surname = surname + self.email = email + self.passwordHash = passwordHash + } +} + +public struct CreateCustomUser: Migration { + + public init() {} + + public func prepare(on database: Database) -> EventLoopFuture { + database.schema(CustomUser.schema) + .id() + .field("name", .string, .required) + .field("surname", .string, .required) + .field("email", .string, .required) + .field("password_hash", .string, .required) + .field("deleted_at", .date) + .create() + } + + public func revert(on database: Database) -> EventLoopFuture { + database.schema(CustomUser.schema).delete() + } +} + +extension CustomUser: CorvusModelUser { + + public func verify(password: String) throws -> Bool { + try Bcrypt.verify(password, created: self.passwordHash) + } +} + +extension CustomUser { + + public func generateToken() throws -> CorvusToken { + try .init( + value: [UInt8].random(count: 16).base64, + userID: self.requireID() + ) + } +} From 96da793ce0a9d8388a7568abf45cd703210b0c5f Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Tue, 7 Apr 2020 12:47:01 +0200 Subject: [PATCH 09/16] Changed auth group tests to use User component --- .../Authentication/CorvusModelUser.swift | 67 +------------------ .../Corvus/Authentication/CorvusUser.swift | 10 ++- Sources/Corvus/Endpoints/User.swift | 16 ++--- Tests/CorvusTests/AuthenticationTests.swift | 25 +++---- Tests/CorvusTests/Models/CustomUser.swift | 4 ++ 5 files changed, 28 insertions(+), 94 deletions(-) diff --git a/Sources/Corvus/Authentication/CorvusModelUser.swift b/Sources/Corvus/Authentication/CorvusModelUser.swift index bb36b7c..bfa685e 100644 --- a/Sources/Corvus/Authentication/CorvusModelUser.swift +++ b/Sources/Corvus/Authentication/CorvusModelUser.swift @@ -1,69 +1,4 @@ import Fluent import Vapor -public protocol CorvusModelUser: CorvusModel, Authenticatable { - - var name: String { get set } - var passwordHash: String { get set } - func verify(password: String) throws -> Bool -} - -extension CorvusModelUser { - - internal init(id: Self.IDValue? = nil, password: String, name: String) throws { - self.init() - self.name = name - self.passwordHash = try Bcrypt.hash(password) - } -} - -extension CorvusModelUser { - public static func authenticator( - database: DatabaseID? = nil - ) -> CorvusModelUserAuthenticator { - CorvusModelUserAuthenticator(database: database) - } - - var _$name: Field { - guard let mirror = Mirror(reflecting: self).descendant("_name"), - let username = mirror as? Field else { - fatalError("name property must be declared using @Field") - } - - return username - } - - var _$passwordHash: Field { - guard let mirror = Mirror(reflecting: self).descendant("_passwordHash"), - let passwordHash = mirror as? Field else { - fatalError("passwordHash property must be declared using @Field") - } - - return passwordHash - } -} - -public struct CorvusModelUserAuthenticator: BasicAuthenticator - where User: CorvusModelUser -{ - public let database: DatabaseID? - - public func authenticate( - basic: BasicAuthorization, - for request: Request - ) -> EventLoopFuture { - User.query(on: request.db(self.database)) - .filter(\._$name == basic.username) - .first() - .flatMapThrowing - { - guard let user = $0 else { - return nil - } - guard try user.verify(password: basic.password) else { - return nil - } - return user - } - } -} +public protocol CorvusModelUser: ModelUser {} diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index 5688d83..5b03cac 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -2,7 +2,7 @@ import Vapor import Fluent /// A default implementation of a user for basic authentication. -public final class CorvusUser: CorvusModelUser { +public final class CorvusUser: CorvusModel { /// The corresponding database schema. public static let schema = "corvus_users" @@ -68,7 +68,13 @@ public struct CreateCorvusUser: Migration { /// An extension to conform to the `ModelUser` protocol, which provides /// functionality to authenticate a user with username and password. -extension CorvusUser { +extension CorvusUser: CorvusModelUser { + + /// Provides a path to the user's username (or in Corvus, the email). + public static let usernameKey = \CorvusUser.$name + + /// Provides a path to the user's password. + public static let passwordHashKey = \CorvusUser.$passwordHash /// Verifies a given string by checking if it matches a user's password. /// diff --git a/Sources/Corvus/Endpoints/User.swift b/Sources/Corvus/Endpoints/User.swift index 40b60b4..04d55a6 100644 --- a/Sources/Corvus/Endpoints/User.swift +++ b/Sources/Corvus/Endpoints/User.swift @@ -3,7 +3,7 @@ import Fluent /// A class that contains Create, Read, Update and Delete functionality for a /// generic type `T` conforming to `CorvusModel` grouped under a given path. -public final class User: Endpoint { +public final class User: Endpoint { /// The route path to the parameters. let pathComponents: [PathComponent] @@ -32,12 +32,9 @@ public final class User: Endpoint { } return Group(pathComponents) { - Custom(type: .post) { req in + Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) - let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) // This works because initializers of CorvusModelUser and CorvusUser are equal, what happens if not? - return user - .save(on: req.db) - .flatMapThrowing { user.name } + return requestContent.save(on: req.db).map { .ok } } BasicAuthGroup { @@ -55,12 +52,9 @@ public final class User: Endpoint { /// SoftDelete functionality grouped under one. public var contentWithSoftDelete: Endpoint { Group(pathComponents) { - Custom(type: .post) { req in + Custom(type: .post) { req in let requestContent = try req.content.decode(T.self) - let user = try T.init(password: requestContent.passwordHash, name: requestContent.name) - return user - .save(on: req.db) - .flatMapThrowing { user.name } + return requestContent.save(on: req.db).map { .ok } } BasicAuthGroup { diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index 5e4659c..9f28d93 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -12,7 +12,7 @@ final class AuthenticationTests: XCTestCase { var content: Endpoint { Group("api") { - CRUD("users", softDelete: false) + User("users", softDelete: false) BasicAuthGroup("accounts") { Create() @@ -43,18 +43,13 @@ final class AuthenticationTests: XCTestCase { ) let account = Account(name: "Berzan") - var response: Account! try app.testable() .test( .POST, "/api/users", headers: ["content-type": "application/json"], - body: user.encode(), - afterResponse: { - response = try $0.content.decode(Account.self) - account.id = response.id - } + body: user.encode() ) .test( .POST, @@ -63,10 +58,7 @@ final class AuthenticationTests: XCTestCase { body: account.encode() ) { res in print(res.body.string) - XCTAssertEqualJSON( - res.body.string, - account - ) + XCTAssertEqual(res.status, .ok) } } @@ -75,7 +67,7 @@ final class AuthenticationTests: XCTestCase { var content: Endpoint { Group("api") { - CRUD("users", softDelete: false) + User("users", softDelete: false) BasicAuthGroup("accounts") { Create() @@ -129,7 +121,7 @@ final class AuthenticationTests: XCTestCase { var content: Endpoint { Group("api") { - CRUD("users", softDelete: false) + User("users", softDelete: false) Login("login") @@ -146,6 +138,7 @@ final class AuthenticationTests: XCTestCase { app.databases.use(.sqlite(.memory), as: .test, isDefault: true) app.middleware.use(CorvusToken.authenticator().middleware()) + app.middleware.use(CorvusUser.authenticator().middleware()) app.migrations.add(CreateAccount()) app.migrations.add(CreateCorvusUser()) app.migrations.add(CreateCorvusToken()) @@ -180,7 +173,7 @@ final class AuthenticationTests: XCTestCase { ) { res in token = try res.content.decode(CorvusToken.self) XCTAssertTrue(true) - } + } .test( .POST, "/api/accounts", @@ -200,7 +193,7 @@ final class AuthenticationTests: XCTestCase { var content: Endpoint { Group("api") { - CRUD("users", softDelete: false) + User("users", softDelete: false) Login("login") @@ -263,6 +256,7 @@ final class AuthenticationTests: XCTestCase { app.databases.use(.sqlite(.memory), as: .test, isDefault: true) app.middleware.use(CorvusToken.authenticator().middleware()) + app.middleware.use(CorvusUser.authenticator().middleware()) app.migrations.add(CreateSecureAccount()) app.migrations.add(CreateCorvusUser()) app.migrations.add(CreateCorvusToken()) @@ -392,6 +386,7 @@ final class AuthenticationTests: XCTestCase { app.databases.use(.sqlite(.memory), as: .test, isDefault: true) app.middleware.use(CustomToken.authenticator().middleware()) + app.middleware.use(CustomUser.authenticator().middleware()) app.migrations.add(CreateCustomAccount()) app.migrations.add(CreateCustomUser()) app.migrations.add(CreateCustomToken()) diff --git a/Tests/CorvusTests/Models/CustomUser.swift b/Tests/CorvusTests/Models/CustomUser.swift index 70d892c..18e6f8c 100644 --- a/Tests/CorvusTests/Models/CustomUser.swift +++ b/Tests/CorvusTests/Models/CustomUser.swift @@ -64,6 +64,10 @@ public struct CreateCustomUser: Migration { extension CustomUser: CorvusModelUser { + public static let usernameKey = \CustomUser.$name + + public static let passwordHashKey = \CustomUser.$passwordHash + public func verify(password: String) throws -> Bool { try Bcrypt.verify(password, created: self.passwordHash) } From 77c8c4c1a92a8ff1560a98488a5df263110c0af0 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Tue, 7 Apr 2020 14:32:21 +0200 Subject: [PATCH 10/16] Added test for user auth modifier --- Tests/CorvusTests/AuthenticationTests.swift | 84 +++++++++++++++++++ .../Utilities/CorvusUser+Equatable.swift | 14 ++++ 2 files changed, 98 insertions(+) create mode 100644 Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index 9f28d93..be93f72 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -491,4 +491,88 @@ final class AuthenticationTests: XCTestCase { XCTAssertEqualJSON(res.body.string, account) } } + + func testUserAuthModifier() throws { + final class UserAuthModifierTest: RestApi { + + var content: Endpoint { + Group("api") { + Create() + User("users", softDelete: false) + Login("login") + } + } + } + + let app = Application(.testing) + defer { app.shutdown() } + let userAuthModifierTest = UserAuthModifierTest() + + app.databases.use(.sqlite(.memory), as: .test, isDefault: true) + app.middleware.use(CorvusToken.authenticator().middleware()) + app.middleware.use(CorvusUser.authenticator().middleware()) + app.migrations.add(CreateSecureAccount()) + app.migrations.add(CreateCorvusUser()) + app.migrations.add(CreateCorvusToken()) + + try app.autoMigrate().wait() + + try app.register(collection: userAuthModifierTest) + + let user1 = CorvusUser( + name: "berzan", + passwordHash: try Bcrypt.hash("pass") + ) + + let user2 = CorvusUser( + name: "paul", + passwordHash: try Bcrypt.hash("pass") + ) + + let basic1 = "berzan:pass" + .data(using: .utf8)! + .base64EncodedString() + + let basic2 = "paul:pass" + .data(using: .utf8)! + .base64EncodedString() + + var userRes: CorvusUser! + + try app.testable() + .test( + .POST, + "/api", + headers: ["content-type": "application/json"], + body: user1.encode(), + afterResponse: { res in + userRes = try res.content.decode(CorvusUser.self) + } + ) + .test( + .POST, + "/api/users", + headers: ["content-type": "application/json"], + body: user2.encode() + ) + .test( + .GET, + "/api/users/\(userRes.id!)", + headers: [ + "Authorization": "Basic \(basic2)" + ] + ) { res in + XCTAssertEqual(res.status, .unauthorized) + } + .test( + .GET, + "/api/users/\(userRes.id!)", + headers: [ + "Authorization": "Basic \(basic1)" + ] + ) { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqualJSON(res.body.string, user1) + } + } } diff --git a/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift b/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift new file mode 100644 index 0000000..4df945c --- /dev/null +++ b/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift @@ -0,0 +1,14 @@ +import Corvus + +extension CorvusUser: Equatable { + + public static func == (lhs: CorvusUser, rhs: CorvusUser) -> Bool { + var result = lhs.name == rhs.name + + if let lhsId = lhs.id, let rhsId = rhs.id { + result = result && lhsId == rhsId + } + + return result + } +} From 6f2ed680262938e202792b5b9bd33929b7c7a038 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Tue, 7 Apr 2020 14:50:37 +0200 Subject: [PATCH 11/16] Applied SwiftLint --- .swiftlint.yml | 133 ++++++++++++++---- .../Authentication/CorvusModelUserToken.swift | 6 +- Sources/Corvus/Endpoints/Login.swift | 5 +- .../Endpoints/Modifiers/AuthModifier.swift | 3 +- Tests/CorvusTests/ApplicationTests.swift | 15 +- Tests/CorvusTests/AuthenticationTests.swift | 1 + 6 files changed, 128 insertions(+), 35 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index cc82acc..f457105 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,18 +1,11 @@ -# SwiftLint file LS1 TUM -# Created by Paul Schmiedmayer on 08/16/18. -# Copyright © 2018 Paul Schmiedmayer. All rights reserved. -# The opt_in_rules also include all rules that are enabled by default to provide a good overview of all rules. - -disabled_rules: - - discouraged_optional_collection # TODO: Enable as soon as https://github.com/realm/SwiftLint/issues/2298 is closed. - # Prefer empty collection over optional collection. - - leading_whitespace - # Files should not contain leading whitespace. - - opening_brace - - first_where - # Prefer using ``.first(where:)`` over ``.filter { }.first` in collections. - -opt_in_rules: +# Apodini SwiftLint file Apodini + +# The whitelist_rules configuration also includes rules that are enabled by default to provide a good overview of all rules. +whitelist_rules: + - anyobject_protocol: + # Prefer using AnyObject over class for class-only protocols. + - array_init + # Prefer using Array(seq) over seq.map { $0 } to convert a sequence into an Array. - block_based_kvo # Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. - class_delegate_protocol @@ -21,13 +14,14 @@ opt_in_rules: # Closing brace with closing parenthesis should not have any whitespaces in the middle. - closure_body_length # Closure bodies should not span too many lines. - # See closure_body_length below for the exact configuration. - closure_end_indentation # Closure end should have the same indentation as the line that started it. - closure_parameter_position # Closure parameters should be on the same line as opening brace. - closure_spacing # Closure expressions should have a single space inside each brace. + - collection_alignment + # All elements in a collection literal should be vertically aligned - colon # Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. - comma @@ -36,8 +30,14 @@ opt_in_rules: # The initializers declared in compiler protocols such as ExpressibleByArrayLiteral shouldn't be called directly. - conditional_returns_on_newline # Conditional statements should always return on the next line + - contains_over_filter_count + # Prefer contains over comparing filter(where:).count to 0. + - contains_over_filter_is_empty + # Prefer contains over using filter(where:).isEmpty - contains_over_first_not_nil # Prefer `contains` over `first(where:) != nil` + - contains_over_range_nil_comparison + # Prefer contains over range(of:) != nil and range(of:) == nil - control_statement # if, for, guard, switch, while, and catch statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses. - convenience_type @@ -50,8 +50,14 @@ opt_in_rules: # Discouraged direct initialization of types that can be harmful. e.g. UIDevice(), Bundle() - discouraged_optional_boolean # Prefer non-optional booleans over optional booleans. + # - discouraged_optional_collection # Enable as soon as https://github.com/realm/SwiftLint/issues/2298 is fixed + # Prefer empty collection over optional collection. + - duplicate_imports + # Duplicate Imports - dynamic_inline # Avoid using 'dynamic' and '@inline(__always)' together. + - empty_collection_literal + # Prefer checking isEmpty over comparing collection to an empty array or dictionary literal. - empty_count # Prefer checking `isEmpty` over comparing `count` to zero. - empty_enum_arguments @@ -62,11 +68,21 @@ opt_in_rules: # When using trailing closures, empty parentheses should be avoided after the method call. - empty_string # Prefer checking `isEmpty` over comparing string to an empty string literal. + - empty_xctest_method + # Empty XCTest method should be avoided. + - enum_case_associated_values_count + # Number of associated values in an enum case should be low + - explicit_init + # Explicitly calling .init() should be avoided. - fatal_error_message # A fatalError call should have a message. - file_length # Files should not span too many lines. # See file_length below for the exact configuration. + - file_types_order + # Specifies how the types within a file should be ordered. + - flatmap_over_map_reduce + # Prefer flatMap over map followed by reduce([], +). - for_where # where clauses are preferred over a single if inside a for. - force_cast @@ -83,6 +99,8 @@ opt_in_rules: # See function_parameter_count below for the exact configuration. - generic_type_name # Generic type name should only contain alphanumeric characters, start with an uppercase character and span between 1 and 20 characters in length. + - identical_operands + # Comparing two identical operands is likely a mistake. - identifier_name # Identifier names should only contain alphanumeric characters and start with a lowercase character or should only contain capital letters. # In an exception to the above, variable names may start with a capital letter when they are declared static and immutable. @@ -93,6 +111,8 @@ opt_in_rules: # Prefer implicit returns in closures. - implicitly_unwrapped_optional # Implicitly unwrapped optionals should be avoided when possible. + - indentation_width + # Indent code using either one tab or the configured amount of spaces, unindent to match previous indentations. Don’t indent the first line. - inert_defer # If defer is at the end of its parent scope, it will be executed right where it is anyway. - is_disjoint @@ -102,29 +122,45 @@ opt_in_rules: - large_tuple # Tuples shouldn't have too many members. Create a custom type instead. # See large_tuple below for the exact configuration. + - last_where + # Prefer using .last(where:) over .filter { }.last in collections. + - leading_whitespace + # Files should not contain leading whitespace. - legacy_cggeometry_functions # CGGeometry: Struct extension properties and methods are preferred over legacy functions - legacy_constant # Struct-scoped constants are preferred over legacy global constants (CGSize, CGRect, NSPoint, ...). - legacy_constructor # Swift constructors are preferred over legacy convenience functions (CGPointMake, CGSizeMake, UIOffsetMake, ...). + - legacy_hashing + # Prefer using the hash(into:) function instead of overriding hashValue + - legacy_multiple + # Prefer using the isMultiple(of:) function instead of using the remainder operator (%). + - legacy_nsgeometry_functions + # Struct extension properties and methods are preferred over legacy functions + - legacy_random + # Prefer using type.random(in:) over legacy functions. - line_length # Lines should not span too many characters. # See line_length below for the exact configuration. - literal_expression_end_indentation # Array and dictionary literal end should have the same indentation as the line that started it. + - lower_acl_than_parent + # Ensure definitions have a lower access control level than their enclosing parent - mark # MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...' + - missing_docs + # Declarations should be documented. - modifier_order # Modifier order should be consistent. - multiline_arguments # Arguments should be either on the same line, or one per line. - multiline_function_chains # Chained function calls should be either on the same line, or one per line. + - multiline_literal_brackets + # Multiline literals should have their surrounding brackets in a new line. - multiline_parameters # Functions and methods parameters should be either on the same line, or one per line. - - multiple_closures_with_trailing_closure - # Trailing closure syntax should not be used when passing more than one closure argument. - nesting # Types and statements should only be nested to a certain level deep. # See nesting below for the exact configuration. @@ -132,18 +168,30 @@ opt_in_rules: # Prefer Nimble operator overloads over free matcher functions. - no_fallthrough_only # Fallthroughs can only be used if the case contains at least one other statement. + - no_space_in_method_call + # Don’t add a space between the method name and the parentheses. - notification_center_detachment # An object should only remove itself as an observer in deinit. + - nslocalizedstring_key + # Static strings should be used as key in NSLocalizedString in order to genstrings work. + - nsobject_prefer_isequal + # NSObject subclasses should implement isEqual instead of ==. - object_literal # Prefer object literals over image and color inits. - operator_usage_whitespace # Operators should be surrounded by a single whitespace when they are being used. - operator_whitespace # Operators should be surrounded by a single whitespace when defining them. + - optional_enum_case_matching + # Matching an enum case against an optional enum without ‘?’ is supported on Swift 5.1 and above. + - orphaned_doc_comment + # A doc comment should be attached to a declaration. - overridden_super_call # Some overridden methods should always call super - pattern_matching_keywords # Combine multiple pattern matching bindings by moving keywords out of tuples. + - prefer_self_type_over_type_of_self + # Prefer Self over type(of: self) when accessing properties or calling methods. - private_action # IBActions should be private. - private_outlet @@ -160,10 +208,16 @@ opt_in_rules: # UIViewController loadView()) - protocol_property_accessors_order # When declaring properties in protocols, the order of accessors should be get set. + - reduce_boolean + # Prefer using .allSatisfy() or .contains() over reduce(true) or reduce(false) + - reduce_into + # Prefer reduce(into:_:) over reduce(_:_:) for copy-on-write types - redundant_discardable_let # Prefer _ = foo() over let _ = foo() when discarding a result from a function. - redundant_nil_coalescing # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant + - redundant_objc_attribute + # Objective-C attribute (@objc) is redundant in declaration. - redundant_optional_initialization # Initializing an optional variable with nil is redundant. - redundant_set_access_control @@ -178,16 +232,26 @@ opt_in_rules: # Return arrow and return type should be separated by a single space or on a separate line. - shorthand_operator # Prefer shorthand operators (+=, -=, *=, /=) over doing the operation and assigning. + - single_test_class + # Test files should contain a single QuickSpec or XCTestCase class. - sorted_first_last # Prefer using `min()`` or `max()`` over `sorted().first` or `sorted().last` - statement_position # Else and catch should be on the same line, one space after the previous declaration. + - static_operator + # Operators should be declared as static functions, not free functions. + - superfluous_disable_command + # SwiftLint ‘disable’ commands are superfluous when the disabled rule would not have triggered a violation in the disabled region. Use “ - ” if you wish to document a command. - switch_case_alignment # Case statements should vertically align with their enclosing switch statement, or indented if configured otherwise. - syntactic_sugar # Shorthand syntactic sugar should be used, i.e. [Int] instead of Array. - todo # TODOs and FIXMEs should be resolved. + - toggle_bool + # Prefer someBool.toggle() over someBool = !someBool. + - trailing_closure + # Trailing closure syntax should be used whenever possible. - trailing_comma # Trailing commas in arrays and dictionaries should be avoided/enforced. - trailing_newline @@ -209,12 +273,24 @@ opt_in_rules: # Avoid using unneeded break statements. - unneeded_parentheses_in_closure_argument # Parentheses are not needed when declaring closure arguments. + - untyped_error_in_catch + # Catch statements should not declare error variables without type casting. + - unused_capture_list + # Unused reference in a capture list should be removed. - unused_closure_parameter # Unused parameter in a closure should be replaced with _. + - unused_control_flow_label + # Unused control flow label should be removed. + - unused_declaration + # Declarations should be referenced at least once within all files linted. - unused_enumerated # When the index or the item is not used, .enumerated() can be removed. + - unused_import + # All imported modules should be required to make the file compile. - unused_optional_binding # Prefer != nil over let _ = + - unused_setter_value + # Setter value is not used. - valid_ibinspectable # @IBInspectable should be applied to variables only, have its type explicit and be of a supported type - vertical_parameter_alignment @@ -224,6 +300,10 @@ opt_in_rules: - vertical_whitespace # Limit vertical whitespace to a single empty line. # See vertical_whitespace below for the exact configuration. + - vertical_whitespace_closing_braces + # Don’t include vertical whitespace (empty line) before closing braces. + - vertical_whitespace_opening_braces + # Don’t include vertical whitespace (empty line) after opening braces. - void_return # Prefer -> Void over -> (). - weak_delegate @@ -233,22 +313,27 @@ opt_in_rules: - yoda_condition # The variable should be placed on the left, the constant on the right of a comparison operator. +# These rules still get triggered even though they are not in the whitelist_rules +disabled_rules: + - multiple_closures_with_trailing_closure + # Trailing closure syntax should not be used when passing more than one closure argument. + - opening_brace + excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods - - Tests - - "*/Pods" - - "*/*/Pods" - - "*/*/*/Pods" - - "*/*/*/*/Pods" - - "*/*/*/*/*/Pods" - .build + - .swiftpm - R.generated.swift closure_body_length: # Closure bodies should not span too many lines. - 25 # warning - default: 20 - 25 # error - default: 100 +enum_case_associated_values_count: # Number of associated values in an enum case should be low + - 5 # warning - default: 5 + - 5 # error - default: 6 + file_length: # Files should not span too many lines. - 500 # warning - default: 400 - 500 # error - default: 1000 diff --git a/Sources/Corvus/Authentication/CorvusModelUserToken.swift b/Sources/Corvus/Authentication/CorvusModelUserToken.swift index 2bfff83..5c87e4a 100644 --- a/Sources/Corvus/Authentication/CorvusModelUserToken.swift +++ b/Sources/Corvus/Authentication/CorvusModelUserToken.swift @@ -1,6 +1,7 @@ import Fluent import Vapor +// swiftlint:disable identifier_name public protocol CorvusModelUserToken: CorvusModel { associatedtype User: CorvusModel & Authenticatable @@ -11,7 +12,7 @@ public protocol CorvusModelUserToken: CorvusModel { extension CorvusModelUserToken { - internal init(id: Self.IDValue? = nil, value: String, userId: User.IDValue) { + init(id: Self.IDValue? = nil, value: String, userId: User.IDValue) { self.init() self.value = value _$user.id = userId @@ -45,7 +46,8 @@ extension CorvusModelUserToken { } } -public struct CorvusModelUserTokenAuthenticator: BearerAuthenticator +public struct CorvusModelUserTokenAuthenticator: +BearerAuthenticator { public typealias User = T.User public let database: DatabaseID? diff --git a/Sources/Corvus/Endpoints/Login.swift b/Sources/Corvus/Endpoints/Login.swift index 43b2cd5..4c54736 100644 --- a/Sources/Corvus/Endpoints/Login.swift +++ b/Sources/Corvus/Endpoints/Login.swift @@ -24,7 +24,10 @@ where T.User: CorvusModelUser { /// - Returns: An `EventLoopFuture` containing the created `CorvusToken`. public func handler(_ req: Request) throws -> EventLoopFuture { let user = try req.auth.require(T.User.self) - let token = T.init(value: [UInt8].random(count: 16).base64, userId: try user.requireID()) + let token = T.init( + value: [UInt8].random(count: 16).base64, + userId: try user.requireID() + ) return token.save(on: req.db).map { token } } diff --git a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift index f119535..6000243 100644 --- a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift @@ -4,7 +4,8 @@ import Fluent /// A class that wraps a component which utilizes an `.auth()` modifier. That /// allows Corvus to chain modifiers, as it gets treated as any other struct /// conforming to `AuthEndpoint`. -public final class AuthModifier: AuthEndpoint { +public final class AuthModifier: +AuthEndpoint { /// The return type for the `.handler()` modifier. public typealias Element = Q.Element diff --git a/Tests/CorvusTests/ApplicationTests.swift b/Tests/CorvusTests/ApplicationTests.swift index e8c7422..85fa734 100644 --- a/Tests/CorvusTests/ApplicationTests.swift +++ b/Tests/CorvusTests/ApplicationTests.swift @@ -4,6 +4,7 @@ import FluentSQLiteDriver import XCTVapor import Foundation +// swiftlint:disable file_length type_body_length function_body_length final class ApplicationTests: XCTestCase { func testCreate() throws { @@ -403,7 +404,7 @@ final class ApplicationTests: XCTestCase { try app.register(collection: readOneTest) let account = Account(id: nil, name: "Berzan") - var AccountRes: Account! + var accountRes: Account! try app.testable() .test( @@ -412,27 +413,27 @@ final class ApplicationTests: XCTestCase { headers: ["content-type": "application/json"], body: account.encode() ) { res in - AccountRes = try res.content.decode(Account.self) + accountRes = try res.content.decode(Account.self) } - .test(.GET, "/api/accounts/\(AccountRes.id!)") { res in + .test(.GET, "/api/accounts/\(accountRes.id!)") { res in let response = try res.content.decode(Account.self) XCTAssertEqual(res.status, .ok) XCTAssertEqual(response, account) } - .test(.DELETE, "/api/accounts/\(AccountRes.id!)") { res in + .test(.DELETE, "/api/accounts/\(accountRes.id!)") { res in print(res.body.string) XCTAssertEqual(res.status, .ok) } - .test(.GET, "/api/accounts/\(AccountRes.id!)") { res in + .test(.GET, "/api/accounts/\(accountRes.id!)") { res in XCTAssertEqual(res.status, .notFound) } .test( .PATCH, - "/api/accounts/trash/\(AccountRes.id!)/restore" + "/api/accounts/trash/\(accountRes.id!)/restore" ) { res in XCTAssertEqual(res.status, .ok) } - .test(.GET, "/api/accounts/\(AccountRes.id!)") { res in + .test(.GET, "/api/accounts/\(accountRes.id!)") { res in let response = try res.content.decode(Account.self) XCTAssertEqual(res.status, .ok) XCTAssertEqual(response, account) diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index be93f72..f906e9e 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -5,6 +5,7 @@ import Vapor import XCTVapor import Foundation +// swiftlint:disable file_length type_body_length function_body_length final class AuthenticationTests: XCTestCase { func testBasicAuthenticatorSuccess() throws { From ce3d4583e8ba0742ebe95e09142b25f63445ec21 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Tue, 7 Apr 2020 14:54:00 +0200 Subject: [PATCH 12/16] Updated interpolated string line breaks --- .swiftlint.yml | 2 +- Tests/CorvusTests/AuthenticationTests.swift | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index f457105..a66c800 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -362,7 +362,7 @@ line_length: # Lines should not span too many characters. ignores_comments: true # default: false ignores_urls: true # default: false ignores_function_declarations: false # default: false - ignores_interpolated_strings: true # default: false + ignores_interpolated_strings: false # default: false nesting: # Types should be nested at most 2 level deep, and statements should be nested at most 5 levels deep. type_level: diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index f906e9e..c09f454 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -55,7 +55,10 @@ final class AuthenticationTests: XCTestCase { .test( .POST, "/api/accounts", - headers: ["Authorization": "Basic \(basic)", "content-type": "application/json"], + headers: [ + "Authorization": "Basic \(basic)", + "content-type": "application/json" + ], body: account.encode() ) { res in print(res.body.string) @@ -176,17 +179,20 @@ final class AuthenticationTests: XCTestCase { XCTAssertTrue(true) } .test( - .POST, - "/api/accounts", - headers: ["content-type": "application/json", "Authorization": "Bearer \(token.value)"], - body: account.encode() + .POST, + "/api/accounts", + headers: [ + "content-type": "application/json", + "Authorization": "Bearer \(token.value)" + ], + body: account.encode() ) { res in XCTAssertEqual(res.status, .ok) XCTAssertEqualJSON( res.body.string, account ) - } + } } func testBearerAuthenticatorFailure() throws { From 20e373af2e20733edaa6d7989978ed08409b27df Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Tue, 7 Apr 2020 15:30:45 +0200 Subject: [PATCH 13/16] Updated documentation --- .jazzy.yml | 1 - .../Authentication/CorvusModelUser.swift | 1 + .../Authentication/CorvusModelUserToken.swift | 38 ++++++++++++++++++- .../Corvus/Authentication/CorvusToken.swift | 8 ++-- .../Corvus/Authentication/CorvusUser.swift | 11 +++--- Sources/Corvus/Endpoints/Create/Create.swift | 1 + Sources/Corvus/Endpoints/Custom.swift | 4 +- Sources/Corvus/Endpoints/Delete/Delete.swift | 2 + .../Corvus/Endpoints/Delete/SoftDelete.swift | 2 + .../Endpoints/Groups/BasicAuthGroup.swift | 4 +- .../Endpoints/Groups/BearerAuthGroup.swift | 4 +- Sources/Corvus/Endpoints/Login.swift | 9 +++-- .../Endpoints/Modifiers/AuthModifier.swift | 11 ++++-- .../Modifiers/Read/ChildrenModifier.swift | 2 + .../Modifiers/Read/FilterModifier.swift | 2 + .../Modifiers/UserAuthModifier.swift | 18 +++++---- Sources/Corvus/Endpoints/Read/ReadAll.swift | 1 + Sources/Corvus/Endpoints/Read/ReadOne.swift | 2 + .../Corvus/Endpoints/Restore/Restore.swift | 2 + Sources/Corvus/Endpoints/Update/Update.swift | 2 + Sources/Corvus/Endpoints/User.swift | 6 +-- .../Endpoints/Utilities/EmptyEndpoint.swift | 6 ++- .../Corvus/Protocols/Endpoints/Endpoint.swift | 12 +++++- .../Protocols/Endpoints/QueryEndpoint.swift | 6 +++ .../Protocols/Endpoints/RestEndpoint.swift | 3 ++ Sources/Corvus/Protocols/RestApi.swift | 3 ++ Tests/CorvusTests/ApplicationTests.swift | 5 ++- 27 files changed, 126 insertions(+), 40 deletions(-) diff --git a/.jazzy.yml b/.jazzy.yml index 4041787..2b486dd 100644 --- a/.jazzy.yml +++ b/.jazzy.yml @@ -1,6 +1,5 @@ module: Corvus author: Berzan Yildiz -min_acl: internal theme: fullwidth output: ./docs documentation: ./*.md diff --git a/Sources/Corvus/Authentication/CorvusModelUser.swift b/Sources/Corvus/Authentication/CorvusModelUser.swift index bfa685e..6da2463 100644 --- a/Sources/Corvus/Authentication/CorvusModelUser.swift +++ b/Sources/Corvus/Authentication/CorvusModelUser.swift @@ -1,4 +1,5 @@ import Fluent import Vapor +/// A protocol that wraps `Vapor`'s `ModelUser` for consistency in naming. public protocol CorvusModelUser: ModelUser {} diff --git a/Sources/Corvus/Authentication/CorvusModelUserToken.swift b/Sources/Corvus/Authentication/CorvusModelUserToken.swift index 5c87e4a..e3ae606 100644 --- a/Sources/Corvus/Authentication/CorvusModelUserToken.swift +++ b/Sources/Corvus/Authentication/CorvusModelUserToken.swift @@ -2,16 +2,33 @@ import Fluent import Vapor // swiftlint:disable identifier_name + +/// A protocol that defines bearer authentication tokens, similar to +/// `ModelUserToken`. public protocol CorvusModelUserToken: CorvusModel { + /// The `User` type the token belongs to. associatedtype User: CorvusModel & Authenticatable + + /// The `String` value of the token. var value: String { get set } + + /// The `User` associated with the token. var user: User { get set } + + /// A boolean that deletes tokens if they're not in use. var isValid: Bool { get } } +/// An extension to provide a default initializer to tokens so that they can +/// be initialized manually in module code. extension CorvusModelUserToken { - + + /// Initializes a token. + /// - Parameters: + /// - id: The unique identifier of the token. + /// - value: The `String` value of the token. + /// - userId: The id of the `User` the token belongs to. init(id: Self.IDValue? = nil, value: String, userId: User.IDValue) { self.init() self.value = value @@ -19,14 +36,19 @@ extension CorvusModelUserToken { } } +/// An extension to provide an authenticator for the token that can be +/// registered to the `Vapor` application, and field accessors for `value` and +/// `user`. extension CorvusModelUserToken { + /// Provides a `Vapor` authenticator defined below. public static func authenticator( database: DatabaseID? = nil ) -> CorvusModelUserTokenAuthenticator { CorvusModelUserTokenAuthenticator(database: database) } + /// Provides access to the `value` attribute. var _$value: Field { guard let mirror = Mirror(reflecting: self).descendant("_value"), let token = mirror as? Field else { @@ -36,6 +58,7 @@ extension CorvusModelUserToken { return token } + /// Provides access to the `user` attribute. var _$user: Parent { guard let mirror = Mirror(reflecting: self).descendant("_user"), let user = mirror as? Parent else { @@ -46,12 +69,23 @@ extension CorvusModelUserToken { } } +/// Provides a `BearerAuthenticator` struct that defines how tokens are +/// authenticated. public struct CorvusModelUserTokenAuthenticator: BearerAuthenticator { + + /// The token's user. public typealias User = T.User + + /// The database the token is saved in. public let database: DatabaseID? - + + /// Authenticates a token. + /// - Parameters: + /// - bearer: The bearer token passed in the request. + /// - request: The `Request` to be authenticated. + /// - Returns: The `User` the token belongs to. public func authenticate( bearer: BearerAuthorization, for request: Request diff --git a/Sources/Corvus/Authentication/CorvusToken.swift b/Sources/Corvus/Authentication/CorvusToken.swift index cc5bef9..9f6e846 100644 --- a/Sources/Corvus/Authentication/CorvusToken.swift +++ b/Sources/Corvus/Authentication/CorvusToken.swift @@ -2,7 +2,7 @@ import Vapor import Fluent /// A default implementation of a bearer token. -public final class CorvusToken: CorvusModelUserToken { +public final class CorvusToken: CorvusModel { /// The corresponding database schema. public static let schema = "corvus_tokens" @@ -67,9 +67,9 @@ public struct CreateCorvusToken: Migration { } } -/// An extension to conform to the `ModelUserToken` protocol, which provides -/// functionality to authenticate a token. -extension CorvusToken { +/// An extension to conform to the `CorvusModelUserToken` protocol, which +/// provides functionality to authenticate a token. +extension CorvusToken: CorvusModelUserToken { /// Prevents tokens from being deleted after authentication. public var isValid: Bool { diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index 5b03cac..fbaf7c3 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -15,7 +15,7 @@ public final class CorvusUser: CorvusModel { @Field(key: "name") public var name: String - /// The password of the user, used during authentication. + /// The hashed password of the user, used during authentication. @Field(key: "password_hash") public var passwordHash: String @@ -31,8 +31,7 @@ public final class CorvusUser: CorvusModel { /// - Parameters: /// - id: The identifier of the user, auto generated if not provided. /// - name: The name of the user. - /// - email: The email (or username) of the user. - /// - password: The password of the user. + /// - passwordHash: The hashed password of the user. public init( id: UUID? = nil, name: String, @@ -66,14 +65,14 @@ public struct CreateCorvusUser: Migration { } } -/// An extension to conform to the `ModelUser` protocol, which provides +/// An extension to conform to the `CorvusModelUser` protocol, which provides /// functionality to authenticate a user with username and password. extension CorvusUser: CorvusModelUser { - /// Provides a path to the user's username (or in Corvus, the email). + /// Provides a path to the user's username. public static let usernameKey = \CorvusUser.$name - /// Provides a path to the user's password. + /// Provides a path to the user's hashed password. public static let passwordHashKey = \CorvusUser.$passwordHash /// Verifies a given string by checking if it matches a user's password. diff --git a/Sources/Corvus/Endpoints/Create/Create.swift b/Sources/Corvus/Endpoints/Create/Create.swift index 6e18719..4ba28a4 100644 --- a/Sources/Corvus/Endpoints/Create/Create.swift +++ b/Sources/Corvus/Endpoints/Create/Create.swift @@ -17,6 +17,7 @@ public final class Create: QueryEndpoint { /// /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing the saved object. + /// - Throws: An `Abort` error if something goes wrong. public func handler(_ req: Request) throws -> EventLoopFuture { diff --git a/Sources/Corvus/Endpoints/Custom.swift b/Sources/Corvus/Endpoints/Custom.swift index dfe8df0..434f6d5 100644 --- a/Sources/Corvus/Endpoints/Custom.swift +++ b/Sources/Corvus/Endpoints/Custom.swift @@ -27,11 +27,11 @@ public final class Custom: RestEndpoint { /// - customHandler: A closure that implements the functionality for the /// `Custom` component. public init( - path: PathComponent..., + pathComponents: PathComponent..., type operationType: OperationType, _ customHandler: @escaping (Request) throws -> EventLoopFuture ) { - self.pathComponents = path + self.pathComponents = pathComponents self.operationType = operationType self.customHandler = customHandler } diff --git a/Sources/Corvus/Endpoints/Delete/Delete.swift b/Sources/Corvus/Endpoints/Delete/Delete.swift index 933c29f..03fed28 100644 --- a/Sources/Corvus/Endpoints/Delete/Delete.swift +++ b/Sources/Corvus/Endpoints/Delete/Delete.swift @@ -30,6 +30,7 @@ public final class Delete: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having found the object with the supplied ID. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { let parameter = String(id.description.dropFirst()) guard let itemId = req.parameters.get( @@ -46,6 +47,7 @@ public final class Delete: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A HTTPStatus of either `.ok`, when the object was /// successfully deleted, or `.notFound`, when the object was not found. + /// - Throws: An `Abort` error if the item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { try query(req) .first() diff --git a/Sources/Corvus/Endpoints/Delete/SoftDelete.swift b/Sources/Corvus/Endpoints/Delete/SoftDelete.swift index 1daf707..9d13378 100644 --- a/Sources/Corvus/Endpoints/Delete/SoftDelete.swift +++ b/Sources/Corvus/Endpoints/Delete/SoftDelete.swift @@ -29,6 +29,7 @@ public final class SoftDelete: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having found the object with the supplied ID. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { let parameter = String(id.description.dropFirst()) guard let itemId = req.parameters.get( @@ -45,6 +46,7 @@ public final class SoftDelete: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A HTTPStatus of either `.ok`, when the object was /// successfully deleted, or `.notFound`, when the object was not found. + /// - Throws: An `Abort` error if the item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { try query(req) .first() diff --git a/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift b/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift index a27a092..e550087 100644 --- a/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift +++ b/Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift @@ -2,7 +2,7 @@ import Vapor import Fluent /// A special type of `Group` that protects its `content` with basic -/// authentication for a generic `ModelUser`. +/// authentication for a generic `CorvusModelUser`. public struct BasicAuthGroup: Endpoint { /// An array of `PathComponent` describing the path that the @@ -32,7 +32,7 @@ public struct BasicAuthGroup: Endpoint { /// A method that registers the `content` of the `BasicAuthGroup` to the /// supplied `RoutesBuilder`. It also registers basic authentication - /// middleware using `T` conforming to `ModelUser`. + /// middleware using `T` conforming to `CorvusModelUser`. /// /// - Parameter routes: A `RoutesBuilder` containing all the information /// about the HTTP route leading to the current component. diff --git a/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift b/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift index 823c524..781ceb6 100644 --- a/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift +++ b/Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift @@ -2,7 +2,7 @@ import Vapor import Fluent /// A special type of `Group` that protects its `content` with bearer token -/// authentication for a generic `ModelUserToken`. +/// authentication for a generic `CorvusModelUserToken`. public struct BearerAuthGroup: Endpoint { /// An array of `PathComponent` describing the path that the @@ -32,7 +32,7 @@ public struct BearerAuthGroup: Endpoint { /// A method that registers the `content` of the `BearerAuthGroup` to the /// supplied `RoutesBuilder`. It also registers basic authentication - /// middleware using `T`conforming to `ModelUserToken`. + /// middleware using `T`conforming to `CorvusModelUserToken`. /// /// - Parameter routes: A `RoutesBuilder` containing all the information /// about the HTTP route leading to the current component. diff --git a/Sources/Corvus/Endpoints/Login.swift b/Sources/Corvus/Endpoints/Login.swift index 4c54736..a03cc71 100644 --- a/Sources/Corvus/Endpoints/Login.swift +++ b/Sources/Corvus/Endpoints/Login.swift @@ -3,7 +3,8 @@ import Fluent /// A class that provides functionality to log in a user with username and /// password credentials sent in a HTTP POST `Request` and save a token for -/// that user. +/// that user. Needs an object of type `T` which represents the token to be +/// created upon login. public final class Login: Endpoint where T.User: CorvusModelUser { @@ -21,7 +22,8 @@ where T.User: CorvusModelUser { /// and saving it in the database. /// /// - Parameter req: An incoming HTTP `Request`. - /// - Returns: An `EventLoopFuture` containing the created `CorvusToken`. + /// - Returns: An `EventLoopFuture` containing the created token. + /// - Throws: An `Abort` error if something goes wrong. public func handler(_ req: Request) throws -> EventLoopFuture { let user = try req.auth.require(T.User.self) let token = T.init( @@ -33,7 +35,8 @@ where T.User: CorvusModelUser { } /// A method that registers the `handler()` to the supplied `RoutesBuilder`. - /// It also registers basic authentication middleware using `CorvusUser`. + /// It also registers basic authentication middleware using the user + /// belonging to the token `T`. /// /// - Parameter routes: A `RoutesBuilder` containing all the information /// about the HTTP route leading to the current component. diff --git a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift index 6000243..3586b00 100644 --- a/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift @@ -3,14 +3,15 @@ import Fluent /// A class that wraps a component which utilizes an `.auth()` modifier. That /// allows Corvus to chain modifiers, as it gets treated as any other struct -/// conforming to `AuthEndpoint`. +/// conforming to `AuthEndpoint`. Requires an object `T` that represents the +/// user to authorize. public final class AuthModifier: AuthEndpoint { /// The return type for the `.handler()` modifier. public typealias Element = Q.Element - /// The return value of the `.handler()`, so the type being operated on in + /// The return value of the `.query()`, so the type being operated on in /// the current component. public typealias QuerySubject = Q.QuerySubject @@ -50,17 +51,19 @@ AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query defined /// by the `queryEndpoint`. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { try queryEndpoint.query(req) } - /// A method which checks if the `CorvusUser` supplied in the `Request` is + /// A method which checks if the user `T` supplied in the `Request` is /// equal to the user belonging to the particular `QuerySubject`. /// /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing an eagerloaded value as /// defined by `Element`. If authentication fails or a user is not found, /// HTTP `.unauthorized` and `.notFound` are thrown respectively. + /// - Throws: An `Abort` error if an item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { let users = try query(req) .with(userKeyPath) @@ -101,7 +104,7 @@ AuthEndpoint { extension AuthEndpoint { /// A modifier used to make sure components only authorize requests where - /// the supplied `CorvusUser` is actually related to the `QuerySubject`. + /// the supplied user `T` is actually related to the `QuerySubject`. /// /// - Parameter user: A `KeyPath` to the related user property. /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` diff --git a/Sources/Corvus/Endpoints/Modifiers/Read/ChildrenModifier.swift b/Sources/Corvus/Endpoints/Modifiers/Read/ChildrenModifier.swift index 3ca0490..d779c24 100644 --- a/Sources/Corvus/Endpoints/Modifiers/Read/ChildrenModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/Read/ChildrenModifier.swift @@ -44,6 +44,7 @@ ReadEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having attached a with modifier to the `queryEndpoint`'s query. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { try queryEndpoint.query(req).with(path) } @@ -54,6 +55,7 @@ ReadEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing an eagerloaded value as /// defined by `Element`. + /// - Throws: An `Abort` error if the item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { try query(req).first().flatMapThrowing { optionalItem in guard let item = optionalItem else { diff --git a/Sources/Corvus/Endpoints/Modifiers/Read/FilterModifier.swift b/Sources/Corvus/Endpoints/Modifiers/Read/FilterModifier.swift index 6081e02..f0d4704 100644 --- a/Sources/Corvus/Endpoints/Modifiers/Read/FilterModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/Read/FilterModifier.swift @@ -38,6 +38,7 @@ public final class FilterModifier: ReadEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having attached a filter to the `queryEndpoint`'s query. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { try queryEndpoint.query(req).filter(filter) } @@ -47,6 +48,7 @@ public final class FilterModifier: ReadEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing an array of the /// `FilterModifier`'s `QuerySubject`. + /// - Throws: An `Abort` error if the item is not found. public func handler(_ req: Request) throws -> EventLoopFuture<[QuerySubject]> { try query(req).all() diff --git a/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift index 9149492..67699b7 100644 --- a/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/UserAuthModifier.swift @@ -1,16 +1,16 @@ import Vapor import Fluent -/// A class that wraps a component which utilizes an `.auth()` modifier. That -/// allows Corvus to chain modifiers, as it gets treated as any other struct -/// conforming to `AuthEndpoint`. +/// A class that wraps a component which utilizes an `.userAuth()` modifier. +/// That allows Corvus to chain modifiers, as it gets treated as any other +/// struct conforming to `AuthEndpoint`. public final class UserAuthModifier: AuthEndpoint where Q.QuerySubject: CorvusModelUser { /// The return type for the `.handler()` modifier. public typealias Element = Q.Element - /// The return value of the `.handler()`, so the type being operated on in + /// The return value of the `.query()`, so the type being operated on in /// the current component. public typealias QuerySubject = Q.QuerySubject @@ -36,17 +36,19 @@ where Q.QuerySubject: CorvusModelUser { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query defined /// by the `queryEndpoint`. + /// - Throws: An `Abort` error if an item is not found. public func query(_ req: Request) throws -> QueryBuilder { try queryEndpoint.query(req) } - /// A method which checks if the `CorvusUser` supplied in the `Request` is + /// A method which checks if the user supplied in the `Request` is /// equal to the user belonging to the particular `QuerySubject`. /// /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing an eagerloaded value as /// defined by `Element`. If authentication fails or a user is not found, /// HTTP `.unauthorized` and `.notFound` are thrown respectively. + /// - Throws: An `Abort` error if an item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { let users = try query(req).all() @@ -74,8 +76,8 @@ where Q.QuerySubject: CorvusModelUser { } } -/// An extension that adds the `.auth()` modifier to components conforming to -/// `AuthEndpoint`. +/// An extension that adds the `.userAuth()` modifier to components conforming +/// to `AuthEndpoint`. extension AuthEndpoint where Self.QuerySubject: CorvusModelUser { /// A modifier used to make sure components only authorize requests where @@ -84,7 +86,7 @@ extension AuthEndpoint where Self.QuerySubject: CorvusModelUser { /// - Parameter user: A `KeyPath` to the related user property. /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` /// to the user. - internal func userAuth() -> UserAuthModifier { + func userAuth() -> UserAuthModifier { UserAuthModifier(self) } } diff --git a/Sources/Corvus/Endpoints/Read/ReadAll.swift b/Sources/Corvus/Endpoints/Read/ReadAll.swift index 108f28c..9d4ee15 100644 --- a/Sources/Corvus/Endpoints/Read/ReadAll.swift +++ b/Sources/Corvus/Endpoints/Read/ReadAll.swift @@ -25,6 +25,7 @@ public final class ReadAll: ReadEndpoint { /// /// - Parameter req: An incoming `Request`. /// - Returns: An array of `QuerySubjects`. + /// - Throws: An `Abort` error if something goes wrong. public func handler(_ req: Request) throws -> EventLoopFuture<[QuerySubject]> { diff --git a/Sources/Corvus/Endpoints/Read/ReadOne.swift b/Sources/Corvus/Endpoints/Read/ReadOne.swift index e3c06f6..d8d960a 100644 --- a/Sources/Corvus/Endpoints/Read/ReadOne.swift +++ b/Sources/Corvus/Endpoints/Read/ReadOne.swift @@ -31,6 +31,7 @@ public final class ReadOne: ReadEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having found the object with the supplied ID. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { let parameter = String(id.description.dropFirst()) guard let itemId = req.parameters.get( @@ -48,6 +49,7 @@ public final class ReadOne: ReadEndpoint { /// /// - Parameter req: An incoming `Request`. /// - Returns: The found object. + /// - Throws: An `Abort` error if the item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { diff --git a/Sources/Corvus/Endpoints/Restore/Restore.swift b/Sources/Corvus/Endpoints/Restore/Restore.swift index 4200182..612aa5e 100644 --- a/Sources/Corvus/Endpoints/Restore/Restore.swift +++ b/Sources/Corvus/Endpoints/Restore/Restore.swift @@ -41,6 +41,7 @@ public final class Restore: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having found the object with the supplied ID. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { let parameter = String(id.description.dropFirst()) guard let itemId = req.parameters.get( @@ -63,6 +64,7 @@ public final class Restore: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A HTTPStatus of either `.ok`, when the object was /// successfully deleted, or `.notFound`, when the object was not found. + /// - Throws: An `Abort` error if something goes wrong. public func handler(_ req: Request) throws -> EventLoopFuture { try query(req) .first() diff --git a/Sources/Corvus/Endpoints/Update/Update.swift b/Sources/Corvus/Endpoints/Update/Update.swift index 3855ede..8ed356d 100644 --- a/Sources/Corvus/Endpoints/Update/Update.swift +++ b/Sources/Corvus/Endpoints/Update/Update.swift @@ -27,6 +27,7 @@ public final class Update: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: A `QueryBuilder`, which represents a `Fluent` query after /// having found the object with the supplied ID. + /// - Throws: An `Abort` error if the item is not found. public func query(_ req: Request) throws -> QueryBuilder { let parameter = String(id.description.dropFirst()) guard let itemId = req.parameters.get( @@ -43,6 +44,7 @@ public final class Update: AuthEndpoint { /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing the updated value of the /// object of type `QuerySubject`. + /// - Throws: An `Abort` error if the item is not found. public func handler(_ req: Request) throws -> EventLoopFuture { diff --git a/Sources/Corvus/Endpoints/User.swift b/Sources/Corvus/Endpoints/User.swift index 04d55a6..eca7d2f 100644 --- a/Sources/Corvus/Endpoints/User.swift +++ b/Sources/Corvus/Endpoints/User.swift @@ -2,7 +2,7 @@ import Vapor import Fluent /// A class that contains Create, Read, Update and Delete functionality for a -/// generic type `T` conforming to `CorvusModel` grouped under a given path. +/// generic type `T` representing a user object. public final class User: Endpoint { /// The route path to the parameters. @@ -24,7 +24,7 @@ public final class User: Endpoint { self.useSoftDelete = softDelete } - /// The `content` of the `CRUD`, containing Create, Read, Update and Delete + /// The `content` of the `User`, containing Create, Read, Update and Delete /// functionality grouped under one. public var content: Endpoint { if useSoftDelete { @@ -48,7 +48,7 @@ public final class User: Endpoint { } } - /// The `content` of the `CRUD`, containing Create, Read, Update, Delete and + /// The `content` of the `User`, containing Create, Read, Update, Delete and /// SoftDelete functionality grouped under one. public var contentWithSoftDelete: Endpoint { Group(pathComponents) { diff --git a/Sources/Corvus/Endpoints/Utilities/EmptyEndpoint.swift b/Sources/Corvus/Endpoints/Utilities/EmptyEndpoint.swift index 19410d0..397c75c 100644 --- a/Sources/Corvus/Endpoints/Utilities/EmptyEndpoint.swift +++ b/Sources/Corvus/Endpoints/Utilities/EmptyEndpoint.swift @@ -2,6 +2,10 @@ import Vapor /// An empty default value when no `Endpoint` value is needed. public struct EmptyEndpoint: Endpoint { - /// An empty default implementation of `.register()` to avoid endless loops when registering `EmptyEndpoint`s + /// An empty default implementation of `.register()` to avoid endless loops + /// when registering `EmptyEndpoint`s. + /// + /// - Parameter routes: The `RoutesBuilder` that contains HTTP route + /// information up to this point. public func register(to routes: RoutesBuilder) { } } diff --git a/Sources/Corvus/Protocols/Endpoints/Endpoint.swift b/Sources/Corvus/Protocols/Endpoints/Endpoint.swift index 9519ff6..c50d7ba 100644 --- a/Sources/Corvus/Protocols/Endpoints/Endpoint.swift +++ b/Sources/Corvus/Protocols/Endpoints/Endpoint.swift @@ -11,6 +11,9 @@ public protocol Endpoint { /// A method needed to implement registration of an endpoint to the /// `Router` provided by Vapor, this handles the logic of making certain /// operations accessible on certain route paths. + /// + /// - Parameter routes: The `RoutesBuilder` containing HTTP route + /// information up to this point. func register(to routes: RoutesBuilder) } @@ -27,7 +30,11 @@ extension Endpoint { /// do not need to be registered. extension Endpoint { - /// A default implementation of `.register()` for components that do not need special behaviour. + /// A default implementation of `.register()` for components that do not + /// need special behaviour. + /// + /// - Parameter routes: The `RoutesBuilder` containing HTTP route + /// information up to this point. public func register(to routes: RoutesBuilder) { content.register(to: routes) } @@ -39,6 +46,9 @@ extension Array: Endpoint where Element == Endpoint { /// An `Array` of `Endpoint` is registered by registering all of the /// `Array`'s elements. + /// + /// - Parameter routes: The `RoutesBuilder` containing HTTP route + /// information up to this point. public func register(to routes: RoutesBuilder) { forEach({ $0.register(to: routes) }) } diff --git a/Sources/Corvus/Protocols/Endpoints/QueryEndpoint.swift b/Sources/Corvus/Protocols/Endpoints/QueryEndpoint.swift index bc7a286..56a1b65 100644 --- a/Sources/Corvus/Protocols/Endpoints/QueryEndpoint.swift +++ b/Sources/Corvus/Protocols/Endpoints/QueryEndpoint.swift @@ -10,6 +10,8 @@ public protocol QueryEndpoint: RestEndpoint { associatedtype QuerySubject: CorvusModel /// A method to run database queries on a component's `QuerySubject`. + /// + /// - Parameter req: The incoming `Request`. func query(_ req: Request) throws -> QueryBuilder } @@ -19,6 +21,10 @@ extension QueryEndpoint { /// A default implementation of `.query()` for components that do not /// require customized database queries. + /// + /// - Parameter req: The incoming `Request`. + /// - Throws: An error if something goes wrong. + /// - Returns: A `QueryBuilder` object for further querying. public func query(_ req: Request) throws -> QueryBuilder { QuerySubject.query(on: req.db) } diff --git a/Sources/Corvus/Protocols/Endpoints/RestEndpoint.swift b/Sources/Corvus/Protocols/Endpoints/RestEndpoint.swift index 2c1855b..7c96a86 100644 --- a/Sources/Corvus/Protocols/Endpoints/RestEndpoint.swift +++ b/Sources/Corvus/Protocols/Endpoints/RestEndpoint.swift @@ -15,6 +15,8 @@ public protocol RestEndpoint: Endpoint { /// A method that runs logic on the results of the `.query()` and returns /// those results asynchronously in an `EventLoopFuture`. + /// + /// - Parameter req: The incoming `Request`. func handler(_ req: Request) throws -> EventLoopFuture } @@ -26,6 +28,7 @@ public extension RestEndpoint { /// Registers the component to the `Vapor` router depending on its /// `operationType`. + /// /// - Parameter routes: The `RoutesBuilder` to extend. func register(to routes: RoutesBuilder) { switch operationType { diff --git a/Sources/Corvus/Protocols/RestApi.swift b/Sources/Corvus/Protocols/RestApi.swift index e2a22b0..9536f00 100644 --- a/Sources/Corvus/Protocols/RestApi.swift +++ b/Sources/Corvus/Protocols/RestApi.swift @@ -12,6 +12,9 @@ extension RestApi { /// A default implementation for `boot` that recurses down the API's /// hierarchy. + /// + /// - Parameter routes: The `RoutesBuilder` containing HTTP route + /// information up to this point. public func boot(routes: RoutesBuilder) throws { content.register(to: routes) } diff --git a/Tests/CorvusTests/ApplicationTests.swift b/Tests/CorvusTests/ApplicationTests.swift index 85fa734..28f99a5 100644 --- a/Tests/CorvusTests/ApplicationTests.swift +++ b/Tests/CorvusTests/ApplicationTests.swift @@ -294,7 +294,10 @@ final class ApplicationTests: XCTestCase { var content: Endpoint { Group("api", "accounts") { - Custom(path: "userId", type: .post) { req in + Custom( + pathComponents: "userId", + type: .post + ) { req in let requestContent = try req.content.decode( Account.self ) From 2f4b02baaedd726c2f502949f8430571a1274cfd Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Wed, 8 Apr 2020 09:39:17 +0200 Subject: [PATCH 14/16] Updated swiftlint to watch line length for everything --- .swiftlint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index a66c800..311df3f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -359,8 +359,8 @@ large_tuple: # Tuples shouldn't have too many members. Create a custom type inst line_length: # Lines should not span too many characters. warning: 80 # default: 120 error: 80 # default: 200 - ignores_comments: true # default: false - ignores_urls: true # default: false + ignores_comments: false # default: false + ignores_urls: false # default: false ignores_function_declarations: false # default: false ignores_interpolated_strings: false # default: false From a5ff1b5e6db7f6c81c351c666f5b2ca3beac3a1d Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Wed, 8 Apr 2020 09:54:00 +0200 Subject: [PATCH 15/16] Code style improvements for Response Modifier code --- Sources/Corvus/Authentication/CorvusUser.swift | 14 +++++++------- .../Endpoints/Modifiers/ResponseModifier.swift | 15 +++++++++------ .../Corvus/Protocols/http/CorvusResponse.swift | 2 ++ Tests/CorvusTests/ApplicationTests.swift | 4 +++- Tests/CorvusTests/AuthenticationTests.swift | 18 +++++++++--------- Tests/CorvusTests/Models/CustomUser.swift | 12 ++++++------ .../Utilities/CorvusUser+Equatable.swift | 2 +- 7 files changed, 37 insertions(+), 30 deletions(-) diff --git a/Sources/Corvus/Authentication/CorvusUser.swift b/Sources/Corvus/Authentication/CorvusUser.swift index fbaf7c3..1f84192 100644 --- a/Sources/Corvus/Authentication/CorvusUser.swift +++ b/Sources/Corvus/Authentication/CorvusUser.swift @@ -12,8 +12,8 @@ public final class CorvusUser: CorvusModel { public var id: UUID? /// The name of the user. - @Field(key: "name") - public var name: String + @Field(key: "username") + public var username: String /// The hashed password of the user, used during authentication. @Field(key: "password_hash") @@ -30,15 +30,15 @@ public final class CorvusUser: CorvusModel { /// /// - Parameters: /// - id: The identifier of the user, auto generated if not provided. - /// - name: The name of the user. + /// - username: The username of the user. /// - passwordHash: The hashed password of the user. public init( id: UUID? = nil, - name: String, + username: String, passwordHash: String ) { self.id = id - self.name = name + self.username = username self.passwordHash = passwordHash } } @@ -53,7 +53,7 @@ public struct CreateCorvusUser: Migration { public func prepare(on database: Database) -> EventLoopFuture { database.schema(CorvusUser.schema) .id() - .field("name", .string, .required) + .field("username", .string, .required) .field("password_hash", .string, .required) .field("deleted_at", .date) .create() @@ -70,7 +70,7 @@ public struct CreateCorvusUser: Migration { extension CorvusUser: CorvusModelUser { /// Provides a path to the user's username. - public static let usernameKey = \CorvusUser.$name + public static let usernameKey = \CorvusUser.$username /// Provides a path to the user's hashed password. public static let passwordHashKey = \CorvusUser.$passwordHash diff --git a/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift b/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift index d066678..746e3e0 100644 --- a/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift +++ b/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift @@ -1,9 +1,9 @@ import Vapor import Fluent -/// A class that wraps a component which utilizes a `.respond(with:)` modifier. That -/// allows Corvus to chain modifiers, as it gets treated as any other struct -/// conforming to `RestEndpoint`. +/// A class that wraps a component which utilizes a `.respond(with:)` modifier. +/// That allows Corvus to chain modifiers, as it gets treated as any other +/// struct conforming to `RestEndpoint`. public final class ResponseModifier< Q: RestEndpoint, R: CorvusResponse>: @@ -34,8 +34,10 @@ RestEndpoint where Q.Element == R.Item { /// - Parameter req: An incoming `Request`. /// - Returns: An `EventLoopFuture` containing the /// `ResponseModifier`'s `Response`. - public func handler(_ req: Request) - throws -> EventLoopFuture { + /// - Throws: An `Abort` error if something goes wrong. + public func handler(_ req: Request) throws -> + EventLoopFuture + { try restEndpoint.handler(req).map(Response.init) } @@ -48,7 +50,8 @@ extension RestEndpoint { /// `CorvusResponse`. /// /// - Parameter as: A type conforming to `CorvusResponse`. - /// - Returns: An instance of a `ResponseModifier` with the supplied `CorvusResponse`. + /// - Returns: An instance of a `ResponseModifier` with the supplied + /// `CorvusResponse`. public func respond( with: R.Type ) -> ResponseModifier { diff --git a/Sources/Corvus/Protocols/http/CorvusResponse.swift b/Sources/Corvus/Protocols/http/CorvusResponse.swift index c9c7c46..d7fab75 100644 --- a/Sources/Corvus/Protocols/http/CorvusResponse.swift +++ b/Sources/Corvus/Protocols/http/CorvusResponse.swift @@ -10,5 +10,7 @@ public protocol CorvusResponse: Content { /// Initialises a `CorvusResponse` with a given item. /// Normally this is the result of the `QueryEndpoints`'s handler function. + /// + /// - Parameter item: The item to initialize the response with. init(item: Item) } diff --git a/Tests/CorvusTests/ApplicationTests.swift b/Tests/CorvusTests/ApplicationTests.swift index 6d63feb..c41bd5e 100644 --- a/Tests/CorvusTests/ApplicationTests.swift +++ b/Tests/CorvusTests/ApplicationTests.swift @@ -694,7 +694,9 @@ final class ApplicationTests: XCTestCase { // This response is a more complex example using generics. // This allows for responses which work with any number of models. - struct ReadResponse: CorvusResponse, Equatable { + struct ReadResponse: + CorvusResponse, + Equatable { let success = true let payload: [Model] diff --git a/Tests/CorvusTests/AuthenticationTests.swift b/Tests/CorvusTests/AuthenticationTests.swift index c09f454..265c663 100644 --- a/Tests/CorvusTests/AuthenticationTests.swift +++ b/Tests/CorvusTests/AuthenticationTests.swift @@ -39,7 +39,7 @@ final class AuthenticationTests: XCTestCase { .base64EncodedString() let user = CorvusUser( - name: "berzan", + username: "berzan", passwordHash: try Bcrypt.hash("pass") ) @@ -94,7 +94,7 @@ final class AuthenticationTests: XCTestCase { try app.register(collection: basicAuthenticatorTest) let user = CorvusUser( - name: "berzan", + username: "berzan", passwordHash: try Bcrypt.hash("pass") ) let account = Account(name: "berzan") @@ -152,7 +152,7 @@ final class AuthenticationTests: XCTestCase { try app.register(collection: bearerAuthenticatorTest) let user = CorvusUser( - name: "berzan", + username: "berzan", passwordHash: try Bcrypt.hash("pass") ) let account = Account(name: "berzan") @@ -273,12 +273,12 @@ final class AuthenticationTests: XCTestCase { try app.register(collection: authModifierTest) let user1 = CorvusUser( - name: "berzan", + username: "berzan", passwordHash: try Bcrypt.hash("pass") ) let user2 = CorvusUser( - name: "paul", + username: "paul", passwordHash: try Bcrypt.hash("pass") ) @@ -403,14 +403,14 @@ final class AuthenticationTests: XCTestCase { try app.register(collection: authModifierTest) let user1 = CustomUser( - name: "berzan", + username: "berzan", surname: "yildiz", email: "berzan@corvus.com", passwordHash: try Bcrypt.hash("pass") ) let user2 = CustomUser( - name: "paul", + username: "paul", surname: "schmiedmayer", email: "paul@corvus.com", passwordHash: try Bcrypt.hash("pass") @@ -527,12 +527,12 @@ final class AuthenticationTests: XCTestCase { try app.register(collection: userAuthModifierTest) let user1 = CorvusUser( - name: "berzan", + username: "berzan", passwordHash: try Bcrypt.hash("pass") ) let user2 = CorvusUser( - name: "paul", + username: "paul", passwordHash: try Bcrypt.hash("pass") ) diff --git a/Tests/CorvusTests/Models/CustomUser.swift b/Tests/CorvusTests/Models/CustomUser.swift index 18e6f8c..3b2ad87 100644 --- a/Tests/CorvusTests/Models/CustomUser.swift +++ b/Tests/CorvusTests/Models/CustomUser.swift @@ -10,8 +10,8 @@ public final class CustomUser: CorvusModel { @ID public var id: UUID? - @Field(key: "name") - public var name: String + @Field(key: "username") + public var username: String @Field(key: "surname") public var surname: String @@ -29,13 +29,13 @@ public final class CustomUser: CorvusModel { public init( id: UUID? = nil, - name: String, + username: String, surname: String, email: String, passwordHash: String ) { self.id = id - self.name = name + self.username = username self.surname = surname self.email = email self.passwordHash = passwordHash @@ -49,7 +49,7 @@ public struct CreateCustomUser: Migration { public func prepare(on database: Database) -> EventLoopFuture { database.schema(CustomUser.schema) .id() - .field("name", .string, .required) + .field("username", .string, .required) .field("surname", .string, .required) .field("email", .string, .required) .field("password_hash", .string, .required) @@ -64,7 +64,7 @@ public struct CreateCustomUser: Migration { extension CustomUser: CorvusModelUser { - public static let usernameKey = \CustomUser.$name + public static let usernameKey = \CustomUser.$username public static let passwordHashKey = \CustomUser.$passwordHash diff --git a/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift b/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift index 4df945c..27602a6 100644 --- a/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift +++ b/Tests/CorvusTests/Utilities/CorvusUser+Equatable.swift @@ -3,7 +3,7 @@ import Corvus extension CorvusUser: Equatable { public static func == (lhs: CorvusUser, rhs: CorvusUser) -> Bool { - var result = lhs.name == rhs.name + var result = lhs.username == rhs.username if let lhsId = lhs.id, let rhsId = rhs.id { result = result && lhsId == rhsId From dd8203a28952f0172006ad0fcc549c5f1ccb0981 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Wed, 8 Apr 2020 09:56:35 +0200 Subject: [PATCH 16/16] Fixed swiftlint errors --- Sources/Corvus/Endpoints/Restore/Restore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Corvus/Endpoints/Restore/Restore.swift b/Sources/Corvus/Endpoints/Restore/Restore.swift index 612aa5e..0725e0b 100644 --- a/Sources/Corvus/Endpoints/Restore/Restore.swift +++ b/Sources/Corvus/Endpoints/Restore/Restore.swift @@ -73,7 +73,8 @@ public final class Restore: AuthEndpoint { .map { .ok } } - /// A method that registers the `.handler()` to the supplied `RoutesBuilder`. + /// A method that registers the `.handler()` to the supplied + /// `RoutesBuilder`. /// /// - Parameter routes: A `RoutesBuilder` containing all the information /// about the HTTP route leading to the current component.