顯示具有 ROR 標籤的文章。 顯示所有文章
顯示具有 ROR 標籤的文章。 顯示所有文章

2018年12月8日 星期六

在 Rails 使用 custom font


參考 https://coderwall.com/p/v5c8kq/web-fonts-and-rails-asset-pipeline 把檔案放到 `app/assets/fonts` 裡面

然後在 `application.scss` 裡加上

```scss

@font-face {
  font-family: 'Calibre';
  src: font-url('calibre/regular/Calibre-Regular.otf')  format('opentype'),
       font-url('calibre/regular/Calibre-Regular.woff') format('woff'),
       font-url('calibre/regular/Calibre-Regular.ttf')  format('truetype'),
       font-url('calibre/regular/Calibre-Regular.svg#Calibre-Regular') format('svg');
  font-weight: normal;
  font-style: normal;
}

@font-face {
  font-family: 'Calibre';
  src: font-url('calibre/medium/Calibre-Semibold.woff2') format('woff');\
  font-weight: bold;
  font-style: bold;
}

b, strong {
  font-weight:bold
}

h1, h2, h3, h4, h5, h6 {
  font-weight:bold
}

```

這樣就行

(font-face 可以放在別的 file 再 import 也可以)




https://coderwall.com/p/v5c8kq/web-fonts-and-rails-asset-pipeline
https://github.com/nicotaing/chloepark/tree/master/themes/park/source/fonts/calibre/regular

2018年12月2日 星期日

Ansible + rails + letsencrypt


First, install nginx

roles/nginx/tasks/main.yml

```yml
- name: Install nginx
  apt:
    name: nginx
    state: latest
- name: Disable default nginx config
  file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify:
    - Restart nginx
```

roles/nginx/handlers/main.yml


```yml
- name: Restart nginx
  service: name=nginx state=restarted
```

Then, install LetsEncrypt

roles/letsencrypt/tasks/main.yml

```yml
- name: Generate dhparams file
  shell: openssl dhparam -out {{ dhparam_pem_path }} 2048
  args:
    creates: "{{ dhparam_pem_path }}"
- name: Update apt cache
  apt: update_cache=yes cache_valid_time=86400
- name: Install depends
  apt: name={{ item }} state=latest
  with_items:
    - python
    - python-dev
    - gcc
    - dialog
    - libaugeas0
    - augeas-lenses
    - libssl-dev
    - libffi-dev
    - ca-certificates
    - python-pip
    - python-virtualenv
    - git
    - libpython-dev
    - zlib1g-dev
- name: Lets Encrypt client
  git: dest=/opt/certbot clone=yes repo=https://github.com/certbot/certbot force=yes
# Auto-renew certificates and reload nginx
- name: Add crontab to renew certificates
  cron: minute="30" hour="2" weekday="1" job="/opt/certbot/certbot-auto renew >> /var/log/le-renew.log"
- name: Add crontab to reload server
  cron: minute="35" hour="2" weekday="1" job="/etc/init.d/nginx reload"
```


Last, configure service

roles/service/tasks/main.yml

```yml
- name: Add http nginx configuration
  template:
    src: templates/http.conf.j2
    dest: /etc/nginx/sites-available/{{ service_name }}.http.conf
  notify:
    - Restart nginx
- name: Enable nginx config
  file:
    src: /etc/nginx/sites-available/{{ service_name }}.http.conf
    dest: /etc/nginx/sites-enabled/{{ service_name }}.http.conf
    state: link
  notify:
    - Restart nginx
- name: Create letsencrypt certificate
  shell: ./certbot-auto certonly --webroot --email {{ service_admin_email }} --agree-tos --webroot-path=/usr/share/nginx/html -d {{ service_host }};
  args:
    chdir: /opt/certbot
- name: Add https nginx configuration
  template:
    src: templates/https.conf.j2
    dest: /etc/nginx/sites-available/{{ service_name }}.https.conf
  notify:
    - Restart nginx
- name: Add external https nginx symlink
  file:
    src: /etc/nginx/sites-available/{{ service_name }}.https.conf
    dest: /etc/nginx/sites-enabled/{{ service_name }}.https
    state: link
  notify:
    - Restart nginx
```

with nginx http template, this will redirect all http request into https


roles/service/templates/http.conf.j2

```
server {
  listen 80;
  server_name {{ service_host }};

  root /usr/share/nginx/html;

  index index.html index.htm index.nginx-debian.html;

  location ~ /.well-known {
    allow all;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}
```

also add a https template for rails


roles/service/templates/https.conf.j2

```
upstream app {
  server unix:///home/deploy/apps/waynechu_blog/shared/tmp/sockets/puma.sock fail_timeout=0;
}

server {
  listen 443 ssl;
  server_name {{ service_host }};

  root /home/deploy/apps/{{ service_name }}/current/public;

  try_files $uri/index.html $uri @app;

  ssl_certificate {{ letsencrypt_ssl_dir }}/{{ service_host }}/fullchain.pem;
  ssl_certificate_key {{ letsencrypt_ssl_dir }}/{{ service_host }}/privkey.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_dhparam {{ dhparam_pem_path }};
  ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_stapling on;
  ssl_stapling_verify on;

  location @app {
    proxy_pass http://app;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_redirect off;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 10M;
  keepalive_timeout 10;
}
```

roles/service/handlers/main.yml


```yml
- name: Restart nginx
  service: name=nginx state=restarted
```


At the last, set your own vars

vars/main.yml

```yml
service_host: "example.cc"
service_name: "example"
service_admin_email: "example@gmail.com"
letsencrypt_ssl_dir: "/etc/letsencrypt/live"
dhparam_pem_path: "/home/deploy/dhparams.pem"
```


That's it, now you have a https rails web



References:

https://medium.com/@gmaliar/generating-lets-encrypt-certificates-for-nginx-using-ansible-9fd27b90993a
https://gist.github.com/mattiaslundberg/ba214a35060d3c8603e9b1ec8627d349
https://docs.ansible.com/ansible/2.5/modules/letsencrypt_module.html
https://gorails.com/guides/free-ssl-with-rails-and-nginx-using-let-s-encrypt
https://blog.frost.tw/posts/2018/05/28/Getting-started-deploy-your-Ruby-on-Rails-Part-8/

2018年11月18日 星期日

Implement algolia autocomplete with rails


```js
// app/javascripts/application.js
const algoliasearch = require('algoliasearch/lite')
const autocomplete = require('autocomplete.js')

const client = algoliasearch(process.env.ALGOLIA_APPLICATION_ID, process.env.ALGOLIA_SEARCH_ONLY_API_KEY)

const pack = {
  algoliaClient: client,
  autocomplete: autocomplete
}

window.Pack = pack
```


```erb
<!-- Algolia search autocomplete HTML markup -->
<div class="aa-input-container" id="aa-input-container">
  <input type="search" id="aa-search-input" class="aa-input-search" placeholder="Search multiple fields in one place..." name="search" autocomplete="off" />
  <svg class="aa-input-icon" viewBox="654 -372 1664 1664">
    <path d="M1806,332c0-123.3-43.8-228.8-131.5-316.5C1586.8-72.2,1481.3-116,1358-116s-228.8,43.8-316.5,131.5  C953.8,103.2,910,208.7,910,332s43.8,228.8,131.5,316.5C1129.2,736.2,1234.7,780,1358,780s228.8-43.8,316.5-131.5  C1762.2,560.8,1806,455.3,1806,332z M2318,1164c0,34.7-12.7,64.7-38,90s-55.3,38-90,38c-36,0-66-12.7-90-38l-343-342  c-119.3,82.7-252.3,124-399,124c-95.3,0-186.5-18.5-273.5-55.5s-162-87-225-150s-113-138-150-225S654,427.3,654,332  s18.5-186.5,55.5-273.5s87-162,150-225s138-113,225-150S1262.7-372,1358-372s186.5,18.5,273.5,55.5s162,87,225,150s113,138,150,225  S2062,236.7,2062,332c0,146.7-41.3,279.7-124,399l343,343C2305.7,1098.7,2318,1128.7,2318,1164z" />
  </svg>
</div>

<!-- Initialize autocomplete menu -->
<script>
  var index = Pack.algoliaClient.initIndex('<%= index_name %>');
  Pack.autocomplete('#aa-search-input',
  { hint: false }, {
    source: Pack.autocomplete.sources.hits(index, {hitsPerPage: 5, filters: 'discarded_at_i < 1'}),
    displayKey: 'name',
    templates: {
      suggestion: function(suggestion) {
        return `<a href='<%= path_prefix %>/${suggestion.objectID}'>${suggestion._highlightResult.name.value}</a>`
      }
    }
  }).on("autocomplete:selected", function (event, suggestion, dataset) {
    window.location.href = `<%= path_prefix %>/${suggestion.objectID}`;
  })
</script>

```


2018年11月17日 星期六

Submit a rails ActiveStorage issue

https://github.com/rails/rails/issues/34470



### Steps to reproduce

When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.

### Expected behavior

I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.

```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc

const result = {
  "source": "GoogleDrive",
  "id": "uppy-kittyjpg-image/jpeg",
  "name": "kitty.jpg",
  "extension": "jpg",
  "meta": {
    "name": "kitty.jpg",
    "type": "image/jpeg"
  },
  "type": "image/jpeg",
  "data": {
    "isFolder": false,
    "icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
    "name": "kitty.jpg",
    "mimeType": "image/jpeg",
    "id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
    "thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
    "requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
    "modifiedDate": "2018-11-07T15:06:35.239Z",
    "custom": {
      "isTeamDrive": false
    }
  },
  "progress": {
    "percentage": 0,
    "bytesUploaded": 0,
    "bytesTotal": 0,
    "uploadComplete": false,
    "uploadStarted": false
  },
  "size": 0,
  "isRemote": true,
  "remote": {
    "serverUrl": "http://localhost:3020",
    "url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
    "body": {
      "fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
    },
    "providerOptions": {
      "serverUrl": "http://localhost:3020",
      "provider": "drive",
      "authProvider": "google"
    }
  },
  "preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}

const uploadFile = (file) {
  const url = input.dataset.directUploadUrl
  const upload = new DirectUpload(file, url)

  upload.create((error, blob) => {
      if (error) {
          console.log("Error!!")
      } else {
          console.log("No Error")
      }
  })
}

uploadFile(result.data) // => Should print something
```

### Actual behavior

```js
const uploadFile = (file) {
  const url = input.dataset.directUploadUrl
  const upload = new DirectUpload(file, url)

  upload.create((error, blob) => {
      if (error) {
          console.log("Error!!")
      } else {
          console.log("No Error")
      }
  })
}

uploadFile(result.data) // => Nothing printed
```

### Digging into it

The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.

According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42

It will just return false and invoke no callback.

I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.


### System configuration
**Rails version**:
5.2.1

**Ruby version**:
2.5.1

**ActiveStorage npm package version**
5.2.1

Integrate Uppy with Rails and ActiveStorage and use google drive direct upload

We're going to integrate Uppy with "upload from Google Drive" feature with rails backend and ActiveStorage, in this case we are going to upload multiple images at once and using the direct upload concept.


What we need?


1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)



## 1.  Install Uppy JS into rails using webpacker

```json
// package.json

{
  "name": "platform",
  "private": true,
  "dependencies": {
    "@rails/webpacker": "3.5",
    "dotenv": "^6.1.0",
    "uppy": "^0.28.0"
  },
  "devDependencies": {
    "webpack-dev-server": "2.11.2"
  }
}
```

To hide credential from git history, we add `dotenv` npm package to do so

```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')

const dotenvFiles = [
  `.env.${process.env.NODE_ENV}.local`,
  '.env.local',
  `.env.${process.env.NODE_ENV}`,
  '.env'
]

dotenvFiles.forEach((dotenvFile) => {
  dotenv.config({ path: dotenvFile, silent: true })
})

environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))


module.exports = environment
```

```
# .env
APPLICATION_URL=http://localhost:3001

UPPY_COMPANION_SERVER_URL=http://localhost:3020
```

```js
// app/javascripts/uppy.js

const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")

const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)

const startUppy = () => {
  const uppy = Uppy({
    debug: true,
    restrictions: {
      allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
    },
  })
    .use(Dashboard, { trigger: uppyTrigger })
    .use(GoogleDrive, {
      target: Dashboard,
      serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
    })
    .use(XHRUpload, {
      endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
      bundle: false,
      headers: { "X-CSRF-Token": csrfToken }
    })

  uppy.on("upload-success", (file, body) => {
    uppy.setFileState(file.id, {
      xhr: Object.assign({}, file, {
        uploadURL: body["uploadURL"],
        signedId: body["signedId"]
      })
    })
  })

  uppy.on("complete", result => {
    result.successful.forEach(file => {
      insertImageSignedId(form, input, file.xhr.signedId)
    })
  })
}

const insertImageSignedId = (form, input, signed_id) => {
  const hiddenField = document.createElement("input")
  hiddenField.setAttribute("type", "hidden")
  hiddenField.setAttribute("value", signed_id)
  hiddenField.name = input.name
  form.appendChild(hiddenField)
}

if (uppyTriggerElement) {
  startUppy()
}
```


And add uppy.js into layout

```erb
<!DOCTYPE html>
<html>
<head>
  <title>Uppy Example</title>
  <%= stylesheet_link_tag    'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
  <%= csrf_meta_tags %>
</head>
<body>
  <div class="container">
    <%= yield %>
    <%= javascript_pack_tag 'uppy' %>
  </div>
</body>
</html>
```


form example

```erb
      <div class="form-group">
        <%= f.label "Images" %>
        <div id="js-uppy-upload" class="form-control">
          Upload
        </div>
        <%=  %>
        <%= f.file_field :images, multiple: true, class: "hidden" %>
      </div>
```

## 2. Figure out how direct upload works with ActiveStorage

ActuveStorage has it's own direct upload JS client, however, it's not useful in this case because it's only able to upload real file data from local but not able to handle google drive upload, therefore we need to implement our own `direct upload` API for it, thankfully it's easy

Add a new endpoint into your rails project, this simplest implementation is below:

```rb
class UploaderController < ApplicationController
  # TODO: Add CSRF protection
  skip_before_action :verify_authenticity_token

  # TODO: Add authentication, cookie based authentication should be good enough

  def image
    # XHRUpload will send image 1 by 1
    # https://uppy.io/docs/xhr-upload/#bundle-false
    file = params[:files].first
    blob = ActiveStorage::Blob.create_after_upload!(
      io: file,
      filename: file.original_filename,
      content_type: file.content_type
    )

    render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
  end
end
```

Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)

Now the upload workflow will be


User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record

## 3. A companion server (a proxy server to handle google auth)

I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself


```json
// package.json

{
  "name": "example-uppy-companion-server",
  "version": "1.0.0",
  "description": "Example uppy companion server",
  "engines": {
    "node": "10.x"
  },
  "main": "index.js",
  "scripts": {
    "start": "heroku local"
  },
  "dependencies": {
    "@uppy/companion": "^0.15.0",
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
    "express-session": "^1.15.6"
  },
  "devDependencies": {
    "request": "^2.81.0",
    "tape": "^4.7.0"
  }
}
```


```js
// index.js

const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')

const app = express()

app.use(bodyParser.json())
app.use(session({
  secret: 'some-secret',
  resave: true,
  saveUninitialized: true
}))

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
  res.setHeader(
    'Access-Control-Allow-Methods',
    'GET, POST, OPTIONS, PUT, PATCH, DELETE'
  )
  res.setHeader(
    'Access-Control-Allow-Headers',
    'Authorization, Origin, Content-Type, Accept'
  )
  res.setHeader('Access-Control-Allow-Credentials', 'true')
  next()
})

// Routes
app.get('/', (req, res) => {
  res.setHeader('Content-Type', 'text/plain')
  res.send('Welcome to Companion')
})

// initialize uppy
const uppyOptions = {
  providerOptions: {
    google: {
      key:  process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
      secret:  process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
    }
  },
  server: {
    host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
    protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
  },
  filePath: "./output",
  secret: "some-secret",
  debug: true
};

app.use(companion.app(uppyOptions))

// handle 404
app.use((req, res, next) => {
  return res.status(404).json({ message: 'Not Found' })
})

// handle server errors
app.use((err, req, res, next) => {
  console.error('\x1b[31m', err.stack, '\x1b[0m')
  res.status(err.status || 500).json({ message: err.message, error: err })
})

companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)

console.log('Welcome to Companion!')
```

```
# .env

PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http

PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET

PORT=3020
```




https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65

2018年11月14日 星期三

Using marked.js with hightlight.js in rails to provide github flavored markdown to github style html


In this example, I use webpacker to import highlight js and marked js and css file, you can use cdn as well.



`app/javascripts/application.js`
```js
import marked from 'marked';
import hljs from 'highlight.js';

marked.setOptions({
  highlight: function(code) {
    return hljs.highlightAuto(code).value;
  },
})

window.marked = marked
```

Let's say we have Post model with `content` field with markdown text

`app/views/posts/show.html.erb`

```erb
<h1><%= @post.title %></h1>

<%= @post.user.email %> - <%= @post.created_at %>

<div id="postContent">
  <%= @post.content %>
</div>

<%= content_for :javascripts do %>
<script>
  var contentElement = document.querySelector('#postContent')
  contentElement.innerHTML = marked(contentElement.innerHTML)
</script>
<% end %>
```

And add application.js into layout by adding this line into `app/views/layout/application.html.erb`

```erb
<%= javascript_pack_tag 'application' %>
```

Now marked.js and hightligh.js are both working properly, check the HTML DOM on post page and you'll see html elements with some auto generated css classes.

However, it's not showing the right styles because we only apply JS but not CSS, now what we need to do is add github style which is provided by highlight.js.

In my other post I've shown how to add it: https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html

In `app/javascripts/styles.js`

```js
import 'highlight.js/styles/github.css';
```

And then add to layout

```erb
<%= stylesheet_pack_tag 'styles' %>
```

And that's it, now the Post show page will show markdown text in HTML format with github styles.

Cheers.


https://github.com/highlightjs/highlight.js/issues/578
https://marked.js.org/#/USING_ADVANCED.md#highlight
https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html

Import css from npm module in rails with webpacker


I use highlight.js

In `app/javascripts/styles.js`

```js
import 'highlight.js/styles/github.css';
```

And then add to layout

```erb
<%= stylesheet_pack_tag 'styles' %>
```

Done.

You can simply replace highlight.js with any other module like bootstrap though

https://github.com/rails/webpacker/blob/master/docs/css.md

2018年8月19日 星期日

setup ubuntu with ansible for rails deployment


https://semaphoreci.com/community/tutorials/how-to-deploy-rails-applications-with-ansible-capistrano-and-semaphore


2018年7月2日 星期一

reset password token leaking

reset password 的網址常常是

https://yourdomain/password/reset?token=xxxx

但這樣就會被第三方網站從 `HTTP Referer` 內找到並紀錄起來 reset password 的 token,例如 google analytics,就有安全疑慮

幾個做法:

1. 一進到這個頁面馬上 invalidate 這個 token 然後generate 一個新的塞到 form 裡面
2. 進到 passwords#edit 時就把 params 移除存到 session 裡面再 redirect 回 passwords#edit 頁面,這樣 referer 就沒有token 資訊

https://robots.thoughtbot.com/is-your-site-leaking-password-reset-links


2018年6月29日 星期五

可以傳 AR object 的自制 form object


```ruby
class BaseForm
  include ActiveModel::Model

  attr_reader :record
  def initialize(record)
    @record = record
    record.attributes.each do |key, value|
      class_eval do
        delegate "#{key}=", to: :record
        delegate "#{key}", to: :record
      end
    end
  end

  def save
    valid? && record.save
  end

  def save!
    valid? && record.save!
  end
end

```


rails model conditional validation 的神奇用法 extend

lol, 沒想過可以這樣

```ruby
user = User.find(id)
user.extend(User::RegistrationContext)
```

```ruby
# app/models/user/registration_context.rb
module User::RegistrationContext
  def self.extended(model)
    class << model
      validates :terms_of_service_accepted, acceptance: true
    end
  end

  attr_accessor :terms_of_service_accepted
end
```

```ruby
user = User.new
user.extend(User::RegistrationContext)
user.terms_of_service_accepted = "0"
user.valid?
=> false
user.errors.messages[:terms_of_service_accepted]
=> ["must be accepted"]
```

哈,雖然應該不會去用這種做法,但還是學習了XD

https://karolgalanciak.com/blog/2018/06/24/rails-and-conditional-validations-in-models/

2018年6月23日 星期六

Vue + Webpacker + Rails



https://mkdev.me/en/posts/rails-5-vue-js-how-to-stop-worrying-and-love-the-frontend

Suspender 用 1.46.0 可以使用 rails 5.1.6

ThoughtBot 的 rails bootstraper,現在最新版 default 使用了 rails 5.2 很煩,因為我不喜歡 5.2 的 credential

要使用 5.1.x 版本的話要用 v1.46.0 版本的 Suspender,但 1.46.0 是用 Ruby 版本 2.5.0

所以確切方法是


```
rvm install 2.5.0
rvm use 2.5.0
gem install suspenders -v 1.46.0
suspender your_project_name
```

然後再進去 project 裡面把 Gemfile 和 .ruby-version 裡的 ruby version 改成2.5.1

https://github.com/thoughtbot/suspenders/tree/v1.46.0

Rails where.not 的寫法會略過欄位是 Null 的值


`where.not` 的寫法會略過欄位是 Null 的值,所要對有可能是 Null 值的字串欄位使用 `where.not` 有兩個做法:
1. default 空字串
2. 使用 `or`

另外如果是 boolean 欄位但有可能是 null (雖然一開始就不應該這樣,至少 default true or false),解決方法就脆直接用 where 反正總共才三個值

ex:

User.where(subscribed: [nil, false])


https://robots.thoughtbot.com/activerecord-s-where-not-and-nil

2018年5月25日 星期五

2018年5月24日 星期四

Rails 內建的加解密 MessageEncryptor


http://edgeapi.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html



2018年5月23日 星期三

How to Create Postgres Indexes Concurrently in ActiveRecord Migrations


之前的文章提到要避免 deployment downtime 其中一個方式是 create index concurrently
http://everyday1percent.blogspot.com/2018/05/rails-deployment-downtime.html

原來 rails 有內建的 API 做到這件事

```ruby
class AddIndexToBuildMetricIdOnBuilds < ActiveRecord::Migration
  disable_ddl_transaction!

  def change
    add_index :builds, :build_metric_id, :algorithm => :concurrently
  end
end
```

好潮。




https://robots.thoughtbot.com/how-to-create-postgres-indexes-concurrently-in
https://semaphoreci.com/blog/2017/06/21/faster-rails-indexing-large-database-tables.html


Devise 每次都會同時下 ORDER BY 和 LIMIT 的 sql 去找 users


同事提出的建議:

```
SELECT "users".* FROM "users" WHERE "users"."id" = $? ORDER BY "users"."id" ASC LIMIT $?

id 是 primary key, 理論上不會有重複的資料.

SELECT id, COUNT(id) AS cid FROM users GROUP BY id ORDER BY cid DESC;
id | cid
--------+-----
1 | 1
58291 | 1
(以下省略)

也確認目前的資料中, 的確沒有出現重複的 id.

這個 query 的 ORDER BY 跟 LIMIT 確認不會被 pgsql 的 query plan / optimizer 省略掉, 還是會產生 procedure call 消耗些微的 cpu / memory, 所以還是建議修改這個 query.
```

查了一下主要罪魁禍首是


Devise::Models::Authenticatable.serialize_from_session

這個 method 是每次已登入的 user 訪問頁面時就會 trigger 的,裡面用了 User.to_adapter.get([id]) 的方式來拿資料

```
 :014 > User.to_adapter.get([1])
  User Load (2.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
```

另外比較不重要但是也會影響的地方就是登入時的驗證,確切來說就是 Devise::Models::DatabaseAuthenticatable.find_for_database_authentication ,裡面用了 to_adapter.find_first

```
:005 > User.to_adapter.find_first(id: 1)
  User Load (1.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
```

可以參考的解法:

* customize serializer: https://github.com/plataformatec/devise/wiki/How-to:-Write-your-own-serializer
* overwrite, ex:

```
class User < ApplicationRecord
  def self.serialize_from_session(key, salt)
    record = find_by(id: key)
    record if record && record.authenticatable_salt == salt
  end
end
```

* 想辦法在不必要驗證的地方 skip 上述 find user 的 middelware



bcrypt, devise, and rails secret_key_base

devise 用 secret_key_base 當作產生 token 的依據
devise 用 bcrypt hashify password 然後儲存
rails 的 has_secure_password 也是用 bcrypt 實現


bcrypt() is a hashing algorithm designed by Niels Provos and David Mazières of the OpenBSD Project.


題外話,如果要改 secret_key_base ,那 devise 裡的token, 包含confirmation, reset_password 都會 invalid 需要重新生成。

https://github.com/codahale/bcrypt-ruby
https://coderwall.com/p/sjegjq/use-bcrypt-for-passwords

2018年5月19日 星期六

新的做 soft delete 的 gem - discard


https://github.com/jhawthorn/discard


沒有 default 加上 default scope 我覺得是很棒的決定,這個 gem 留給開發者更多的彈性實作 soft delete,我覺得在大多數的 case 下都是很夠用了,相較之下一樣是 soft delete 的 gem paranoia 就太多 magic 了