Skip to content
This repository has been archived by the owner on Sep 7, 2021. It is now read-only.

Feature/user endpoint #9

Merged
merged 17 commits into from
Apr 9, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Sources/Corvus/Authentication/CorvusModelUser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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<Self> {
CorvusModelUserAuthenticator<Self>(database: database)
}

var _$name: Field<String> {
guard let mirror = Mirror(reflecting: self).descendant("_name"),
let username = mirror as? Field<String> else {
fatalError("name property must be declared using @Field")
}

return username
}

var _$passwordHash: Field<String> {
guard let mirror = Mirror(reflecting: self).descendant("_passwordHash"),
let passwordHash = mirror as? Field<String> else {
fatalError("passwordHash property must be declared using @Field")
}

return passwordHash
}
}

public struct CorvusModelUserAuthenticator<User>: BasicAuthenticator
where User: CorvusModelUser
{
public let database: DatabaseID?

public func authenticate(
basic: BasicAuthorization,
for request: Request
) -> EventLoopFuture<User?> {
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
}
}
}
73 changes: 73 additions & 0 deletions Sources/Corvus/Authentication/CorvusModelUserToken.swift
Original file line number Diff line number Diff line change
@@ -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<Self> {
CorvusModelUserTokenAuthenticator<Self>(database: database)
}

var _$value: Field<String> {
guard let mirror = Mirror(reflecting: self).descendant("_value"),
let token = mirror as? Field<String> else {
fatalError("value property must be declared using @Field")
}

return token
}

var _$user: Parent<User> {
guard let mirror = Mirror(reflecting: self).descendant("_user"),
let user = mirror as? Parent<User> else {
fatalError("user property must be declared using @Parent")
}

return user
}
}

public struct CorvusModelUserTokenAuthenticator<T: CorvusModelUserToken>: BearerAuthenticator
{
public typealias User = T.User
public let database: DatabaseID?

public func authenticate(
bearer: BearerAuthorization,
for request: Request
) -> EventLoopFuture<User?> {
let db = request.db(self.database)
return T.query(on: db)
.filter(\._$value == bearer.token)
.first()
.flatMap
{ token -> EventLoopFuture<User?> 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 }
}
}
}
8 changes: 1 addition & 7 deletions Sources/Corvus/Authentication/CorvusToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
43 changes: 7 additions & 36 deletions Sources/Corvus/Authentication/CorvusUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@ public final class CorvusUser: CorvusModel {
@Field(key: "name")
public var name: String
bmikaili marked this conversation as resolved.
Show resolved Hide resolved

/// 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)
Expand All @@ -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
}
}

Expand All @@ -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()
}
Expand All @@ -76,21 +68,15 @@ 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 {

/// 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
extension CorvusUser: CorvusModelUser {

/// 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)
}
}

Expand All @@ -107,18 +93,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
}
}
2 changes: 1 addition & 1 deletion Sources/Corvus/Endpoints/Groups/BasicAuthGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: ModelUser>: Endpoint {
public struct BasicAuthGroup<T: CorvusModelUser>: Endpoint {

/// An array of `PathComponent` describing the path that the
/// `BasicAuthGroup` extends.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Corvus/Endpoints/Groups/BearerAuthGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: ModelUserToken>: Endpoint {
public struct BearerAuthGroup<T: CorvusModelUserToken>: Endpoint {

/// An array of `PathComponent` describing the path that the
/// `BearerAuthGroup` extends.
Expand Down
13 changes: 8 additions & 5 deletions Sources/Corvus/Endpoints/Login.swift
Original file line number Diff line number Diff line change
@@ -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<T: CorvusModelUserToken & ResponseEncodable>: Endpoint
where T.User: CorvusModelUser {

/// The route for the login functionality
let path: PathComponent
Expand All @@ -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<CorvusToken> {
let user = try req.auth.require(CorvusUser.self)
let token = try user.generateToken()
public func handler(_ req: Request) throws -> EventLoopFuture<T> {
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 }
}

Expand All @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions Sources/Corvus/Endpoints/Modifiers/AuthModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Q: AuthEndpoint>: AuthEndpoint {
public final class AuthModifier<Q: AuthEndpoint, T: CorvusModelUser>: AuthEndpoint {

/// The return type for the `.handler()` modifier.
public typealias Element = Q.Element
Expand All @@ -17,7 +17,7 @@ public final class AuthModifier<Q: AuthEndpoint>: AuthEndpoint {
/// authenticated.
public typealias UserKeyPath = KeyPath<
Q.QuerySubject,
Q.QuerySubject.Parent<CorvusUser>
Q.QuerySubject.Parent<T>
>

/// The `ReadEndpoint` the `.auth()` modifier is attached to.
Expand Down Expand Up @@ -74,11 +74,11 @@ public final class AuthModifier<Q: AuthEndpoint>: 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
Expand All @@ -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<Self>.UserKeyPath
) -> AuthModifier<Self> {
public func auth<T: CorvusModelUser>(
_ user: AuthModifier<Self, T>.UserKeyPath
) -> AuthModifier<Self, T> {
AuthModifier(self, user: user)
}
}
Loading