Delivering backend - Case study

Delivering backend - Case study

It's a short description of the project I've done recently. Focused on presenting stack, best practices, and patterns used. This our typical approach to Ruby backend at Netguru.

4cf300db4be834dfb4ddd76337205946?s=128

Michał Poczwardowski

March 21, 2018
Tweet

Transcript

  1. D C 21 /03/2018 - TRUG Michał Poczwardowski michal.poczwardowski@netguru.co dmp

    @ 3cityIT slack
  2. Agenda • Intro • Stack • Code Samples • Patterns

    • 3rd-parties • Security Audit
  3. None
  4. 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
  5. From scratch (1st commit) rails new --database=postgresql --skip-test --skip-coffee --skip-action-cable

    --skip-yarn
  6. README.md

  7. 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
  8. Codebeat

  9. 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"
  10. 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"
  11. 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
  12. 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"
  13. API - (almost) JSONAPI compliant (+ swagger)

  14. 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
  15. 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
  16. 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
  17. 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
  18. 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/
  19. 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
  20. 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
  21. What could be done better? • JSONAPI - jsonapi-resources creates

    links that are not correct
  22. Security Audit

  23. https://securityheaders.io

  24. 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
  25. Questions / Thanks!