Swiftpack.co - Package - SoftwareOps/Sentry

Sentry

An easy way to setup an authorization application interface for Vapor inspired by Devise. This module is implemented as a JSON API without views.

Sentry is composed of 6 modules:

Database Authenticatable: Using AuthProvider

Confirmable: sends emails with confirmation instructions and verifies whether an account is already confirmed during sign in.

Recoverable: resets the user password and sends reset instructions.

Registerable: handles signing up users through a registration process

Trackable: tracks sign in count, timestamps and IP address.

Invitable: implemented models can send emails to other models to start registration process

Getting Started

A list of sections below will help you get started. This project is reliant on setting up your own email (we use Sengrid), creating the parent authenticable model (eg. User with email and password columns), and using Fluent to respresent models from the database.

Swift Package Manager

Add dependency to your Package manager in Package.swift

.Package(url: "https://github.com/SoftwareOps/Sentry.git", majorVersion: 0)

Sourcery

Sentry is built using stencils templates to generate code based on implemented protocols.

Installing Sourcery can be quite tricky so I opted for the easier option right now by installing the binary from Sourcery's github releases. Once you download the binary you will copy the contents of the bin directly to /usr/local/bin. Once you have it installed try using sourcery --help to verify it is installed.

Setup Authorization

We followed this implementation provided by Vapor 2.0 Docs for token based implementation. Our package will generate your token model and relational records on a model extension (eg. tokens and token instance variables). However you will need to setup a password authenticable model.

Append Model fields

This is your model instance variables you will need to support this package.

// Confirmable properties
var confirmedAt: Date?
var confirmationSentAt: Date?
var confirmationToken: String?

// Reconfirmable properties
var unconfirmedEmail: String?

// Trackable properties
var signInCount: Int
var currentSignInAt: Date?
var lastSignInAt: Date?
var currentSignInIp: String?
var lastSignInIp: String?

// Recoverable properties
var resetPasswordSentAt: Date?
var resetPasswordToken: String?

// Invitable properties
var invitationToken: String?
var invitationSentAt: Date?
var invitationAcceptedAt: Date?
var invitedById: Int?
var invitedByType: String?

and following row properties to retrieve from database

// Confirmable fields
try row.set(SentryField.confirmedAt, confirmedAt)
try row.set(SentryField.confirmationSentAt, confirmationSentAt)
try row.set(SentryField.confirmationToken, confirmationToken)

// Reconfirmable fields
try row.set(SentryField.unconfirmedEmail, unconfirmedEmail)

// Trackable fields
try row.set(SentryField.signInCount, signInCount)
try row.set(SentryField.currentSignInAt, currentSignInAt)
try row.set(SentryField.lastSignInAt, lastSignInAt)
try row.set(SentryField.currentSignInIp, currentSignInIp)
try row.set(SentryField.lastSignInIp, lastSignInIp)

// Recoverable fields
try row.set(SentryField.resetPasswordSentAt, resetPasswordSentAt)
try row.set(SentryField.resetPasswordToken, resetPasswordToken)

// Invitable fields
try row.set(SentryField.invitationToken, invitationToken)
try row.set(SentryField.invitationSentAt, invitationSentAt)
try row.set(SentryField.invitationAcceptedAt, invitationAcceptedAt)
try row.set(SentryField.invitedById, invitedById)
try row.set(SentryField.invitedByType, invitedByType)
...

Implement Protocols

This example will be assuming your Model is User but the name can be anything you choose.

extension User: Sessionable { }

extension User: Registerable { }

extension User: Confirmable {
    var confirmationEmail: String? {
        if (self.unconfirmedEmail != nil) {
            return self.unconfirmedEmail
        }else{
            return self.email
        }
    }
}

extension User: Reconfirmable { }

extension User: Trackable { }

extension User: Recoverable {
    var resetPasswordEmail: String? {
        return self.email
    }
}

extension User: Invitable {
    var invitationEmail: String? {
        return self.email
    }
}

Implement Error Handling

Currently this class is required as the controller implements this static class function to catch errors to throw to Vapor in JSON format (open to other solutions for this).

import Foundation
import HTTP

class ModelError {
    
    public static func catchSave(error: Error) throws {
        switch(error) {
            default:
                throw Abort(.badRequest, reason: "Unsuccessful save")
        }
    }
    
}

Generate

Now that you have sourcery installed you can use the follow line to generate controllers, models, and migrations. You will have to find the build folder of your swift package by looking in .build of your root directory for Sentry after installing the swift package.

Run the following commands in the root of your project and replace build path in the commands with your own.

Models

sourcery --sources ./Sources/ --templates <build-path>/Sources/Sentry/Stencils/Models --output Sources/App/Models

Migrations

sourcery --sources ./Sources/ --templates <build-path>/Sources/Sentry/Stencils/Migrations --output Sources/App/Migrations

Controllers

sourcery --sources ./Sources/ --templates <build-path>/Sources/Sentry/Stencils/Controllers --output Sources/App/Controllers

Extensions

sourcery --sources ./Sources/ --templates <build-path>/Sources/Sentry/Stencils/Extensions --output Sources/App/Extensions

Append Routes

After generating your controllers for each model record you must add your routes to your Vapor application like so:

try builder.collection(UserRegistrationsController.self)
try builder.collection(UserConfirmationsController.self)
try builder.collection(UserSessionsController.self)
try builder.collection(UserPasswordsController.self)
try builder.collection(UserInvitationsController.self)

Append Migrations

After generating your migrations for each model you must add the package migrations to your preparations list like so:

private func addPreparations() {
   ...
    preparations.append(AddConfirmationFieldsToUsers.self)
    preparations.append(AddTrackableToUsers.self)
    preparations.append(AddRecoverableToUsers.self)
    preparations.append(AddReconfirmableToUsers.self)
    preparations.append(AddInvitableToUsers.self)
    preparations.append(UserToken.self)
}

Configuration

Add the following to your Config/app.json or respective environment.

{
	"ASSET_URL": "http://localhost:8080", // Used to build links and image urls in emails
  ...
}

Github

link
Stars: 6
Help us keep the lights on

Used By

Total: 0