Slide 1

Slide 1 text

D C 21 /03/2018 - TRUG Michał Poczwardowski [email protected] dmp @ 3cityIT slack

Slide 2

Slide 2 text

Agenda ● Intro ● Stack ● Code Samples ● Patterns ● 3rd-parties ● Security Audit

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Questions ● Client? Huge Business (from scratch - after PDS) / corpo ● Problem Domain? Recruitment / Employer Branding ● Team? Netguru + outside agency (responsible for UI and visuals) ● Stack? Grape API + ActiveAdmin for Backend / Frontend@Ember

Slide 5

Slide 5 text

From scratch (1st commit) rails new --database=postgresql --skip-test --skip-coffee --skip-action-cable --skip-yarn

Slide 6

Slide 6 text

README.md

Slide 7

Slide 7 text

README.md - minimum! ● Project stack ● Project setup ● Detailed info about used patterns and solutions ● Known Issues ● How to get into staging? ● How to get into production? ● Deployments explained ● Default seeds

Slide 8

Slide 8 text

Codebeat

Slide 9

Slide 9 text

Stack / Gemfile (tip: keep them organized) #1 # rails gem "rails", "~> 5.1.3" # general gems gem "aasm" gem "activeadmin" gem "acts-as-taggable-on" gem "acts_as_list" gem "dry-monads" gem "jbuilder", "~> 2.5" gem "pg", "~> 0.18" gem "puma", "~> 3.7" gem "rails-i18n", "~> 5.0.0" gem "redis-namespace" gem "rollbar" gem "sidekiq"

Slide 10

Slide 10 text

Stack / Gemfile #2 # authentication & authorization gem "devise" gem "devise_security_extension", git: "https://github.com/phatworx/devise_security_extension.git" # => rubygems.org devise_security_extension 0.10.0 version does not support rails 5.1 # => raising undefined method "before_filter" error gem "doorkeeper" gem "pundit" gem "rack-attack" gem "rack-cors"

Slide 11

Slide 11 text

Stack / Gemfile #3 # grape + jsonapi gem "grape" gem "grape-jsonapi-resources" gem "grape-middleware-logger" gem "grape-swagger" gem "grape-swagger-rails" gem "hashie-forbidden_attributes" # to make grape params validation work

Slide 12

Slide 12 text

Stack / Gemfile #4 # 3rd parties - APIs and processing gem "cloudinary" gem "griddler" gem "griddler-sendgrid" gem "httpi" # picked as a connection wizard because already needed by savon gem "nokogiri" gem "recaptcha", require: "recaptcha/rails" gem "savon", "~> 2"

Slide 13

Slide 13 text

API - (almost) JSONAPI compliant (+ swagger)

Slide 14

Slide 14 text

Endpoints Base class API::V1::Users::Base < Core namespace :users do mount ChangePassword mount Me mount Register route_param :id, type: Integer do before { @user = User.find(params[:id]) } mount Delete mount Show mount Update end end # app/controllers/api/v1/users/base.rb

Slide 15

Slide 15 text

Example Endpoint - just authorize and service call class API::V1::Users::ChangePassword < Base desc "Change user password using current one" helpers Params helpers API::V1::Helpers::JSONAPIParams before { doorkeeper_authorize! } params do use :jsonapi_data_attributes, required: [{ name: "current-password", type: String, desc: "Current user password" }], use: [{ predefined: :_password_confirmable }] end patch "change-password" do authorize current_user, :change_password? assure_rightness ::UserServices::ChangePasswordUsingCurrent.call(current_user, jsonapi_attributes(params)) end end # app/controllers/api/v1/users/change_password.rb

Slide 16

Slide 16 text

ApplicationService class UserServices::Register < ApplicationServiceInTransaction def call(attributes) Right(attributes) .bind(method(:ensure_agreements)) .bind { |attrs| ::GeneralServices::InjectModelIdsFromListInAttributes.call(attrs, :city) } .bind(method(:create_user)) end private def ensure_agreements(attributes) return Right(attributes) if required_agreements?(attributes) Left(I18n.t("user.registration.no_required_agreements")) end (...) end end # app/services/user_services/register.rb

Slide 17

Slide 17 text

Models without logic and before/after actions class Message < ApplicationRecord belongs_to :author, class_name: "User", optional: true belongs_to :conversation has_many :activity_records, as: :trackable validates :content, presence: true end

Slide 18

Slide 18 text

dry-monads http://dry-rb.org/gems/dry-monads/ Result The Result monad is useful to express a series of computations that might return an error object with additional information. The Result mixin has two type constructors: Success and Failure. The Success can be thought of as “everything went success” and the Failure is used when “something has gone wrong”. // http://dry-rb.org/gems/dry-monads/result/

Slide 19

Slide 19 text

Why handle services using Either/Success monad ● .succcess? and .failure? work the same for every service, ● chainability, ● universal error handling - the same interface for all services ● ApplicationServiceInTransaction, ● extract logic into shared pieces

Slide 20

Slide 20 text

Integrations aka 3rd parties ● KENEXA - WSDL/SOAP requests - recruitment cloud software, poorly documented, a lot of pain ● Sendgrid Inbound Parse - handling incoming emails ● Cloudinary - assets images/videos

Slide 21

Slide 21 text

What could be done better? ● JSONAPI - jsonapi-resources creates links that are not correct

Slide 22

Slide 22 text

Security Audit

Slide 23

Slide 23 text

https://securityheaders.io

Slide 24

Slide 24 text

Secure headers https://github.com/twitter/secureheaders SecureHeaders::Configuration.default do |config| # CSP stands for Content Security Policy config.csp = { base_uri: %w('self'), block_all_mixed_content: true, child_src: %w('self'), connect_src: [], default_src: %w('self'), font_src: %w('self' data:), form_action: %w('self'), frame_ancestors: [], img_src: %w('self'), manifest_src: %w('self'), media_src: [], object_src: %w('self'), plugin_types: [], report_uri: [], sandbox: false, script_src: %w('self'), style_src: %w('self' 'unsafe-inline'), upgrade_insecure_requests: false, worker_src: %w('self'), } config.referrer_policy = %w(no-referrer-when-downgrade) end # config/initializers/secure_headers.rb

Slide 25

Slide 25 text

Questions / Thanks!