Jekyll2021-09-23T11:08:50+00:00https://rubyyagi.com/feed.xmlRuby Yagi šŸRuby, Rails, Web dev articlesAxel KeeSimplify loop using any?, all? and none?2021-09-23T11:01:00+00:002021-09-23T11:01:00+00:00https://rubyyagi.com/simplify-loop-using-any-all-none<p>Ruby has some useful methods for enumerable (Array, Hash, etc), this post will talk about usage of <a href="https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-any-3F">any?</a>, <a href="https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-all-3F">all?</a>, and <a href="https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-none-3F">none?</a>.</p> <p>For examples used on this post, an <strong>Order</strong> model might have many <strong>Payment</strong> model (customer can pay order using split payments).</p> <h2 id="any">any?</h2> <p>To check if an order has any paid payments, a loop-based implementation might look like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">has_paid_payment</span> <span class="o">=</span> <span class="kp">false</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="k">if</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s2">"paid"</span> <span class="c1"># one of the payment is paid</span> <span class="n">has_paid_payment</span> <span class="o">=</span> <span class="kp">true</span> <span class="k">break</span> <span class="k">end</span> <span class="k">end</span> <span class="k">if</span> <span class="n">has_paid_payment</span> <span class="c1"># ...</span> <span class="k">end</span> </code></pre></div></div> <p>We can simplify the code above using <strong>any?</strong> like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">any?</span><span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'paid'</span><span class="p">}</span> <span class="c1"># this will be executed if there is at least one paid payment</span> <span class="k">end</span> </code></pre></div></div> <p><strong>any?</strong> method can take in a block, and it will return true if the block ever returns a value that is not false or nil. (ie. true, or 123, or ā€œabcā€)</p> <p>If no block is supplied, it will perform a self check for the elements:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">any?</span> <span class="c1"># is equivalent to</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">any?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span> <span class="p">}</span> </code></pre></div></div> <hr /> <h2 id="all">all?</h2> <p>To check if all payments has been paid for an order, a loop-based implementation might look like this:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fully_paid</span> <span class="o">=</span> <span class="kp">true</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="k">if</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">!=</span> <span class="s2">"paid"</span> <span class="c1"># one of the payment is not paid, hence not fully paid</span> <span class="n">fully_paid</span> <span class="o">=</span> <span class="kp">false</span> <span class="k">break</span> <span class="k">end</span> <span class="k">end</span> <span class="k">if</span> <span class="n">fully_paid</span> <span class="c1"># ...</span> <span class="k">end</span> </code></pre></div></div> <p>We can simplify the code above using <strong>all?</strong> like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">all?</span><span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'paid'</span><span class="p">}</span> <span class="c1"># this will be executed if all payments are paid</span> <span class="k">end</span> </code></pre></div></div> <p><strong>all?</strong> method can take in a block, and it will return true if the block <strong>never</strong> returns a value that is false or nil for all of the elements.</p> <p>If no block is supplied, it will perform a self check for the elements:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">all?</span> <span class="c1"># is equivalent to</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">all?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span> <span class="p">}</span> </code></pre></div></div> <hr /> <h2 id="none">none?</h2> <p>This is the opposite of <strong>all?</strong>.</p> <p><strong>none?</strong> method accepts a block, and only returns true if the block <strong>never returns true</strong> for all elements.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">none?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'paid'</span> <span class="p">}</span> <span class="c1"># this will be executed if there is no paid payment for the order</span> <span class="k">end</span> </code></pre></div></div> <p>If no block is supplied, it will perform a self check for the elements:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">none?</span> <span class="c1"># is equivalent to</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">none?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span> <span class="p">}</span> </code></pre></div></div> <p>By utilizing any?, all? and none?, we can remove the usage of loop and make it more readable.</p> <h1 id="reference">Reference</h1> <p><a href="https://ruby-doc.org/core-3.0.2/Enumerable.html">Ruby doc on enumeration</a></p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeRuby has some useful methods for enumerable (Array, Hash, etc), this post will talk about usage of any?, all?, and none?.How to truncate long text and show read more / less button2021-08-26T10:43:00+00:002021-08-26T10:43:00+00:00https://rubyyagi.com/how-to-truncate-long-text-and-show-read-more-less-button<p>If you are working on an app that has user generated content (like blog posts, comments etc), there might be scenario where it is too long to display and it would make sense to truncate them and put a ā€œread moreā€ button below it.</p> <p>This post will solely focus on the front end, which the web browser will detect if the text needs to be truncated, and only show read more / read less as needed. (dont show ā€œread moreā€ if the text is short enough)</p> <p>TL;DR? Check out the <a href="https://codepen.io/cupnoodle/pen/powvObw">Demo at CodePen</a>.</p> <h2 id="truncate-text-using-css">Truncate text using CSS</h2> <p>Referenced from <a href="https://github.com/tailwindlabs/tailwindcss-line-clamp">TailwindCSS/line-clamp plugin</a>.</p> <p>Using the combination of these CSS properties, we can truncate the text (eg: <code class="language-plaintext highlighter-rouge">&lt;p&gt;...&lt;/p&gt;</code>) to the number of lines we want :</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.line-clamp-4</span> <span class="p">{</span> <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">-webkit-box</span><span class="p">;</span> <span class="nl">-webkit-box-orient</span><span class="p">:</span> <span class="n">vertical</span><span class="p">;</span> <span class="c">/* truncate to 4 lines */</span> <span class="nl">-webkit-line-clamp</span><span class="p">:</span> <span class="m">4</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>If the text is longer than 4 lines, it will be truncated and will have ending of ā€œ<strong>ā€¦</strong>ā€. If the text is shorter than 4 lines, no changes is made.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/25-how-to-truncate-long-text-and-show-read-more-less-button/truncate_demo.png" alt="truncate demo" /></p> <p>Now that we managed to truncate the text, the next step is to check whether a text is truncated or not, as you can see, the short text above (second paragraph) is not truncated even if we have set <code class="language-plaintext highlighter-rouge">webkit-line-clamp</code> for it.</p> <p>We want to check if the text is actually truncated so that we can show ā€œread moreā€ button for it (we dont need to show read more for short text that is not truncated).</p> <h2 id="check-if-a-text-is-truncated-using-offsetheight-and-scrollheight">Check if a text is truncated using offsetHeight and scrollHeight</h2> <p>Thereā€™s two attributes for HTML elements which we can use to check if the text is truncated, <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight">offsetHeight</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight">scrollHeight</a>. From Mozilla Dev Documentation,</p> <p><strong>offsetHeight</strong> :</p> <blockquote> <p>is a measurement in pixels of the elementā€™s CSS height, including any borders, padding, and horizontal scrollbars (if rendered).</p> </blockquote> <p><strong>scrollHeight</strong> :</p> <blockquote> <p>is equal to the minimum height the element would require in order to fit all the content in the viewport without using a vertical scrollbar.</p> </blockquote> <p>Hereā€™s <a href="https://stackoverflow.com/a/22675563/1901264">a good answer</a> from StackOverflow on what is offsetHeight and scrollHeight. Hereā€™s the visualized summary :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/25-how-to-truncate-long-text-and-show-read-more-less-button/heights.png" alt="heights" /></p> <p>scrollHeight is the total scrollable content height, and offsetHeight is the visible height on the screen. For an overflow view, the scrollHeight is larger than offsetHeight.</p> <p>We can deduce that if the scrollHeight is larger than the offsetHeight, then the element is truncated.</p> <p>Hereā€™s the javascript code to check if an element is truncated :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">p</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">element</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">&lt;</span> <span class="nx">element</span><span class="p">.</span><span class="nx">scrollHeight</span> <span class="o">||</span> <span class="nx">element</span><span class="p">.</span><span class="nx">offsetWidth</span> <span class="o">&lt;</span> <span class="nx">element</span><span class="p">.</span><span class="nx">scrollWidth</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// your element has overflow and truncated</span> <span class="c1">// show read more / read less button</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="c1">// your element doesn't overflow (not truncated)</span> <span class="p">}</span> </code></pre></div></div> <p>This code should be run after the paragraph element is rendered on the screen, you can put this in a <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> tag right before the body closing tag <code class="language-plaintext highlighter-rouge">&lt;/body&gt;</code>.</p> <h2 id="toggle-truncation">Toggle truncation</h2> <p>Assuming you already have the <code class="language-plaintext highlighter-rouge">.line-clamp-4</code> CSS class ready, we can toggle truncation by adding / removing this class on the paragraph element, the code for this can be put into <code class="language-plaintext highlighter-rouge">onclick</code> action of the read more / read less button:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">"document.querySelector('p').classList.remove('line-clamp-4')"</span><span class="nt">&gt;</span>Read more...<span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">"document.querySelector('p').classList.add('line-clamp-4')"</span><span class="nt">&gt;</span>Read less...<span class="nt">&lt;/button&gt;</span> </code></pre></div></div> <h2 id="demo">Demo</h2> <p>I have made a demo for this using Alpine.js and TailwindCSS, you can check the demo at CodePen here : <a href="https://codepen.io/cupnoodle/pen/powvObw">https://codepen.io/cupnoodle/pen/powvObw</a>. Feel free to use this in your project.</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeIf you are working on an app that has user generated content (like blog posts, comments etc), there might be scenario where it is too long to display and it would make sense to truncate them and put a ā€œread moreā€ button below it.Rails tip: use #presence to get params value, or default to a preset value2021-08-21T08:55:00+00:002021-08-21T08:55:00+00:00https://rubyyagi.com/rails-tip-presence<p>Hereā€™s a method I encountered recently at my work and I found it might be useful to you as well.</p> <p>There might be some attribute which you want to set to a default parameter if the user didnā€™t supply any, eg:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># if the parameters is nil or blank, we will default to french fries</span> <span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">present?</span> <span class="p">?</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">]</span> <span class="p">:</span> <span class="s2">"french fries"</span> </code></pre></div></div> <p>This one liner looks concise, but we can make it even shorter like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">presence</span> <span class="o">||</span> <span class="s2">"french fries"</span> </code></pre></div></div> <p>The <strong>presence</strong> method will return nil if the string is a blank string / empty array or nil , then the <code class="language-plaintext highlighter-rouge">||</code> (double pipe) will use the value on the right side (ā€œfrench friesā€).</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">''</span><span class="p">.</span><span class="nf">presence</span> <span class="c1"># =&gt; nil</span> </code></pre></div></div> <p>If the string is not blank, <strong>.presence</strong> will return the string itself.</p> <p>When <strong>params[:sides]</strong> == ā€œā€ :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">presence</span> <span class="o">||</span> <span class="s2">"french fries"</span> <span class="c1"># =&gt; "french fries"</span> </code></pre></div></div> <p>When <strong>params[:sides]</strong> == ā€œcornā€ ,</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">presence</span> <span class="o">||</span> <span class="s2">"french fries"</span> <span class="c1"># =&gt; "corn"</span> </code></pre></div></div> <p>Documentation : <a href="http://api.rubyonrails.org/classes/Object.html#method-i-presence">http://api.rubyonrails.org/classes/Object.html#method-i-presence</a></p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeHereā€™s a method I encountered recently at my work and I found it might be useful to you as well.How to get started with TailwindCSS2021-06-24T14:44:00+00:002021-06-24T14:44:00+00:00https://rubyyagi.com/start-tailwind<h1 id="how-to-get-started-with-tailwindcss">How to get started with TailwindCSS</h1> <p>This article assumes you have heard of <a href="https://tailwindcss.com">TailwindCSS</a>, and interested to try it out but have no idea where to start.</p> <h2 id="try-it-out-online-on-tailwind-play">Try it out online on Tailwind Play</h2> <p>If you just want to try out TailwindCSS and donā€™t want to install anything on your computer yet, <a href="https://play.tailwindcss.com">the official Tailwind Play</a> online playground is a good place to start!</p> <h2 id="try-it-on-your-html-file-without-fiddling-with-the-nodejs-stuff">Try it on your HTML file without fiddling with the nodeJS stuff</h2> <p>This section is for when you want to use TailwindCSS on a static website, but you donā€™t want to deal with the nodeJS NPM stuff.</p> <p>Thankfully, <a href="https://beyondco.de/blog/tailwind-jit-compiler-via-cdn">Beyond Code</a> has released TailwindCSS JIT via CDN. You can read more on how it works on the linked article here : <a href="https://beyondco.de/blog/tailwind-jit-compiler-via-cdn">https://beyondco.de/blog/tailwind-jit-compiler-via-cdn</a></p> <p>To use TailwindCSS classes on your HTML file, simply include this script in between the <code class="language-plaintext highlighter-rouge">&lt;head&gt;...&lt;/head&gt;</code> tag :</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- Include CDN JavaScript --&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://unpkg.com/tailwindcss-jit-cdn"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div></div> <p>Then you can use TailwindCSS class names on your HTML file and it will work!</p> <p>Keep in mind that this JIT javascript file is &gt; 375 KB , which is quite sizeable if you want to use it for production.</p> <blockquote> <p>The bundle size is pretty big (375kb gzipped) when compared to a production built CSS that is usually ~10kb. But some people donā€™t mind this.</p> </blockquote> <p>If you would like to compile a production TailwindCSS, you would have to deal with NPM / nodeJS (I promise it would be painless) , which will go into in the next section.</p> <h2 id="compile-and-purge-tailwindcss-into-a-css-file-for-production-use">Compile and purge TailwindCSS into a .css file for production use</h2> <p>Say you already have the HTML ready with TailwindCSS classes, and now you want to build a production ready CSS file, this is where we need to npm install some stuff and run some npm build script.</p> <p>If you havenā€™t already, open terminal, and navigate to your project folder root, and <strong>initiate npm</strong> :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm init -y </code></pre></div></div> <p>This will create a package.json file in your project folder root.</p> <p>Next, we are going to install TailwindCSS, PostCSS and AutoPrefixer package :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm install -D tailwindcss@latest postcss@latest autoprefixer@latest </code></pre></div></div> <p>Next, create a PostCSS config file on the project folder root, the file name should be <strong>postcss.config.js</strong> , and add TailwindCSS and Autoprefixer plugin into this config file.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// postcss.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">{</span> <span class="na">tailwindcss</span><span class="p">:</span> <span class="p">{},</span> <span class="na">autoprefixer</span><span class="p">:</span> <span class="p">{},</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>If you have custom configuration for TailwindCSS (eg: extension or replacement of default class/font), you can create a Tailwind config file using this command :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx tailwindcss init </code></pre></div></div> <p>This will create a <code class="language-plaintext highlighter-rouge">tailwind.config.js</code> file at the root of your project folder.</p> <p>You can customize this file to extend / replace existing settings, letā€™s open it and change the <strong>purge</strong> dictionary to include all html files :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// tailwind.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">purge</span><span class="p">:</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">./**/*.html</span><span class="dl">'</span> <span class="p">],</span> <span class="na">darkMode</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// or 'media' or 'class'</span> <span class="na">theme</span><span class="p">:</span> <span class="p">{</span> <span class="na">extend</span><span class="p">:</span> <span class="p">{},</span> <span class="p">},</span> <span class="na">variants</span><span class="p">:</span> <span class="p">{},</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">[],</span> <span class="p">}</span> </code></pre></div></div> <p>This will tell TailwindCSS to search for all .html and .js files in the project folder for TailwindCSS classes (eg: mx-4 , text-xl etc), and remove classes that did not appear in the .html and .js files, this can cut down the generated production css file later.</p> <p>Next, create a .css file if you donā€™t have one already, and insert these tailwind directive into it :</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* style.css */</span> <span class="k">@tailwind</span> <span class="n">base</span><span class="p">;</span> <span class="k">@tailwind</span> <span class="n">components</span><span class="p">;</span> <span class="k">@tailwind</span> <span class="n">utilities</span><span class="p">;</span> <span class="c">/* your custom css class/ids here */</span> <span class="nc">.custom-class</span> <span class="p">{</span> <span class="p">}</span> </code></pre></div></div> <p>These directives (eg: <code class="language-plaintext highlighter-rouge">@tailwind base</code>) will be replaced with tailwind styles later on.</p> <p>Now you can run the following line in terminal to generate the production purged Tailwind CSS file :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NODE_ENV=production npx tailwindcss -i ./style.css -c ./tailwind.config.js -o ./dist.css </code></pre></div></div> <p>This will read the original CSS file (<strong>style.css</strong>), use the config located in the <strong>tailwind.config.js</strong> file, and save the production CSS file into <strong>dist.css</strong> file (on the project folder root).</p> <p>Then you can use this production css file in the HTML <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> part like this :</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;head&gt;</span> <span class="c">&lt;!-- production compiled CSS --&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"dist.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span> <span class="nt">&lt;/head&gt;</span> </code></pre></div></div> <p>It might be tedious to type out the long command above to generate production css, especially if you need to generate it more than one time.</p> <p>To save some typing, you can move the command above into the ā€œscriptsā€ section of the <strong>package.json</strong> file :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// package.json</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">your-project-name</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">version</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1.0.0</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">description</span><span class="dl">"</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="dl">"</span><span class="s2">main</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.js</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">scripts</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">test</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">echo </span><span class="se">\"</span><span class="s2">Error: no test specified</span><span class="se">\"</span><span class="s2"> &amp;&amp; exit 1</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">css</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">NODE_ENV=production npx tailwindcss -i ./style.css -c ./tailwind.config.js -o ./dist.css</span><span class="dl">"</span> <span class="p">},</span> <span class="dl">"</span><span class="s2">keywords</span><span class="dl">"</span><span class="p">:</span> <span class="p">[],</span> <span class="dl">"</span><span class="s2">author</span><span class="dl">"</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="dl">"</span><span class="s2">license</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">ISC</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">devDependencies</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">autoprefixer</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">^10.2.6</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">postcss</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">^8.3.5</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">tailwindcss</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">^2.2.2</span><span class="dl">"</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Notice that I have added ā€œ<strong>css</strong>ā€ and the command to generate production css into the ā€œ<strong>scripts</strong>ā€ section.</p> <p>Now you can simply type <code class="language-plaintext highlighter-rouge">npm run css</code> on your terminal to generate the production css! šŸ™Œ</p> <p>I have also created a repo here : <a href="https://github.com/rubyyagi/tailwindcss-generator">https://github.com/rubyyagi/tailwindcss-generator</a> , which you can simply clone it, replace the repo with your own HTML files, and run <code class="language-plaintext highlighter-rouge">npm run css</code> to generate production CSS!</p> <h2 id="using-tailwindcss-on-ruby-on-rails">Using TailwindCSS on Ruby on Rails</h2> <p>If you are interested to use TailwindCSS on a Rails app, you can refer <a href="https://rubyyagi.com/tailwindcss2-rails6/">this article on how to install and use TailwindCSS on a Rails project</a>.</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeHow to get started with TailwindCSSGetting started with automated testing workflow and CI on Ruby2021-06-15T11:06:00+00:002021-06-15T11:06:00+00:00https://rubyyagi.com/unit-test-start<p>If you have been writing Ruby / Rails code for a while, but still canā€™t grasp on the ā€˜<strong>why</strong>ā€™ to write automated test, (eg. why do so many Ruby dev job posting requires automated test skill like Rspec, minitest etc? How does writing automated test makes me a better developer or help the company I work with? )</p> <p>or if you are not sure on how the <strong>CI</strong> (continuous integration) stuff works, this article is written to address these questions!</p> <p>This article wonā€™t go into how to write unit test, but it will let you <strong>experience what is it like working on a codebase that has automated testing in place, and how it can speed up the development flow and assure quality</strong> (ie. confirm the code works). This article assume you have basic knowledge on how to use Git and Github.</p> <p>I have written a small Ruby Sinatra app (with automated test written using Rspec, you dont have to worry about Rspec for now), you can fork it to your Github account: <a href="https://github.com/rubyyagi/cart">https://github.com/rubyyagi/cart</a> , then follow along this article to add a feature for it.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/fork.png" alt="fork" /></p> <p>After forking the repo, you can download the repo as zip or clone it to your computer. The repo is a simple shopping cart web app, which user can add items to cart and checkout, you can also try the completed online demo here : <a href="https://alpine-cart.herokuapp.com">https://alpine-cart.herokuapp.com</a></p> <p>I recommend <strong>creating a new git branch</strong> before start working on it, to make creating pull request easier in the next step. (create a new branch instead of working on master branch directly.)</p> <p>Remember to run <code class="language-plaintext highlighter-rouge">bundle install</code> after downloading the repo, to install the required gem (Sinatra and Rspec for testing).</p> <p>To run the web app, open terminal, navigate to that repo, and run <code class="language-plaintext highlighter-rouge">ruby app.rb</code> , it should spin up a Sinatra web app, which you can access in your web browser at ā€œ<strong>localhost:4567</strong>ā€</p> <p>You can add items to the cart, then click ā€˜<strong>Checkout</strong>ā€™ , which it then will show the total (you will implement the discount part later).</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/example.gif" alt="checkout" /></p> <p>Open the repo with your favorite code editor, you will notice a folder named ā€œ<strong>spec</strong>ā€ :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/specs.png" alt="specs" /></p> <p>This folder contains the test script I have pre written, there are two test files inside (bulk_discount_spec.rb and cart_spec.rb) , they are used to verify if the <strong>bulk_discount</strong> logic and <strong>cart</strong> logic is implemented correctly, you donā€™t have to make changes on these files.</p> <p>To run these test, open terminal, navigate to this repo, and run <code class="language-plaintext highlighter-rouge">rspec</code> (type ā€˜rspecā€™ and press enter).</p> <p>You should see some test cases fail like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/fail_test.png" alt="test fail" /></p> <p>This is expected, as the <strong>bulk_discount.rb</strong> file (it contains logic for bulk discount, located in models/bulk_discount.rb) has not been implemented yet, hence the total returned from the cart on those test are incorrect.</p> <p>For this exercise, you will need to implement the bulk discount logic in <strong>models/bulk_discount.rb</strong> , specifically inside the <strong>def apply</strong> method.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/bulk_discount.png" alt="bulk discount file" /></p> <p>The <strong>BulkDiscount</strong> class has three properties, amount (in cents), quantity_required (integer), and product_name (string).</p> <p>For example in the <strong>app.rb</strong>, we have two bulk discounts :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">discounts</span> <span class="o">=</span> <span class="p">[</span> <span class="no">BulkDiscount</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">amount: </span><span class="mi">100</span><span class="p">,</span> <span class="ss">quantity_required: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">product_name: </span><span class="s1">'apple'</span><span class="p">),</span> <span class="no">BulkDiscount</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">amount: </span><span class="mi">200</span><span class="p">,</span> <span class="ss">quantity_required: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">product_name: </span><span class="s1">'banana'</span><span class="p">)</span> <span class="p">]</span> <span class="n">cart</span> <span class="o">=</span> <span class="no">Cart</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">discounts: </span><span class="n">discounts</span><span class="p">,</span> <span class="ss">products: </span><span class="n">products</span><span class="p">)</span> </code></pre></div></div> <p>We want to give $1 (100 cents in amount) discount off the cart for every 3 Apple (product_name: ā€˜appleā€™) purchased. For example if the customer buys 7 Apple, the cart will be discounted $2 ($1 discount for every 3 apple, 6 apple = discount $2).</p> <p>For the code in <strong>app.rb</strong> , we want to give bulk discount for apple and banana. Say if the cart has 7 apple and 5 banana, it will be discounted with $6 ($1 x 2 + $2 x 2 = $6, discount of $1 x 2 for apple, $2 x 2 for banana).</p> <p>Back to <strong>bulk_discount.rb</strong>, the <strong>apply</strong> method accepts two parameter, first parameter <strong>total</strong> , which is the total of the carts, in cents (eg: $4 is 400 cents), before the current bulk discount is applied.</p> <p>The second parameter <strong>products</strong> in an array of product (you can check <strong>models/product.rb</strong>) that is currently in the cart.</p> <p>The <strong>apply</strong> method should go through all of the product in the products array, and check if there is product matching the <strong>product_name</strong> of the BulkDiscount, and then check if the quantity of the product is larger than the <strong>quantity_required</strong> , and calculate the amount to be deducted and save it to <strong>amount_to_deduct</strong>.</p> <p>Then the <strong>apply</strong> method will return the discounted total (total - amount_to_deduct).</p> <p>Each time you finish writing the code, you can run <code class="language-plaintext highlighter-rouge">rspec</code> to check if the test cases pass. If all of them pass, it means your implementation is correct.</p> <p>This feedback loop process should be a lot faster than opening the web browser and navigate to the web app, add a few products in cart, click ā€˜checkoutā€™ and see if the discount is displayed and if the displayed discount amount is correct.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/rspec.gif" alt="rspec" /></p> <p>Compared to opening web browser and press ā€˜add to cartā€™ and click ā€˜checkoutā€™ every time you want to test if your code is correct :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/example.gif" alt="example" /></p> <p>With automated test, you can check if an implementation is correct a lot faster than testing it manually.</p> <p>And with automated test in place, you can be confident that you didnā€™t break existing feature when you add new feature or make changes in the future, which makes refactoring safe.</p> <p>(Please work on the exercise yourself first before checking the answer šŸ™ˆ)</p> <p>You can compare your implementation with my implementation here (<a href="https://github.com/rubyyagi/cart/blob/answer/models/bulk_discount.rb">https://github.com/rubyyagi/cart/blob/answer/models/bulk_discount.rb</a>) for the bulk discount.</p> <h2 id="continuous-integration-ci">Continuous Integration (CI)</h2> <p>Once you have finished implementing the bulk discount code part and ensure the test passes, you can push the commit to your forked repo.</p> <p>And then you can create a new pull request from your branch to the original repo (the <strong>rubyyagi/cart</strong>) like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/pr1.png" alt="pull request" /></p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/pr2.png" alt="pull request 2" /></p> <p>After creating the pull request, you will notice that Github (Github Actions) automatically runs the test (same like when you run <code class="language-plaintext highlighter-rouge">rspec</code>) :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/ci1.png" alt="CI" /></p> <p>Once the test has passed, it will show this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/ci2.png" alt="CI 2" /></p> <p>Or if there is failing test, it will show ā€œfailā€.</p> <p>The idea is that when you submit a new pull request (containing new feature or fixes), an automated test suite will be run on some external service (Github Actions for this example, but thereā€™s also other CI service provider like CircleCI, SemaphoreCI etc). And you can configure the repository such that only pull request with passing test can be merged.</p> <p>You can take a look at <a href="https://github.com/rails/rails/pulls">pull requests in the official Rails repository</a>, each pull request will trigger an automated test run, to ensure they donā€™t break existing Rails feature, this inspires confidence on the overall code quality.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/ci3.png" alt="ci3" /></p> <p>In my understanding, <strong>Continuous Integration</strong> (CI) is the process of</p> <ol> <li>Create new branch to work on new feature or fixes</li> <li>Create pull request from the new branch to the main / master branch</li> <li>Running automated test suite (and also linting, security checks) on the branch to ensure everything works</li> <li>A senior dev / team lead/ repository owner will code review the new branch, and request changes if required, else they can approve the new branch and merge it to master/ main branch</li> <li>The new branch is merged to master/ main branch</li> <li>The master / main branch is deployed (to Heroku or AWS or some other service) manually or automatically, or if it is a library , push to RubyGems.org</li> </ol> <p>If you are using Heroku, you can configure to automatic deploy new merge to master / main branch only if the CI (automated test suite) passes :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/cd1.png" alt="cd" /></p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/cd2.png" alt="cd2" /></p> <p>I have already setup the automated test suite to run on Github actions for this repository, you can check the workflow file here if you are interested : <a href="https://github.com/rubyyagi/cart/blob/master/.github/workflows/test.yml">https://github.com/rubyyagi/cart/blob/master/.github/workflows/test.yml</a> .</p> <p>Different CI usually requires different configuration file on the repository, eg: for CircleCI, you need to write the configuration file in <code class="language-plaintext highlighter-rouge">/.circleci/config.yml</code> in your repository, these config are vendor specific.</p> <p>Hope this article has made you understand the importance of automated testing, and how it can speed up development process and inspire confidence in code quality!</p> <h2 id="further-reading">Further Reading</h2> <p><a href="https://rubyyagi.com/intro-rspec-capybara-testing/">Introduction to Rails testing with RSpec and Capybara</a></p> <p><a href="https://rubyyagi.com/rspec-request-spec/">Introduction to API testing using RSpec and Request Spec</a></p> <p><a href="https://www.codewithjason.com/rails-testing-guide/">The beginner guide to Rails testing, written by Jason Swett</a></p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeIf you have been writing Ruby / Rails code for a while, but still canā€™t grasp on the ā€˜whyā€™ to write automated test, (eg. why do so many Ruby dev job posting requires automated test skill like Rspec, minitest etc? How does writing automated test makes me a better developer or help the company I work with? )How to call methods dynamically using string of method name2021-02-15T14:40:00+00:002021-02-15T14:40:00+00:00https://rubyyagi.com/call-dynamic-method-name<p>I was working on a feature for my Rails app, which allows merchant to set fulfillment times on each day like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/20-call-dynamic-method-name/fulfillment_time.png" alt="fulfillment_time" /></p> <p>The main feature is that once an order is placed, the customer canā€™t cancel the placed order after the fulfillment time starts on that day (or on tomorrow).</p> <p>The fulfillment start times are saved in different columns like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">create_table</span> <span class="s2">"fulfillments"</span><span class="p">,</span> <span class="ss">force: :cascade</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"monday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"tuesday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"wednesday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"thursday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"friday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"saturday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"sunday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="k">end</span> </code></pre></div></div> <p>To check if the fulfillment time has started, my app will get the current day of week using <code class="language-plaintext highlighter-rouge">DateTime.current.strftime('%A').downcase</code> . If today is wednesday, then it will return <code class="language-plaintext highlighter-rouge">wednesday</code> .</p> <p>The question now is that <strong>how do I access value of different column based on different day of week</strong>? Say I want to get the value of <code class="language-plaintext highlighter-rouge">thursday_start_time</code> if today is <strong>thursday</strong> ; or the value of <code class="language-plaintext highlighter-rouge">sunday_start_time</code> is today is <strong>sunday</strong> .</p> <p>Fortunately, Rubyā€™s metaprogramming feature allows us to call methods dynamically by just passing the method name into <strong>public_send(method_name)</strong> or <strong>send(method_name)</strong> .</p> <p>Say we have a class like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Duck</span> <span class="k">def</span> <span class="nf">make_noise</span> <span class="nb">puts</span> <span class="s2">"quack"</span> <span class="k">end</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">jump</span> <span class="nb">puts</span> <span class="s2">"can't jump"</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>We can call the <strong>make_noise</strong> method by calling <code class="language-plaintext highlighter-rouge">Duck.new.public_send("make_noise")</code>, this is equivalent to calling Duck.new.make_noise .</p> <p>For the <strong>jump</strong> method, we would need to use <strong>send</strong> instead, as it is a private method, <code class="language-plaintext highlighter-rouge">Duck.new.send("jump")</code></p> <p>For the fulfillment start time, we can then pass in the day dynamically like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># this will return 'monday' if today is monday, or 'tuesday' if today is tuesday etc..</span> <span class="n">day_of_week</span> <span class="o">=</span> <span class="no">DateTime</span><span class="p">.</span><span class="nf">current</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%A'</span><span class="p">).</span><span class="nf">downcase</span> <span class="c1"># if today is monday, it will call 'monday_start_time', etc..</span> <span class="n">start_time</span> <span class="o">=</span> <span class="n">fulfillment</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">day_of_week</span><span class="si">}</span><span class="s2">_start_time"</span><span class="p">)</span> </code></pre></div></div> <p>Sweet!</p> <p>Alternatively, you can also pass in symbol instead of string : <strong>public_send(:make_noise)</strong> .</p> <h2 id="passing-parameters-to-public_send--send">Passing parameters to public_send / send</h2> <p>If you would like to send parameters to the method, you can pass them after the method name :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Animal</span> <span class="k">def</span> <span class="nf">make_noise</span><span class="p">(</span><span class="n">noise</span><span class="p">,</span> <span class="n">times</span><span class="p">)</span> <span class="mi">1</span><span class="o">..</span><span class="n">times</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="nb">puts</span> <span class="n">noise</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="no">Animal</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="s1">'make_noise'</span><span class="p">,</span> <span class="s1">'quack'</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="c1"># quack</span> <span class="c1"># quack</span> <span class="c1"># quack</span> </code></pre></div></div> <script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script> <h2 id="reference">Reference</h2> <p>Ruby Docā€™s <a href="https://ruby-doc.org/core-3.0.0/Object.html#method-i-public_send">public_send documentation</a></p> <p>Ruby Docā€™s <a href="https://ruby-doc.org/core-3.0.0/Object.html#method-i-send">send documentation</a></p>Axel KeeI was working on a feature for my Rails app, which allows merchant to set fulfillment times on each day like this :How to run tests in parallel in Github Actions2021-01-25T17:22:00+00:002021-01-25T17:22:00+00:00https://rubyyagi.com/how-to-run-tests-in-parallel-in-github-actions<h1 id="how-to-run-tests-in-parallel-in-github-actions">How to run tests in parallel in Github Actions</h1> <p>The company I work for has recently switched to Github Actions for CI, as they offer 2000 minutes per month which is quite adequate for our volume. We have been running tests in parallel (2 instances) on the previous CI to save time, and we wanted to do the same on Github Actions.</p> <p>Github Actions provides <a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix">strategy matrix</a> which lets you run tests in parallel, with different configuration for each matrix.</p> <p>For example with a matrix strategy below :</p> <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span> <span class="na">test</span><span class="pi">:</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Test</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">strategy</span><span class="pi">:</span> <span class="na">matrix</span><span class="pi">:</span> <span class="na">ruby</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">2.5.x'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">2.6.x'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">2.7.x'</span> <span class="na">activerecord</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">5.0'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">6.0'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">6.1'</span> </code></pre></div></div> <p>It will run 9 tests in parallel, 3 versions of Ruby x 3 versions of ActiveRecord = 9 tests</p> <p><img src="https://rubyyagi.s3.amazonaws.com/19-parallel-test-github-actions/9tests.png" alt="9 tests" /></p> <p>One downside of Github Actions is that they donā€™t have built-in split tests function to split test files across different CI instances, which many other CI have. For example CircleCI provides split testing command like this :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run rspec in parallel, and split the tests by timings</span> - run: <span class="nb">command</span>: | <span class="nv">TESTFILES</span><span class="o">=</span><span class="si">$(</span>circleci tests glob <span class="s2">"spec/**/*_spec.rb"</span> | circleci tests <span class="nb">split</span> <span class="nt">--split-by</span><span class="o">=</span>timings<span class="si">)</span> bundle <span class="nb">exec </span>rspec <span class="nv">$TESTFILES</span> </code></pre></div></div> <p>With split testing, we can run different test files on different CI instances to reduce the test execution time on each instance.</p> <p>Fortunately, we can write our own script to split the test files in Ruby.</p> <p>Before we proceed to writing split test script, lets configure the Github Actions workflow to run our tests in parallel.</p> <h1 id="configuring-github-actions-workflow-to-run-test-in-parallel">Configuring Github Actions workflow to run test in parallel</h1> <p>Letā€™s configure our Github Actions workflow to run test in parallel :</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/test.yml</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test</span> <span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span> <span class="na">jobs</span><span class="pi">:</span> <span class="na">test</span><span class="pi">:</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">strategy</span><span class="pi">:</span> <span class="na">fail-fast</span><span class="pi">:</span> <span class="no">false</span> <span class="na">matrix</span><span class="pi">:</span> <span class="c1"># Set N number of parallel jobs you want to run tests on.</span> <span class="c1"># Use higher number if you have slow tests to split them on more parallel jobs.</span> <span class="c1"># Remember to update ci_node_index below to 0..N-1</span> <span class="na">ci_node_total</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">2</span><span class="pi">]</span> <span class="c1"># set N-1 indexes for parallel jobs</span> <span class="c1"># When you run 2 parallel jobs then first job will have index 0, the second job will have index 1 etc</span> <span class="na">ci_node_index</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">0</span><span class="pi">,</span> <span class="nv">1</span><span class="pi">]</span> <span class="na">env</span><span class="pi">:</span> <span class="na">BUNDLE_JOBS</span><span class="pi">:</span> <span class="m">2</span> <span class="na">BUNDLE_RETRY</span><span class="pi">:</span> <span class="m">3</span> <span class="na">BUNDLE_PATH</span><span class="pi">:</span> <span class="s">vendor/bundle</span> <span class="na">PGHOST</span><span class="pi">:</span> <span class="s">127.0.0.1</span> <span class="na">PGUSER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">PGPASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">RAILS_ENV</span><span class="pi">:</span> <span class="s">test</span> <span class="na">services</span><span class="pi">:</span> <span class="na">postgres</span><span class="pi">:</span> <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:9.6-alpine</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">5432:5432"</span><span class="pi">]</span> <span class="na">options</span><span class="pi">:</span> <span class="pi">&gt;-</span> <span class="s">--health-cmd pg_isready</span> <span class="s">--health-interval 10s</span> <span class="s">--health-timeout 5s</span> <span class="s">--health-retries 5</span> <span class="na">env</span><span class="pi">:</span> <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">bookmarker_test</span> <span class="na">steps</span><span class="pi">:</span> <span class="c1"># ...</span> </code></pre></div></div> <p>The <strong>ci_node_total</strong> means the total number of parallel instances you want to spin up during the CI process. We are using 2 instances here so the value is [2]. If you would like to use more instances, say 4 instances, then you can change the value to [4] :</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">ci_node_total</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">4</span><span class="pi">]</span> </code></pre></div></div> <p>The <strong>ci_node_index</strong> means the index of the parallel instances you spin up during the CI process, this should match the ci_node_total you have defined earlier.</p> <p>For example, if you have 2 total nodes, your <strong>ci_node_index</strong> should be [0, 1]. If you have 4 total nodes, your <strong>ci_node_index</strong> should be [0, 1, 2, 3]. This is useful for when we write the script to split the tests later.</p> <p>The <strong>fail-fast: false</strong> means that we want to continue running the test on other instances even if there is failing test on one of the instance. The default value for fail-fast is true if we didnā€™t set it, which will stop all instances if there is even one failing test on one instance. Setting fail-fast: false would allow all instances to finish run their tests, and we can see which test has failed later on.</p> <p>Next, we add the testing steps in the workflow :</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">steps</span><span class="pi">:</span> <span class="c1"># ... Ruby / Database setup...</span> <span class="c1"># ...</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Make bin/ci executable</span> <span class="na">run</span><span class="pi">:</span> <span class="s">chmod +x ./bin/ci</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Rspec test</span> <span class="na">id</span><span class="pi">:</span> <span class="s">rspec-test</span> <span class="na">env</span><span class="pi">:</span> <span class="c1"># Specifies how many jobs you would like to run in parallel,</span> <span class="c1"># used for partitioning</span> <span class="na">CI_NODE_TOTAL</span><span class="pi">:</span> <span class="s">${{ matrix.ci_node_total }}</span> <span class="c1"># Use the index from matrix as an environment variable</span> <span class="na">CI_NODE_INDEX</span><span class="pi">:</span> <span class="s">${{ matrix.ci_node_index }}</span> <span class="na">continue-on-error</span><span class="pi">:</span> <span class="no">true</span> <span class="na">run </span><span class="pi">:</span> <span class="pi">|</span> <span class="s">./bin/ci</span> </code></pre></div></div> <p>We pass the <strong>ci_node_total</strong> and <strong>ci_node_index</strong> variables into the environment variables (<strong>env:</strong> ) for the test step.</p> <p>The <strong>continue-on-error: true</strong> will instruct the workflow to continue to the next step even if there is error on the test step (ie. when any of the test fails).</p> <p>And lastly we want to run the custom test script that split the test files and run them <strong>./bin/ci</strong> , remember to make this file executable in the previous step.</p> <p>We will look into how to write the <strong>bin/ci</strong> in the next section.</p> <h2 id="preparing-the-ruby-script-to-split-test-files-and-run-them">Preparing the ruby script to split test files and run them</h2> <p>In this section, we will write the <strong>bin/ci</strong> file.</p> <p>First, create a file named ā€œ<strong>ci</strong>ā€ (without file extension), and place it in the ā€œ<strong>bin</strong>ā€ folder inside your Rails app like this : <img src="https://rubyyagi.s3.amazonaws.com/19-parallel-test-github-actions/ci_file.png" alt="ci_file_location" /></p> <p>Hereā€™s the code for the <strong>ci</strong> file :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span> <span class="n">tests</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">[</span><span class="s2">"spec/**/*_spec.rb"</span><span class="p">].</span> <span class="nf">sort</span><span class="o">.</span> <span class="c1"># Add randomization seed based on SHA of each commit</span> <span class="n">shuffle</span><span class="p">(</span><span class="ss">random: </span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'GITHUB_SHA'</span><span class="p">].</span><span class="nf">to_i</span><span class="p">(</span><span class="mi">16</span><span class="p">))).</span> <span class="nf">select</span><span class="p">.</span> <span class="nf">with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">el</span><span class="p">,</span> <span class="n">i</span><span class="o">|</span> <span class="n">i</span> <span class="o">%</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"CI_NODE_TOTAL"</span><span class="p">].</span><span class="nf">to_i</span> <span class="o">==</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"CI_NODE_INDEX"</span><span class="p">].</span><span class="nf">to_i</span> <span class="k">end</span> <span class="nb">exec</span> <span class="s2">"bundle exec rspec </span><span class="si">#{</span><span class="n">tests</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Dir["spec/**/*_spec.rb"]</code> will return an array of path of test files, which are located inside the <strong>spec</strong> folder , and filename ends with <strong>_spec.rb</strong> . (eg: <em>[ā€œspec/system/bookmarks/create_spec.rbā€, ā€œspec/system/bookmarks/update_spec.rbā€]</em>) .</p> <p>You can change this to <code class="language-plaintext highlighter-rouge">Dir["test/**/*_test.rb"]</code> if you are using minitest instead of Rspec.</p> <p><strong>sort</strong> will sort the array of path of test files in alphabetical order.</p> <p><strong>shuffle</strong> will then randomize the order of the array, using the seed provided in the <strong>random</strong> parameter, so that the randomized order will differ based on the <strong>random</strong> parameter we passed in.</p> <p><strong>ENV[ā€˜GITHUB_SHAā€™]</strong> is the SHA hash of the commit that triggered the Github Actions workflow, which available as environment variables in Github Actions.</p> <p>As <strong>Random.new</strong> only accept integer for the seed parameter, we have to convert the SHA hash (in hexadecimal string, eg: fe5300b3733d69f0a187a5f3237eb05bb134e341 ) into integer using hexadecimal as base (<strong>.to_i(16)</strong>).</p> <p>Then we select the test files that will be run using <strong>select.with_index</strong>. For each CI instance, we will select the test files from the array, which the remainder of the index divided by total number of nodes equals to the CI instance index.</p> <p>For example, say CI_NODE_TOTAL = 2, CI_NODE_INDEX = 1, and we have four test files :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/19-parallel-test-github-actions/explanation.png" alt="explanation" /></p> <p>The test files will be split as shown above.</p> <p>The downside of this split is that the tests will not be splitted based on the duration of the test, which might make some instance running longer than the others. CircleCI does store historical data of each tests duration and can split the test files based on timing, hopefully Github Actions will implement this feature in the future.</p> <h2 id="example-repository">Example repository</h2> <p>I have created an example Rails repository with parallel tests for Github Actions here : <a href="https://github.com/rubyyagi/bookmarker">https://github.com/rubyyagi/bookmarker</a></p> <p>You can check out the <a href="https://github.com/rubyyagi/bookmarker/blob/main/.github/workflows/test.yml">full workflow .yml file here</a> , the <a href="https://github.com/rubyyagi/bookmarker/blob/main/bin/ci">bin/ci file here</a> , and an <a href="https://github.com/rubyyagi/bookmarker/actions/runs/507483357">example test run here</a>.</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeHow to run tests in parallel in Github ActionsDevise only allow one session per user at the same time2021-01-16T15:42:00+00:002021-01-16T15:42:00+00:00https://rubyyagi.com/devise-only-allow-one-session-per-user-at-the-same-time<p>Sometimes for security purpose, or you just dont want users to share their accounts, it is useful to implement a check to ensure that only one login (session) is allowed per user at the same time.</p> <p>To check if there is only one login, we will need a column to store information of the current login information. We can use a string column to store a token, and this token will be randomly generated each time the user is logged in, and then we compare if the current sessionā€™s login token is equal to this token.</p> <p>Assuming your users info are stored in the <strong>users</strong> table (you can change to other name for the command below), create a ā€œcurrent_login_tokenā€ column for the table :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g migration AddCurrentLoginTokenToUsers current_login_token:string </code></pre></div></div> <p>This would generate migration file below :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AddCurrentLoginTokenToUsers</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.1</span><span class="p">]</span> <span class="k">def</span> <span class="nf">change</span> <span class="n">add_column</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">:current_login_token</span><span class="p">,</span> <span class="ss">:string</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Now run <strong>rails db:migrate</strong> to run this migration.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails db:migrate </code></pre></div></div> <p>Next, we would need to override the <strong>create</strong> method of SessionsController from Devise, so that we can generate a random string for the <strong>current_login_token</strong> column each time the user signs in.</p> <p>If you donā€™t already have a custom SessionsController created already, you can generate one using this command :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g devise:controllers users -c=sessions </code></pre></div></div> <p>(Assuming the model name is <strong>user</strong>)</p> <p>The -c flag means the controller to generate, as we only need to generate SessionsController here. (You can read more about the <a href="https://github.com/heartcombo/devise/wiki/Tool:-Generate-and-customize-controllers">devise generator command here</a>)</p> <p>This will generate a controller in <strong>app/controllers/users/sessions_controller.rb</strong> , and this will override the default Deviseā€™s SessionsController.</p> <p>Next, we are going to override the ā€œ<strong>create</strong>ā€ method (the method that will be called when user sign in successfully) inside <strong>app/controllers/users/sessions_controller.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Users::SessionsController</span> <span class="o">&lt;</span> <span class="no">Devise</span><span class="o">::</span><span class="no">SessionsController</span> <span class="n">skip_before_action</span> <span class="ss">:check_concurrent_session</span> <span class="c1"># POST /resource/sign_in</span> <span class="k">def</span> <span class="nf">create</span> <span class="k">super</span> <span class="c1"># after the user signs in successfully, set the current login token</span> <span class="n">set_login_token</span> <span class="k">end</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">set_login_token</span> <span class="n">token</span> <span class="o">=</span> <span class="no">Devise</span><span class="p">.</span><span class="nf">friendly_token</span> <span class="n">session</span><span class="p">[</span><span class="ss">:login_token</span><span class="p">]</span> <span class="o">=</span> <span class="n">token</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">current_login_token</span> <span class="o">=</span> <span class="n">token</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">save</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>We use <a href="https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise.rb#L492">Devise.friendly_token</a> to generate a random string for the token, then store it into the current session (<strong>session[:login_token]</strong>) and also save to the current userā€™s <strong>current_login_token</strong> column.</p> <p>The <strong>skip_before_action :check_concurrent_session</strong> will be explained later, put it there for now.</p> <p>Next, we will edit the <strong>application_controller.rb</strong> file (or whichever controller where most of the controllers that require authorization are inherited from).</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span> <span class="c1"># perform the check before each controller action are executed</span> <span class="n">before_action</span> <span class="ss">:check_concurrent_session</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">check_concurrent_session</span> <span class="k">if</span> <span class="n">already_logged_in?</span> <span class="c1"># sign out the previously logged in user, only left the newly login user</span> <span class="n">sign_out_and_redirect</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">already_logged_in?</span> <span class="c1"># if current user is logged in, but the user login token in session </span> <span class="c1"># doesn't match the login token in database,</span> <span class="c1"># which means that there is a new login for this user</span> <span class="n">current_user</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="p">(</span><span class="n">session</span><span class="p">[</span><span class="ss">:login_token</span><span class="p">]</span> <span class="o">==</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">current_login_token</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>As we have wrote <strong>before_action :check_concurrent_session</strong> in the application_controller.rb , all controllers inherited from this will run the <strong>check_concurrent_session</strong> method before the controller action method are executed, this will check if there is any new login session initiated for the same user.</p> <p>We want to exempt this check when user is on the login part, ie. sessions#create , hence we put a <strong>skip_before_action :check_concurrent_session</strong> to skip this check for the Users::SessionController class earlier.</p> <p>Hereā€™s a demo video of this code :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/18-devise-limit-one-user/single-login-demo.gif" alt="demo" /></p> <p>Notice that after the second login attempt, the previously logged in user gets logged out after clicking a link (executing a controller action).</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script>Axel KeeSometimes for security purpose, or you just dont want users to share their accounts, it is useful to implement a check to ensure that only one login (session) is allowed per user at the same time.How to install TailwindCSS 2 on Rails 62020-12-10T00:00:00+00:002020-12-10T00:00:00+00:00https://rubyyagi.com/tailwindcss2-rails6<p><a href="https://blog.tailwindcss.com/tailwindcss-v2">TailwindCSS v2.0</a> was released on 19 November 2020 šŸŽ‰, but it had a few changes that requires some tweak to install and use it on Rails 6, such as the requirement of PostCSS 8 and Webpack 5. As of current (10 December 2020), Rails 6 comes with webpack 4 and PostCSS 7, which doesnā€™t match the requirement of TailwindCSS v2.</p> <p>If we install TailwindCSS v2 directly on a Rails 6 app using <code class="language-plaintext highlighter-rouge">yarn add tailwindcss </code>, we might get a error (PostCSS plugin tailwindcss requires PostCSS 8) like this when running the server :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/tailwind-postcss-error.png" alt="postcss8 error" /></p> <p><br /></p> <p>This tutorial will guide you on <strong>how to install and use TailwindCSS 2 on Rails 6 using new Webpacker version(v5+)</strong>. If webpack / webpacker v5+ breaks your current Rails app javascript stuff, I recommend using the compability build of Tailwind <a href="https://rubyyagi.com/tailwind-css-on-rails-6-intro/#installing-tailwind-css-on-your-rails-app">following this tutorial</a>.</p> <h2 id="update-webpacker-gem-and-npm-package">Update webpacker gem and npm package</h2> <p>Currently the latest <a href="https://www.npmjs.com/package/@rails/webpacker">Webpacker NPM package</a> is not up to date to the <a href="https://github.com/rails/webpacker">master branch of Webpacker repository</a>, we need to use the Github master version until Rails team decide to push the new version of webpacker into NPM registry.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/webpacker_release.png" alt="webpacker release" /></p> <p>To do this, we need to uninstall the current webpacker in our Rails app :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn remove @rails/webpacker </code></pre></div></div> <p>And install the latest webpacker directly from Github (<strong>notice without the ā€œ@ā€</strong>) :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add rails/webpacker </code></pre></div></div> <p>Hereā€™s the difference in package.json :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/package_compare.png" alt="package.json" /></p> <p>Next, we need to do the same for the Webpacker gem as well, as they havenā€™t push the latest webpacker code in Github to RubyGems registry.</p> <p>In your <strong>Gemfile</strong>, change the webpacker gem from :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'webpacker'</span><span class="p">,</span> <span class="s1">'~&gt; 4.0'</span> </code></pre></div></div> <p><strong>to:</strong></p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"webpacker"</span><span class="p">,</span> <span class="ss">github: </span><span class="s2">"rails/webpacker"</span> </code></pre></div></div> <p>then run <code class="language-plaintext highlighter-rouge">bundle install</code> to install the gem from its Github repository. You should see the webpacker gem is version 5+ now :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/webpacker-master.png" alt="webpacker master" /></p> <h2 id="install-tailwind-css-2">Install Tailwind CSS 2</h2> <p>After updating the webpacker gem, we can install TailwindCSS (and its peer dependency) using yarn :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add tailwindcss postcss autoprefixer </code></pre></div></div> <p>We can use webpacker for handling Tailwind CSS in our Rails app, create a folder named <strong>stylesheets</strong> inside <strong>app/javascript</strong> (the full path would be <strong>app/javascript/stylesheets</strong>).</p> <p>Inside the app/javascript/stylesheets folder, create a file and name it ā€œ<strong>application.scss</strong>ā€, then import Tailwind related CSS inside this file :</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* application.scss */</span> <span class="k">@import</span> <span class="s1">"tailwindcss/base"</span><span class="p">;</span> <span class="k">@import</span> <span class="s1">"tailwindcss/components"</span><span class="p">;</span> <span class="k">@import</span> <span class="s1">"tailwindcss/utilities"</span><span class="p">;</span> </code></pre></div></div> <p>The file structure looks like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/6-tailwind-intro/tailwind_import.png" alt="application scss file structure" /></p> <p>Then in <strong>app/javascript/packs/application.js</strong> , require the application.scss :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/packs/application.js</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@rails/ujs</span><span class="dl">"</span><span class="p">).</span><span class="nx">start</span><span class="p">()</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbolinks</span><span class="dl">"</span><span class="p">).</span><span class="nx">start</span><span class="p">()</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">stylesheets/application.scss</span><span class="dl">"</span><span class="p">)</span> </code></pre></div></div> <p>Next, we have to add <strong>stylesheet_pack_tag</strong> that reference the app/javascript/stylesheets/application.scss file in the layout file (app/views/layouts/application.html.erb). So the layout can import the stylesheet there.</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/layouts/application.html.erb--&gt;</span> <span class="cp">&lt;!DOCTYPE html&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>Bootstraper<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">csrf_meta_tags</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">csp_meta_tag</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">stylesheet_link_tag</span> <span class="err">'</span><span class="na">application</span><span class="err">',</span> <span class="na">media:</span> <span class="err">'</span><span class="na">all</span><span class="err">',</span> <span class="err">'</span><span class="na">data-turbolinks-track</span><span class="err">'</span><span class="na">:</span> <span class="err">'</span><span class="na">reload</span><span class="err">'</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="c">&lt;!-- This refers to app/javascript/stylesheets/application.scss--&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">stylesheet_pack_tag</span> <span class="err">'</span><span class="na">application</span><span class="err">',</span> <span class="err">'</span><span class="na">data-turbolinks-track</span><span class="err">'</span><span class="na">:</span> <span class="err">'</span><span class="na">reload</span><span class="err">'</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">javascript_pack_tag</span> <span class="err">'</span><span class="na">application</span><span class="err">',</span> <span class="err">'</span><span class="na">data-turbolinks-track</span><span class="err">'</span><span class="na">:</span> <span class="err">'</span><span class="na">reload</span><span class="err">'</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;/head&gt;</span> .... </code></pre></div></div> <h2 id="add-tailwindcss-to-postcss-config">Add TailwindCSS to PostCSS config</h2> <p>Next, we will add TailwindCSS into the <strong>postcss.config.js</strong> file (located in your Rails app root) :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// postcss.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">tailwindcss</span><span class="dl">"</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-import</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-flexbugs-fixes</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-preset-env</span><span class="dl">'</span><span class="p">)({</span> <span class="na">autoprefixer</span><span class="p">:</span> <span class="p">{</span> <span class="na">flexbox</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-2009</span><span class="dl">'</span> <span class="p">},</span> <span class="na">stage</span><span class="p">:</span> <span class="mi">3</span> <span class="p">})</span> <span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>If you are not using the default file name for TailwindCSS configuration file (tailwind.config.js) , you need to specify it after the tailwindcss require :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// postcss.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span> <span class="c1">// if you are using non default config filename</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">tailwindcss</span><span class="dl">"</span><span class="p">)(</span><span class="dl">"</span><span class="s2">tailwind.config.js</span><span class="dl">"</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-import</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-flexbugs-fixes</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-preset-env</span><span class="dl">'</span><span class="p">)({</span> <span class="na">autoprefixer</span><span class="p">:</span> <span class="p">{</span> <span class="na">flexbox</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-2009</span><span class="dl">'</span> <span class="p">},</span> <span class="na">stage</span><span class="p">:</span> <span class="mi">3</span> <span class="p">})</span> <span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>Now we can use the Tailwind CSS classes on our HTML elements!</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1&gt;</span>This is static#index<span class="nt">&lt;/h1&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"bg-red-500 w-3/4 mx-auto p-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-white text-2xl"</span><span class="nt">&gt;</span>Test test test<span class="nt">&lt;/p&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p><img src="https://rubyyagi.s3.amazonaws.com/6-tailwind-intro/tailwind_preview.png" alt="demo" /></p> <p>You can refer to <a href="https://tailwindcss.com/docs">Tailwind CSS documentation</a> for the list of classes you can use.</p> <p>For further Tailwind CSS customization and configuring PurgeCSS, you can <a href="https://rubyyagi.com/tailwind-css-on-rails-6-intro/#changing-tailwind-css-default-config">refer this post</a>.</p> <h2 id="references">References</h2> <p>Thanks Kieran for writing the <a href="https://www.kieranklaassen.com/upgrade-rails-to-tailwind-css-2/">Tailwind CSS 2 upgrade guide</a>.</p> <script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script>Axel KeeTailwindCSS v2.0 was released on 19 November 2020 šŸŽ‰, but it had a few changes that requires some tweak to install and use it on Rails 6, such as the requirement of PostCSS 8 and Webpack 5. As of current (10 December 2020), Rails 6 comes with webpack 4 and PostCSS 7, which doesnā€™t match the requirement of TailwindCSS v2.How to implement Rails API authentication with Devise and Doorkeeper2020-12-06T00:00:00+00:002020-12-06T00:00:00+00:00https://rubyyagi.com/rails-api-authentication-devise-doorkeeper<p>Most of the time when we implement API endpoints on our Rails app, we want to limit access of these API to authorized users only, thereā€™s a few strategy for authenticating user through API, ranging from a <a href="https://github.com/gonzalo-bulnes/simple_token_authentication">simple token authentication</a> to a fullblown OAuth provider with JWT.</p> <p>In this tutorial, we will implement an OAuth provider for API authentication on the same Rails app we serve the user, using Devise and <a href="https://github.com/doorkeeper-gem/doorkeeper">Doorkeeper</a> gem.</p> <p>After this tutorial, you would be able to implement Devise sign in/sign up on Rails frontend, and Doorkeeper OAuth (login, register) on the API side for mobile app client, or a separate frontend client like React etc.</p> <p>This tutorial assume that you have some experience using Devise and your Rails app will both have a frontend UI and API for users to register and sign in. We can also use Doorkeeper to allow third party to create their own OAuth application on our own Rails app platform, but that is out of the scope of this article, as this article will focus on creating our own OAuth application for self consumption only.</p> <p><strong>Table of contents</strong></p> <ol> <li><a href="#scaffold-a-model">Scaffold a model</a></li> <li><a href="#setup-devise-gem">Setup Devise gem</a></li> <li><a href="#setup-doorkeeper-gem">Setup Doorkeeper gem</a></li> <li><a href="#customize-doorkeeper-configuration">Customize Doorkeeper configuration</a></li> <li><a href="#create-your-own-oauth-application">Create your own OAuth application</a></li> <li><a href="#how-to-login--logout-and-refresh-token-using-api">How to login , logout and refresh token using API</a></li> <li><a href="#create-api-controllers-that-require-authentication">Create API controllers that require authentication</a></li> <li><a href="#create-an-endpoint-for-user-registration">Create an endpoint for user registration</a></li> <li><a href="#revoke-user-token-manually">Revoke user token manually</a></li> <li><a href="#references">References</a></li> </ol> <h2 id="scaffold-a-model">Scaffold a model</h2> <p>Letā€™s start with some scaffolding so we can have a model, controller and view for CRUD, you can skip this section if you already have an existing Rails app.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rails</span> <span class="n">g</span> <span class="n">scaffold</span> <span class="n">bookmarks</span> <span class="n">title</span><span class="ss">:string</span> <span class="n">url</span><span class="ss">:string</span> </code></pre></div></div> <p>then in <strong>routes.rb</strong> , set the root path to ā€˜bookmarks#indexā€™. Devise requires us to set a root path in routes to work.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span> <span class="n">root</span> <span class="s1">'bookmarks#index'</span> </code></pre></div></div> <p>Now we have a sample CRUD Rails app, we can move on to the next step.</p> <h2 id="setup-devise-gem">Setup Devise gem</h2> <p>Add devise gem in the <strong>Gemfile</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span> <span class="c1"># ...</span> <span class="n">gem</span> <span class="s1">'devise'</span><span class="p">,</span> <span class="s1">'~&gt; 4.7.3'</span> </code></pre></div></div> <p>and run <code class="language-plaintext highlighter-rouge">bundle install</code> to install it.</p> <p>Next, run the Devise installation generator :</p> <p><code class="language-plaintext highlighter-rouge">rails g devise:install</code></p> <p>Then we create the user model (or any other model name you are using like admin, staff etc) using Devise :</p> <p><code class="language-plaintext highlighter-rouge">rails g devise User</code></p> <p>You can customize the devise features you want in the generated migration file, and also in the User model file.</p> <p>Then run <code class="language-plaintext highlighter-rouge">rake db:migrate</code> to create the users table.</p> <p>Now we have the Devise user set up, we can add <strong>authenticate_user!</strong> to <strong>bookmarks_controller.rb</strong> so only logged in users can view the controller now.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/bookmarks_controller.rb</span> <span class="k">class</span> <span class="nc">BookmarksController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span> <span class="n">before_action</span> <span class="ss">:authenticate_user!</span> <span class="c1"># ....</span> <span class="k">end</span> </code></pre></div></div> <p>Next we will move to the main part, which is setting up authentication for the API using Doorkeeper gem.</p> <h2 id="setup-doorkeeper-gem">Setup Doorkeeper gem</h2> <p>Add doorkeeper gem in the <strong>Gemfile</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span> <span class="c1"># ...</span> <span class="n">gem</span> <span class="s1">'doorkeeper'</span><span class="p">,</span> <span class="s1">'~&gt; 5.4.0'</span> </code></pre></div></div> <p>and run <code class="language-plaintext highlighter-rouge">bundle install</code> to install it.</p> <p>Next, run the Doorkeeper installation generator :</p> <p><code class="language-plaintext highlighter-rouge">rails g doorkeeper:install</code></p> <p>This will generate the configuration file for Doorkeeper in <strong>config/initializers/doorkeeper.rb</strong>, which we will customize later.</p> <p>Next, run the Doorkeeper migration generator :</p> <p><code class="language-plaintext highlighter-rouge">rails g doorkeeper:migration</code></p> <p>This will generate a migration file for Doorkeeper in <strong>db/migrate/ā€¦_create_doorkeeper_tables.rb</strong> .</p> <p>We will customize the migration file as we wonā€™t need all the tables / attributes generated.</p> <p>Open the <strong>ā€¦._create_doorkeeper_tables.rb</strong> migration file, then edit to make it look like below :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span> <span class="k">class</span> <span class="nc">CreateDoorkeeperTables</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.0</span><span class="p">]</span> <span class="k">def</span> <span class="nf">change</span> <span class="n">create_table</span> <span class="ss">:oauth_applications</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:uid</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="c1"># Remove `null: false` if you are planning to use grant flows</span> <span class="c1"># that doesn't require redirect URI to be used during authorization</span> <span class="c1"># like Client Credentials flow or Resource Owner Password.</span> <span class="n">t</span><span class="p">.</span><span class="nf">text</span> <span class="ss">:redirect_uri</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:scopes</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s1">''</span> <span class="n">t</span><span class="p">.</span><span class="nf">boolean</span> <span class="ss">:confidential</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="kp">true</span> <span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span> <span class="ss">null: </span><span class="kp">false</span> <span class="k">end</span> <span class="n">add_index</span> <span class="ss">:oauth_applications</span><span class="p">,</span> <span class="ss">:uid</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span> <span class="n">create_table</span> <span class="ss">:oauth_access_tokens</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:resource_owner</span><span class="p">,</span> <span class="ss">index: </span><span class="kp">true</span> <span class="c1"># Remove `null: false` if you are planning to use Password</span> <span class="c1"># Credentials Grant flow that doesn't require an application.</span> <span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:application</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:refresh_token</span> <span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:expires_in</span> <span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:revoked_at</span> <span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:created_at</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:scopes</span> <span class="c1"># The authorization server MAY issue a new refresh token, in which case</span> <span class="c1"># *the client MUST discard the old refresh token* and replace it with the</span> <span class="c1"># new refresh token. The authorization server MAY revoke the old</span> <span class="c1"># refresh token after issuing a new refresh token to the client.</span> <span class="c1"># @see https://tools.ietf.org/html/rfc6749#section-6</span> <span class="c1">#</span> <span class="c1"># Doorkeeper implementation: if there is a `previous_refresh_token` column,</span> <span class="c1"># refresh tokens will be revoked after a related access token is used.</span> <span class="c1"># If there is no `previous_refresh_token` column, previous tokens are</span> <span class="c1"># revoked as soon as a new access token is created.</span> <span class="c1">#</span> <span class="c1"># Comment out this line if you want refresh tokens to be instantly</span> <span class="c1"># revoked after use.</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:previous_refresh_token</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">""</span> <span class="k">end</span> <span class="n">add_index</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span> <span class="n">add_index</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:refresh_token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span> <span class="n">add_foreign_key</span><span class="p">(</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:oauth_applications</span><span class="p">,</span> <span class="ss">column: :application_id</span> <span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>The modification I did on the migration file :</p> <ol> <li>Remove <strong>null: false</strong> on the <strong>redirect_uri</strong> for oauth_applications table. As we are using the OAuth for API authentication, we wonā€™t need to redirect the user to a callback page (like after you sign in with Google / Apple / Facebook on an app, they will redirect to a page usually).</li> <li>Remove the creation of table <strong>oauth_access_grants</strong> , along with its related index and foreign key.</li> </ol> <p>The OAuth application table is used to keep track of the application we created to use for authentication. For example, we can create three application, one for Android app client, one for iOS app client and one for React frontend, this way we can know which clients the users are using. If you only need one client (eg: web frontend), it is fine too.</p> <p>Hereā€™s an example of Github OAuth applications :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/example_oauth.png" alt="github oauth" /></p> <p>Next, run <code class="language-plaintext highlighter-rouge">rake db:migrate</code> to add these tables into database.</p> <p>Next, we will customize the Doorkeeper configuration.</p> <h2 id="customize-doorkeeper-configuration">Customize Doorkeeper configuration</h2> <p>Open <strong>config/initializers/doorkeeper.rb</strong> , and edit the following.</p> <p>Comment out or remove the block for <strong>resource_owner_authenticator</strong> at the top of the file.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#config/initializers/doorkeeper.rb</span> <span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="c1"># Change the ORM that doorkeeper will use (requires ORM extensions installed).</span> <span class="c1"># Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="c1"># This block will be called to check whether the resource owner is authenticated or not.</span> <span class="c1"># resource_owner_authenticator do</span> <span class="c1"># raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"</span> <span class="c1"># Put your resource owner authentication logic here.</span> <span class="c1"># Example implementation:</span> <span class="c1"># User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)</span> <span class="c1"># end</span> </code></pre></div></div> <p>The <strong>resouce_owner_authenticator</strong> block is used to get the authenticated user information or redirect the user to login page from OAuth, for example like this Twitter OAuth page :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/example_oauth_redirect.png" alt="twitter oauth example" /></p> <p>As we are going to exchange OAuth token by using user login credentials (email + password) on the API, we donā€™t need to implement this block, so we can comment it out.</p> <p>To tell Doorkeeper we are using user credentials to login, we need to implement the <strong>resource_owner_from_credentials</strong> block like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#config/initializers/doorkeeper.rb</span> <span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="c1"># Change the ORM that doorkeeper will use (requires ORM extensions installed).</span> <span class="c1"># Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="c1"># This block will be called to check whether the resource owner is authenticated or not.</span> <span class="c1"># resource_owner_authenticator do</span> <span class="c1"># raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"</span> <span class="c1"># Put your resource owner authentication logic here.</span> <span class="c1"># Example implementation:</span> <span class="c1"># User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)</span> <span class="c1"># end</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="c1"># ...</span> </code></pre></div></div> <p>This will allow us to send the user email and password to the /oauth/token endpoint to authenticate user.</p> <p>Then we need to implement the <strong>authenticate</strong> class method on the <strong>app/models/user.rb</strong> model file.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/user.rb</span> <span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span> <span class="c1"># Include default devise modules. Others available are:</span> <span class="c1"># :confirmable, :lockable, :timeoutable, :trackable and :omniauthable</span> <span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:registerable</span><span class="p">,</span> <span class="ss">:recoverable</span><span class="p">,</span> <span class="ss">:rememberable</span><span class="p">,</span> <span class="ss">:validatable</span> <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">format: </span><span class="no">URI</span><span class="o">::</span><span class="no">MailTo</span><span class="o">::</span><span class="no">EMAIL_REGEXP</span> <span class="c1"># the authenticate method from devise documentation</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_for_authentication</span><span class="p">(</span><span class="ss">email: </span><span class="n">email</span><span class="p">)</span> <span class="n">user</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">valid_password?</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="p">?</span> <span class="n">user</span> <span class="p">:</span> <span class="kp">nil</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>You can read more on the authenticate method on <a href="https://github.com/heartcombo/devise/wiki/How-To:-Find-a-user-when-you-have-their-credentials">Deviseā€™s github Wiki page</a>.</p> <p>Next, enable password grant flow in <strong>config/initializers/doorkeeper.rb</strong> , this will allow us to send the user email and password to the /oauth/token endpoint and get OAuth token in return.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="c1"># enable password grant</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="c1"># ....</span> </code></pre></div></div> <p>You can search for ā€œgrant_flowsā€ in this file, and uncomment and edit it.</p> <p>Next, insert <strong>allow_blank_redirect_uri true</strong> into the configuration, so that we can create OAuth application with blank redirect URL (user wonā€™t get redirected after login, as we are using API).</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span> <span class="c1"># ....</span> </code></pre></div></div> <p>As the OAuth application we create is for our own use (not third part), we can skip authorization.</p> <p>Insert <strong>skip_authorization</strong> into the configuration like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span> <span class="n">skip_authorization</span> <span class="k">do</span> <span class="kp">true</span> <span class="k">end</span> <span class="c1"># ....</span> </code></pre></div></div> <p>The authorization we skipped is something like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/oauth_authorization.png" alt="authorization" /></p> <p>As we skipped authorization, user wonā€™t need to click the ā€œauthorizeā€ button to interact with our API.</p> <p>Optionally, if you want to enable refresh token mechanism in OAuth, you can insert the <strong>use_refresh_token</strong> into the configuration. This would allow the client app to request a new access token using the refresh token when the current access token is expired.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span> <span class="n">skip_authorization</span> <span class="k">do</span> <span class="kp">true</span> <span class="k">end</span> <span class="n">use_refresh_token</span> <span class="c1"># ...</span> </code></pre></div></div> <p>With this, we have finished configuring Doorkeeper authentication for our API.</p> <p>Next, we will add the doorkeeper route in <strong>routes.rb</strong> , this will add the /oauth/* routes.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span> <span class="n">use_doorkeeper</span> <span class="k">do</span> <span class="n">skip_controllers</span> <span class="ss">:authorizations</span><span class="p">,</span> <span class="ss">:applications</span><span class="p">,</span> <span class="ss">:authorized_applications</span> <span class="k">end</span> <span class="c1"># ...</span> <span class="k">end</span> </code></pre></div></div> <p>As we donā€™t need the app authorization, we can skip the authorizations and authorized_applications controller. We can also skip the applications controller, as users wonā€™t be able to create or delete OAuth application.</p> <p>Next, we need to create our own OAuth application manually in the console so we can use it for authentication.</p> <h2 id="create-your-own-oauth-application">Create your own OAuth application</h2> <p>Open up rails console, <code class="language-plaintext highlighter-rouge">rails console</code></p> <p>Then create an OAuth application using this command :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> </code></pre></div></div> <p>You can change the name to any name you want, and leave redirect_uri and scopes blank.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/create_client.png" alt="create_client" /></p> <p>This will create a record in the <strong>oauth_applications</strong> table. Keep note that the <strong>uid</strong> attribute and <strong>secret</strong> attribute, these are used for authentication on API later, uid = client_id and secret = client_secret.</p> <p>For production use, you can create a database seed for initial creation of the OAuth applications in <strong>db/seeds.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># db/seeds.rb</span> <span class="c1"># if there is no OAuth application created, create them</span> <span class="k">if</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">count</span><span class="p">.</span><span class="nf">zero?</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"iOS client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"React"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> <span class="k">end</span> </code></pre></div></div> <p>Then run <code class="language-plaintext highlighter-rouge">rake db:seed</code> to create these applications.</p> <p><strong>Doorkeeper::Application</strong> is just a namespaced model name for the <strong>oauth_applications</strong> table, you can perform ActiveRecord query as usual :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># client_id of the application</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">).</span><span class="nf">uid</span> <span class="c1"># client_secret of the application</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">).</span><span class="nf">secret</span> </code></pre></div></div> <p>Now we have Doorkeeper application set up, we can try to login user in the next section.</p> <h2 id="how-to-login--logout-and-refresh-token-using-api">How to login , logout and refresh token using API</h2> <p>We will need a user created to be able to login / logout them using the OAuth endpoints, you can register a dummy user on the devise web UI if you havenā€™t already (eg: localhost:3000/users/sign_up) or create one via the rails console.</p> <p>The HTTP requests below can either send attributes using JSON format or URL-Encoded form.</p> <h3 id="login">Login</h3> <p>To login the user on the OAuth endpoint, we need to send a HTTP POST request to <strong>/oauth/token</strong>, with <strong>grant_type</strong>, <strong>email</strong>, <strong>password</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/login_oauth.png" alt="login oauth" /></p> <p>As we are using password in exchange for OAuth access and refresh token, the <strong>grant_type</strong> value should be <strong>password</strong>.</p> <p><strong>email</strong> and <strong>password</strong> is the login credential of the user.</p> <p><strong>client_id</strong> is the <strong>uid</strong> of the Doorkeeper::Application (OAuth application) we created earlier, with this we can identify which client the user has used to log in.</p> <p><strong>client_secret</strong> is the <strong>secret</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p> <p>On successful login attempt, the API will return <strong>access_token</strong>, <strong>refresh_token</strong>, <strong>token_type</strong>, <strong>expires_in</strong> and <strong>created_at</strong> attributes.</p> <p>We can then use <strong>access_token</strong> to call protected API that requires user authentication.</p> <p><strong>refresh_token</strong> can be used to generate and retrieve a new access token after the current access_token has expired.</p> <p><strong>expires_in</strong> is the time until expiry for the access_token, starting from the UNIX timestamp of <strong>created_at</strong>, the default value is 7200 (seconds), which is around 2 hours.</p> <h3 id="logout">Logout</h3> <p>To log out a user, we can revoke the access token, so that the same access token cannot be used anymore.</p> <p>To revoke an access token, we need to send a HTTP POST request to <strong>/oauth/revoke</strong>, with <strong>token</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p> <p>Other than these attributes, we also need to set Authorization header for the HTTP request to use Basic Auth, using <strong>client_id</strong> value for the <strong>username</strong> and <strong>client_password</strong> value for the <strong>password</strong>. (According to <a href="https://github.com/doorkeeper-gem/doorkeeper/issues/1412#issuecomment-631938006">this reply</a> in Doorkeeper gem repository)</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/logout_auth.png" alt="logout auth" /></p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/logout_body.png" alt="logout body" /></p> <p>After revoking a token, the token record will have a <strong>revoked_at</strong> column filled :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/revoked_token.png" alt="revoked_at" /></p> <h3 id="refresh-token">Refresh token</h3> <p>To retrieve a new access token when the current access token is (almost) expired, we can send a HTTP POST to <strong>/oauth/token</strong> , it is the same endpoint as login, but this time we are using ā€œrefresh_tokenā€ as the value for <strong>grant_type</strong>, and is sending the value of refresh token instead of login credentials.</p> <p>To refresh a token, we need to send <strong>grant_type</strong>, <strong>refresh_token</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p> <p><strong>grant_type</strong> needs to be equal to ā€œ<strong>refresh_token</strong>ā€ here as we are using refresh token to authenticate.</p> <p><strong>refresh_token</strong> should be the refresh token value you have retrieved during login.</p> <p><strong>client_id</strong> is the <strong>uid</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p> <p><strong>client_secret</strong> is the <strong>secret</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/refresh_token.png" alt="refresh_token" /></p> <p>On successful refresh attempt, the API return a new access_token and refresh_token, which we can use to call protected API that requires user authentication.</p> <h2 id="create-api-controllers-that-require-authentication">Create API controllers that require authentication</h2> <p>Now that we have user authentication set up, we can now create API controllers that require authentication.</p> <p>For this, I recommend creating a base API application controller, then subclass this controller for controllers that require authentication.</p> <p>Create a base API application controller (<strong>application_controller.rb</strong>) and place it in <strong>app/controllers/api/application_controller.rb</strong> .</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/application_controller.rb</span> <span class="k">module</span> <span class="nn">Api</span> <span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span> <span class="c1"># equivalent of authenticate_user! on devise, but this one will check the oauth token</span> <span class="n">before_action</span> <span class="ss">:doorkeeper_authorize!</span> <span class="kp">private</span> <span class="c1"># helper method to access the current user from the token</span> <span class="k">def</span> <span class="nf">current_user</span> <span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">id: </span><span class="n">doorkeeper_token</span><span class="p">[</span><span class="ss">:resource_owner_id</span><span class="p">])</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>The API application subclasses from <a href="https://api.rubyonrails.org/classes/ActionController/API.html">ActionController::API</a>, which is a lightweight version of ActionController::Base, and does not contain HTML layout and templating functionality (we dont need it for API anyway), and it doesnā€™t have CORS protection.</p> <p>We add the <strong>doorkeeper_authorize!</strong> method in the before_action callback, as this will check if the user is authenticated with a valid token before calling methods in the controller, this is similar to the authenticate_user! method on devise.</p> <p>We also add a <strong>current_user</strong> method to get the current user object, then we can attach the current user on some modelā€™s CRUD action.</p> <p>As example of a protected API controller, letā€™s create a bookmarks controller to retrieve all bookmarks.</p> <p><strong>app/controllers/api/bookmarks_controller.rb</strong></p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/bookmarks_controller.rb</span> <span class="k">module</span> <span class="nn">Api</span> <span class="k">class</span> <span class="nc">BookmarksController</span> <span class="o">&lt;</span> <span class="no">Api</span><span class="o">::</span><span class="no">ApplicationController</span> <span class="k">def</span> <span class="nf">index</span> <span class="vi">@bookmarks</span> <span class="o">=</span> <span class="no">Bookmark</span><span class="p">.</span><span class="nf">all</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">bookmarks: </span><span class="vi">@bookmarks</span> <span class="p">}</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>The bookmarks controller will inherit from the base API application controller we created earlier, and it will return all the bookmarks in JSON format.</p> <p>Donā€™t forget to add the route for it in <strong>routes.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span> <span class="c1"># ....</span> <span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span> <span class="n">resources</span> <span class="ss">:bookmarks</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index]</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Then we can retrieve the bookmarks by sending a HTTP GET request to <strong>/api/bookmarks</strong>, with the userā€™s access token in the Authorization Header (Authorization: Bearer [User Access Token])</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/get_bookmarks_authorized.png" alt="get boomarks api authorization" /></p> <p>To access protected API controllers, we will need to include the Authorization HTTP header, with the values of ā€œBearer [User Access Token]ā€.</p> <h2 id="create-an-endpoint-for-user-registration">Create an endpoint for user registration</h2> <p>It would be weird if we only allow user registration through website, we would also need to add an API endpoint for user to register an account .</p> <p>For this, letā€™s create a users controller and place it in <strong>app/controllers/api/users_controller.rb</strong>.</p> <p>The <strong>create</strong> action will create an user account from the supplied email and password.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Api</span> <span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">Api</span><span class="o">::</span><span class="no">ApplicationController</span> <span class="n">skip_before_action</span> <span class="ss">:doorkeeper_authorize!</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[create]</span> <span class="k">def</span> <span class="nf">create</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">email: </span><span class="n">user_params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="ss">password: </span><span class="n">user_params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="n">client_app</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">uid: </span><span class="n">params</span><span class="p">[</span><span class="ss">:client_id</span><span class="p">])</span> <span class="k">return</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid client ID'</span><span class="p">},</span> <span class="ss">status: </span><span class="mi">403</span><span class="p">)</span> <span class="k">unless</span> <span class="n">client_app</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">save</span> <span class="c1"># create access token for the user, so the user won't need to login again after registration</span> <span class="n">access_token</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="ss">resource_owner_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">application_id: </span><span class="n">client_app</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">refresh_token: </span><span class="n">generate_refresh_token</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">access_token_expires_in</span><span class="p">.</span><span class="nf">to_i</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s1">''</span> <span class="p">)</span> <span class="c1"># return json containing access token and refresh token</span> <span class="c1"># so that user won't need to call login API right after registration</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">user: </span><span class="p">{</span> <span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">email: </span><span class="n">user</span><span class="p">.</span><span class="nf">email</span><span class="p">,</span> <span class="ss">access_token: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">token</span><span class="p">,</span> <span class="ss">token_type: </span><span class="s1">'bearer'</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">expires_in</span><span class="p">,</span> <span class="ss">refresh_token: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">refresh_token</span><span class="p">,</span> <span class="ss">created_at: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">created_at</span><span class="p">.</span><span class="nf">to_time</span><span class="p">.</span><span class="nf">to_i</span> <span class="p">}</span> <span class="p">})</span> <span class="k">else</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span> <span class="p">},</span> <span class="ss">status: </span><span class="mi">422</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">user_params</span> <span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">generate_refresh_token</span> <span class="kp">loop</span> <span class="k">do</span> <span class="c1"># generate a random token string and return it, </span> <span class="c1"># unless there is already another token with the same string</span> <span class="n">token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span> <span class="k">break</span> <span class="n">token</span> <span class="k">unless</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">exists?</span><span class="p">(</span><span class="ss">refresh_token: </span><span class="n">token</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>As the user doesnā€™t have an account at this point, we want to exempt this action from requiring authentication information, so we added the line <strong>skip_before_action :doorkeeper_authorize!, only: %i[create]</strong> at the top. This will make the <strong>create</strong> method to skip running the <strong>doorkeeper_authorize!</strong> before_action method we defined in the base API controller, and the client app can call the user account creation API endpoint without authentication information.</p> <p>We then create an AccessToken on successful user registration using <strong>Doorkeeper::AccessToken.create()</strong> and return it in the HTTP response, so the user wonā€™t need to login right after registration.</p> <p>Remember to add a route for ths user registration action in <strong>routes.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span> <span class="c1"># ....</span> <span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span> <span class="n">resources</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[create]</span> <span class="n">resources</span> <span class="ss">:bookmarks</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index]</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Then we can call create user API by sending a HTTP POST request containing the userā€™s <strong>email</strong>, <strong>password</strong> and <strong>client_id</strong> to <strong>/api/users</strong> like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/oauth_registration.png" alt="create user API" /></p> <p>The <strong>client_id</strong> is used to identify which client app the user is using for registration.</p> <h1 id="revoke-user-token-manually">Revoke user token manually</h1> <p>Say if you suspect a userā€™s access token has been misused or abused, you can revoke them manually using this function :</p> <p><strong>Doorkeeper::AccessToken.revoke_all_for(application_id, resource_owner)</strong></p> <p>application_id is the <strong>id</strong> of the Doorkeeper::Application (OAuth application) we want to revoke the user from.</p> <p>resource_owner is the <strong>user object</strong>.</p> <p>Example usage:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">client_app</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s1">'Android client'</span><span class="p">)</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">revoke_all_for</span><span class="p">(</span><span class="n">client_app</span><span class="p">.</span><span class="nf">id</span> <span class="p">,</span> <span class="n">user</span><span class="p">)</span> </code></pre></div></div> <h2 id="references">References</h2> <p><a href="https://github.com/heartcombo/devise/wiki/How-To:-Find-a-user-when-you-have-their-credentials">Devise wiki - How To: Find a user when you have their credentials</a></p> <p><a href="https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Resource-Owner-Password-Credentials-flow">Doorkeeper wiki - Using Resource Owner Password Credentials flow</a></p> <script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script>Axel KeeMost of the time when we implement API endpoints on our Rails app, we want to limit access of these API to authorized users only, thereā€™s a few strategy for authenticating user through API, ranging from a simple token authentication to a fullblown OAuth provider with JWT.
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<feed xmlns="http://www.w3.org/2005/Atom">
<generator uri="https://jekyllrb.com/" version="4.1.1">Jekyll</generator>
<link href="https://rubyyagi.com/feed.xml" rel="self" type="application/atom+xml"/>
<link href="https://rubyyagi.com/" rel="alternate" type="text/html"/>
<updated>2021-09-23T11:08:50+00:00</updated>
<id>https://rubyyagi.com/feed.xml</id>
<title type="html">Ruby Yagi šŸ</title>
<subtitle>Ruby, Rails, Web dev articles</subtitle>
<author>
<name>Axel Kee</name>
</author>
<entry>
<title type="html">Simplify loop using any?, all? and none?</title>
<link href="https://rubyyagi.com/simplify-loop-using-any-all-none/" rel="alternate" type="text/html" title="Simplify loop using any?, all? and none?"/>
<published>2021-09-23T11:01:00+00:00</published>
<updated>2021-09-23T11:01:00+00:00</updated>
<id>https://rubyyagi.com/simplify-loop-using-any-all-none</id>
<content type="html" xml:base="https://rubyyagi.com/simplify-loop-using-any-all-none/"><p>Ruby has some useful methods for enumerable (Array, Hash, etc), this post will talk about usage of <a href="https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-any-3F">any?</a>, <a href="https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-all-3F">all?</a>, and <a href="https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-none-3F">none?</a>.</p> <p>For examples used on this post, an <strong>Order</strong> model might have many <strong>Payment</strong> model (customer can pay order using split payments).</p> <h2 id="any">any?</h2> <p>To check if an order has any paid payments, a loop-based implementation might look like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">has_paid_payment</span> <span class="o">=</span> <span class="kp">false</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="k">if</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s2">"paid"</span> <span class="c1"># one of the payment is paid</span> <span class="n">has_paid_payment</span> <span class="o">=</span> <span class="kp">true</span> <span class="k">break</span> <span class="k">end</span> <span class="k">end</span> <span class="k">if</span> <span class="n">has_paid_payment</span> <span class="c1"># ...</span> <span class="k">end</span> </code></pre></div></div> <p>We can simplify the code above using <strong>any?</strong> like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">any?</span><span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'paid'</span><span class="p">}</span> <span class="c1"># this will be executed if there is at least one paid payment</span> <span class="k">end</span> </code></pre></div></div> <p><strong>any?</strong> method can take in a block, and it will return true if the block ever returns a value that is not false or nil. (ie. true, or 123, or ā€œabcā€)</p> <p>If no block is supplied, it will perform a self check for the elements:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">any?</span> <span class="c1"># is equivalent to</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">any?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span> <span class="p">}</span> </code></pre></div></div> <hr /> <h2 id="all">all?</h2> <p>To check if all payments has been paid for an order, a loop-based implementation might look like this:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fully_paid</span> <span class="o">=</span> <span class="kp">true</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="k">if</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">!=</span> <span class="s2">"paid"</span> <span class="c1"># one of the payment is not paid, hence not fully paid</span> <span class="n">fully_paid</span> <span class="o">=</span> <span class="kp">false</span> <span class="k">break</span> <span class="k">end</span> <span class="k">end</span> <span class="k">if</span> <span class="n">fully_paid</span> <span class="c1"># ...</span> <span class="k">end</span> </code></pre></div></div> <p>We can simplify the code above using <strong>all?</strong> like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">all?</span><span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'paid'</span><span class="p">}</span> <span class="c1"># this will be executed if all payments are paid</span> <span class="k">end</span> </code></pre></div></div> <p><strong>all?</strong> method can take in a block, and it will return true if the block <strong>never</strong> returns a value that is false or nil for all of the elements.</p> <p>If no block is supplied, it will perform a self check for the elements:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">all?</span> <span class="c1"># is equivalent to</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">all?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span> <span class="p">}</span> </code></pre></div></div> <hr /> <h2 id="none">none?</h2> <p>This is the opposite of <strong>all?</strong>.</p> <p><strong>none?</strong> method accepts a block, and only returns true if the block <strong>never returns true</strong> for all elements.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">none?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'paid'</span> <span class="p">}</span> <span class="c1"># this will be executed if there is no paid payment for the order</span> <span class="k">end</span> </code></pre></div></div> <p>If no block is supplied, it will perform a self check for the elements:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">none?</span> <span class="c1"># is equivalent to</span> <span class="n">order</span><span class="p">.</span><span class="nf">payments</span><span class="p">.</span><span class="nf">none?</span> <span class="p">{</span> <span class="o">|</span><span class="n">payment</span><span class="o">|</span> <span class="n">payment</span> <span class="p">}</span> </code></pre></div></div> <p>By utilizing any?, all? and none?, we can remove the usage of loop and make it more readable.</p> <h1 id="reference">Reference</h1> <p><a href="https://ruby-doc.org/core-3.0.2/Enumerable.html">Ruby doc on enumeration</a></p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">Ruby has some useful methods for enumerable (Array, Hash, etc), this post will talk about usage of any?, all?, and none?.</summary>
</entry>
<entry>
<title type="html">How to truncate long text and show read more / less button</title>
<link href="https://rubyyagi.com/how-to-truncate-long-text-and-show-read-more-less-button/" rel="alternate" type="text/html" title="How to truncate long text and show read more / less button"/>
<published>2021-08-26T10:43:00+00:00</published>
<updated>2021-08-26T10:43:00+00:00</updated>
<id>https://rubyyagi.com/how-to-truncate-long-text-and-show-read-more-less-button</id>
<content type="html" xml:base="https://rubyyagi.com/how-to-truncate-long-text-and-show-read-more-less-button/"><p>If you are working on an app that has user generated content (like blog posts, comments etc), there might be scenario where it is too long to display and it would make sense to truncate them and put a ā€œread moreā€ button below it.</p> <p>This post will solely focus on the front end, which the web browser will detect if the text needs to be truncated, and only show read more / read less as needed. (dont show ā€œread moreā€ if the text is short enough)</p> <p>TL;DR? Check out the <a href="https://codepen.io/cupnoodle/pen/powvObw">Demo at CodePen</a>.</p> <h2 id="truncate-text-using-css">Truncate text using CSS</h2> <p>Referenced from <a href="https://github.com/tailwindlabs/tailwindcss-line-clamp">TailwindCSS/line-clamp plugin</a>.</p> <p>Using the combination of these CSS properties, we can truncate the text (eg: <code class="language-plaintext highlighter-rouge">&lt;p&gt;...&lt;/p&gt;</code>) to the number of lines we want :</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.line-clamp-4</span> <span class="p">{</span> <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">-webkit-box</span><span class="p">;</span> <span class="nl">-webkit-box-orient</span><span class="p">:</span> <span class="n">vertical</span><span class="p">;</span> <span class="c">/* truncate to 4 lines */</span> <span class="nl">-webkit-line-clamp</span><span class="p">:</span> <span class="m">4</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>If the text is longer than 4 lines, it will be truncated and will have ending of ā€œ<strong>ā€¦</strong>ā€. If the text is shorter than 4 lines, no changes is made.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/25-how-to-truncate-long-text-and-show-read-more-less-button/truncate_demo.png" alt="truncate demo" /></p> <p>Now that we managed to truncate the text, the next step is to check whether a text is truncated or not, as you can see, the short text above (second paragraph) is not truncated even if we have set <code class="language-plaintext highlighter-rouge">webkit-line-clamp</code> for it.</p> <p>We want to check if the text is actually truncated so that we can show ā€œread moreā€ button for it (we dont need to show read more for short text that is not truncated).</p> <h2 id="check-if-a-text-is-truncated-using-offsetheight-and-scrollheight">Check if a text is truncated using offsetHeight and scrollHeight</h2> <p>Thereā€™s two attributes for HTML elements which we can use to check if the text is truncated, <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight">offsetHeight</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight">scrollHeight</a>. From Mozilla Dev Documentation,</p> <p><strong>offsetHeight</strong> :</p> <blockquote> <p>is a measurement in pixels of the elementā€™s CSS height, including any borders, padding, and horizontal scrollbars (if rendered).</p> </blockquote> <p><strong>scrollHeight</strong> :</p> <blockquote> <p>is equal to the minimum height the element would require in order to fit all the content in the viewport without using a vertical scrollbar.</p> </blockquote> <p>Hereā€™s <a href="https://stackoverflow.com/a/22675563/1901264">a good answer</a> from StackOverflow on what is offsetHeight and scrollHeight. Hereā€™s the visualized summary :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/25-how-to-truncate-long-text-and-show-read-more-less-button/heights.png" alt="heights" /></p> <p>scrollHeight is the total scrollable content height, and offsetHeight is the visible height on the screen. For an overflow view, the scrollHeight is larger than offsetHeight.</p> <p>We can deduce that if the scrollHeight is larger than the offsetHeight, then the element is truncated.</p> <p>Hereā€™s the javascript code to check if an element is truncated :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">p</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">element</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">&lt;</span> <span class="nx">element</span><span class="p">.</span><span class="nx">scrollHeight</span> <span class="o">||</span> <span class="nx">element</span><span class="p">.</span><span class="nx">offsetWidth</span> <span class="o">&lt;</span> <span class="nx">element</span><span class="p">.</span><span class="nx">scrollWidth</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// your element has overflow and truncated</span> <span class="c1">// show read more / read less button</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="c1">// your element doesn't overflow (not truncated)</span> <span class="p">}</span> </code></pre></div></div> <p>This code should be run after the paragraph element is rendered on the screen, you can put this in a <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> tag right before the body closing tag <code class="language-plaintext highlighter-rouge">&lt;/body&gt;</code>.</p> <h2 id="toggle-truncation">Toggle truncation</h2> <p>Assuming you already have the <code class="language-plaintext highlighter-rouge">.line-clamp-4</code> CSS class ready, we can toggle truncation by adding / removing this class on the paragraph element, the code for this can be put into <code class="language-plaintext highlighter-rouge">onclick</code> action of the read more / read less button:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">"document.querySelector('p').classList.remove('line-clamp-4')"</span><span class="nt">&gt;</span>Read more...<span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">"document.querySelector('p').classList.add('line-clamp-4')"</span><span class="nt">&gt;</span>Read less...<span class="nt">&lt;/button&gt;</span> </code></pre></div></div> <h2 id="demo">Demo</h2> <p>I have made a demo for this using Alpine.js and TailwindCSS, you can check the demo at CodePen here : <a href="https://codepen.io/cupnoodle/pen/powvObw">https://codepen.io/cupnoodle/pen/powvObw</a>. Feel free to use this in your project.</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">If you are working on an app that has user generated content (like blog posts, comments etc), there might be scenario where it is too long to display and it would make sense to truncate them and put a ā€œread moreā€ button below it.</summary>
</entry>
<entry>
<title type="html">Rails tip: use #presence to get params value, or default to a preset value</title>
<link href="https://rubyyagi.com/rails-tip-presence/" rel="alternate" type="text/html" title="Rails tip: use #presence to get params value, or default to a preset value"/>
<published>2021-08-21T08:55:00+00:00</published>
<updated>2021-08-21T08:55:00+00:00</updated>
<id>https://rubyyagi.com/rails-tip-presence</id>
<content type="html" xml:base="https://rubyyagi.com/rails-tip-presence/"><p>Hereā€™s a method I encountered recently at my work and I found it might be useful to you as well.</p> <p>There might be some attribute which you want to set to a default parameter if the user didnā€™t supply any, eg:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># if the parameters is nil or blank, we will default to french fries</span> <span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">present?</span> <span class="p">?</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">]</span> <span class="p">:</span> <span class="s2">"french fries"</span> </code></pre></div></div> <p>This one liner looks concise, but we can make it even shorter like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">presence</span> <span class="o">||</span> <span class="s2">"french fries"</span> </code></pre></div></div> <p>The <strong>presence</strong> method will return nil if the string is a blank string / empty array or nil , then the <code class="language-plaintext highlighter-rouge">||</code> (double pipe) will use the value on the right side (ā€œfrench friesā€).</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">''</span><span class="p">.</span><span class="nf">presence</span> <span class="c1"># =&gt; nil</span> </code></pre></div></div> <p>If the string is not blank, <strong>.presence</strong> will return the string itself.</p> <p>When <strong>params[:sides]</strong> == ā€œā€ :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">presence</span> <span class="o">||</span> <span class="s2">"french fries"</span> <span class="c1"># =&gt; "french fries"</span> </code></pre></div></div> <p>When <strong>params[:sides]</strong> == ā€œcornā€ ,</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sides</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:sides</span><span class="p">].</span><span class="nf">presence</span> <span class="o">||</span> <span class="s2">"french fries"</span> <span class="c1"># =&gt; "corn"</span> </code></pre></div></div> <p>Documentation : <a href="http://api.rubyonrails.org/classes/Object.html#method-i-presence">http://api.rubyonrails.org/classes/Object.html#method-i-presence</a></p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">Hereā€™s a method I encountered recently at my work and I found it might be useful to you as well.</summary>
</entry>
<entry>
<title type="html">How to get started with TailwindCSS</title>
<link href="https://rubyyagi.com/start-tailwind/" rel="alternate" type="text/html" title="How to get started with TailwindCSS"/>
<published>2021-06-24T14:44:00+00:00</published>
<updated>2021-06-24T14:44:00+00:00</updated>
<id>https://rubyyagi.com/start-tailwind</id>
<content type="html" xml:base="https://rubyyagi.com/start-tailwind/"><h1 id="how-to-get-started-with-tailwindcss">How to get started with TailwindCSS</h1> <p>This article assumes you have heard of <a href="https://tailwindcss.com">TailwindCSS</a>, and interested to try it out but have no idea where to start.</p> <h2 id="try-it-out-online-on-tailwind-play">Try it out online on Tailwind Play</h2> <p>If you just want to try out TailwindCSS and donā€™t want to install anything on your computer yet, <a href="https://play.tailwindcss.com">the official Tailwind Play</a> online playground is a good place to start!</p> <h2 id="try-it-on-your-html-file-without-fiddling-with-the-nodejs-stuff">Try it on your HTML file without fiddling with the nodeJS stuff</h2> <p>This section is for when you want to use TailwindCSS on a static website, but you donā€™t want to deal with the nodeJS NPM stuff.</p> <p>Thankfully, <a href="https://beyondco.de/blog/tailwind-jit-compiler-via-cdn">Beyond Code</a> has released TailwindCSS JIT via CDN. You can read more on how it works on the linked article here : <a href="https://beyondco.de/blog/tailwind-jit-compiler-via-cdn">https://beyondco.de/blog/tailwind-jit-compiler-via-cdn</a></p> <p>To use TailwindCSS classes on your HTML file, simply include this script in between the <code class="language-plaintext highlighter-rouge">&lt;head&gt;...&lt;/head&gt;</code> tag :</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- Include CDN JavaScript --&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://unpkg.com/tailwindcss-jit-cdn"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div></div> <p>Then you can use TailwindCSS class names on your HTML file and it will work!</p> <p>Keep in mind that this JIT javascript file is &gt; 375 KB , which is quite sizeable if you want to use it for production.</p> <blockquote> <p>The bundle size is pretty big (375kb gzipped) when compared to a production built CSS that is usually ~10kb. But some people donā€™t mind this.</p> </blockquote> <p>If you would like to compile a production TailwindCSS, you would have to deal with NPM / nodeJS (I promise it would be painless) , which will go into in the next section.</p> <h2 id="compile-and-purge-tailwindcss-into-a-css-file-for-production-use">Compile and purge TailwindCSS into a .css file for production use</h2> <p>Say you already have the HTML ready with TailwindCSS classes, and now you want to build a production ready CSS file, this is where we need to npm install some stuff and run some npm build script.</p> <p>If you havenā€™t already, open terminal, and navigate to your project folder root, and <strong>initiate npm</strong> :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm init -y </code></pre></div></div> <p>This will create a package.json file in your project folder root.</p> <p>Next, we are going to install TailwindCSS, PostCSS and AutoPrefixer package :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm install -D tailwindcss@latest postcss@latest autoprefixer@latest </code></pre></div></div> <p>Next, create a PostCSS config file on the project folder root, the file name should be <strong>postcss.config.js</strong> , and add TailwindCSS and Autoprefixer plugin into this config file.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// postcss.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">{</span> <span class="na">tailwindcss</span><span class="p">:</span> <span class="p">{},</span> <span class="na">autoprefixer</span><span class="p">:</span> <span class="p">{},</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>If you have custom configuration for TailwindCSS (eg: extension or replacement of default class/font), you can create a Tailwind config file using this command :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx tailwindcss init </code></pre></div></div> <p>This will create a <code class="language-plaintext highlighter-rouge">tailwind.config.js</code> file at the root of your project folder.</p> <p>You can customize this file to extend / replace existing settings, letā€™s open it and change the <strong>purge</strong> dictionary to include all html files :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// tailwind.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">purge</span><span class="p">:</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">./**/*.html</span><span class="dl">'</span> <span class="p">],</span> <span class="na">darkMode</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// or 'media' or 'class'</span> <span class="na">theme</span><span class="p">:</span> <span class="p">{</span> <span class="na">extend</span><span class="p">:</span> <span class="p">{},</span> <span class="p">},</span> <span class="na">variants</span><span class="p">:</span> <span class="p">{},</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">[],</span> <span class="p">}</span> </code></pre></div></div> <p>This will tell TailwindCSS to search for all .html and .js files in the project folder for TailwindCSS classes (eg: mx-4 , text-xl etc), and remove classes that did not appear in the .html and .js files, this can cut down the generated production css file later.</p> <p>Next, create a .css file if you donā€™t have one already, and insert these tailwind directive into it :</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* style.css */</span> <span class="k">@tailwind</span> <span class="n">base</span><span class="p">;</span> <span class="k">@tailwind</span> <span class="n">components</span><span class="p">;</span> <span class="k">@tailwind</span> <span class="n">utilities</span><span class="p">;</span> <span class="c">/* your custom css class/ids here */</span> <span class="nc">.custom-class</span> <span class="p">{</span> <span class="p">}</span> </code></pre></div></div> <p>These directives (eg: <code class="language-plaintext highlighter-rouge">@tailwind base</code>) will be replaced with tailwind styles later on.</p> <p>Now you can run the following line in terminal to generate the production purged Tailwind CSS file :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NODE_ENV=production npx tailwindcss -i ./style.css -c ./tailwind.config.js -o ./dist.css </code></pre></div></div> <p>This will read the original CSS file (<strong>style.css</strong>), use the config located in the <strong>tailwind.config.js</strong> file, and save the production CSS file into <strong>dist.css</strong> file (on the project folder root).</p> <p>Then you can use this production css file in the HTML <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> part like this :</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;head&gt;</span> <span class="c">&lt;!-- production compiled CSS --&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"dist.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span> <span class="nt">&lt;/head&gt;</span> </code></pre></div></div> <p>It might be tedious to type out the long command above to generate production css, especially if you need to generate it more than one time.</p> <p>To save some typing, you can move the command above into the ā€œscriptsā€ section of the <strong>package.json</strong> file :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// package.json</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">your-project-name</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">version</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1.0.0</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">description</span><span class="dl">"</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="dl">"</span><span class="s2">main</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.js</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">scripts</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">test</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">echo </span><span class="se">\"</span><span class="s2">Error: no test specified</span><span class="se">\"</span><span class="s2"> &amp;&amp; exit 1</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">css</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">NODE_ENV=production npx tailwindcss -i ./style.css -c ./tailwind.config.js -o ./dist.css</span><span class="dl">"</span> <span class="p">},</span> <span class="dl">"</span><span class="s2">keywords</span><span class="dl">"</span><span class="p">:</span> <span class="p">[],</span> <span class="dl">"</span><span class="s2">author</span><span class="dl">"</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="dl">"</span><span class="s2">license</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">ISC</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">devDependencies</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">autoprefixer</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">^10.2.6</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">postcss</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">^8.3.5</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">tailwindcss</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">^2.2.2</span><span class="dl">"</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Notice that I have added ā€œ<strong>css</strong>ā€ and the command to generate production css into the ā€œ<strong>scripts</strong>ā€ section.</p> <p>Now you can simply type <code class="language-plaintext highlighter-rouge">npm run css</code> on your terminal to generate the production css! šŸ™Œ</p> <p>I have also created a repo here : <a href="https://github.com/rubyyagi/tailwindcss-generator">https://github.com/rubyyagi/tailwindcss-generator</a> , which you can simply clone it, replace the repo with your own HTML files, and run <code class="language-plaintext highlighter-rouge">npm run css</code> to generate production CSS!</p> <h2 id="using-tailwindcss-on-ruby-on-rails">Using TailwindCSS on Ruby on Rails</h2> <p>If you are interested to use TailwindCSS on a Rails app, you can refer <a href="https://rubyyagi.com/tailwindcss2-rails6/">this article on how to install and use TailwindCSS on a Rails project</a>.</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">How to get started with TailwindCSS</summary>
</entry>
<entry>
<title type="html">Getting started with automated testing workflow and CI on Ruby</title>
<link href="https://rubyyagi.com/unit-test-start/" rel="alternate" type="text/html" title="Getting started with automated testing workflow and CI on Ruby"/>
<published>2021-06-15T11:06:00+00:00</published>
<updated>2021-06-15T11:06:00+00:00</updated>
<id>https://rubyyagi.com/unit-test-start</id>
<content type="html" xml:base="https://rubyyagi.com/unit-test-start/"><p>If you have been writing Ruby / Rails code for a while, but still canā€™t grasp on the ā€˜<strong>why</strong>ā€™ to write automated test, (eg. why do so many Ruby dev job posting requires automated test skill like Rspec, minitest etc? How does writing automated test makes me a better developer or help the company I work with? )</p> <p>or if you are not sure on how the <strong>CI</strong> (continuous integration) stuff works, this article is written to address these questions!</p> <p>This article wonā€™t go into how to write unit test, but it will let you <strong>experience what is it like working on a codebase that has automated testing in place, and how it can speed up the development flow and assure quality</strong> (ie. confirm the code works). This article assume you have basic knowledge on how to use Git and Github.</p> <p>I have written a small Ruby Sinatra app (with automated test written using Rspec, you dont have to worry about Rspec for now), you can fork it to your Github account: <a href="https://github.com/rubyyagi/cart">https://github.com/rubyyagi/cart</a> , then follow along this article to add a feature for it.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/fork.png" alt="fork" /></p> <p>After forking the repo, you can download the repo as zip or clone it to your computer. The repo is a simple shopping cart web app, which user can add items to cart and checkout, you can also try the completed online demo here : <a href="https://alpine-cart.herokuapp.com">https://alpine-cart.herokuapp.com</a></p> <p>I recommend <strong>creating a new git branch</strong> before start working on it, to make creating pull request easier in the next step. (create a new branch instead of working on master branch directly.)</p> <p>Remember to run <code class="language-plaintext highlighter-rouge">bundle install</code> after downloading the repo, to install the required gem (Sinatra and Rspec for testing).</p> <p>To run the web app, open terminal, navigate to that repo, and run <code class="language-plaintext highlighter-rouge">ruby app.rb</code> , it should spin up a Sinatra web app, which you can access in your web browser at ā€œ<strong>localhost:4567</strong>ā€</p> <p>You can add items to the cart, then click ā€˜<strong>Checkout</strong>ā€™ , which it then will show the total (you will implement the discount part later).</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/example.gif" alt="checkout" /></p> <p>Open the repo with your favorite code editor, you will notice a folder named ā€œ<strong>spec</strong>ā€ :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/specs.png" alt="specs" /></p> <p>This folder contains the test script I have pre written, there are two test files inside (bulk_discount_spec.rb and cart_spec.rb) , they are used to verify if the <strong>bulk_discount</strong> logic and <strong>cart</strong> logic is implemented correctly, you donā€™t have to make changes on these files.</p> <p>To run these test, open terminal, navigate to this repo, and run <code class="language-plaintext highlighter-rouge">rspec</code> (type ā€˜rspecā€™ and press enter).</p> <p>You should see some test cases fail like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/fail_test.png" alt="test fail" /></p> <p>This is expected, as the <strong>bulk_discount.rb</strong> file (it contains logic for bulk discount, located in models/bulk_discount.rb) has not been implemented yet, hence the total returned from the cart on those test are incorrect.</p> <p>For this exercise, you will need to implement the bulk discount logic in <strong>models/bulk_discount.rb</strong> , specifically inside the <strong>def apply</strong> method.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/bulk_discount.png" alt="bulk discount file" /></p> <p>The <strong>BulkDiscount</strong> class has three properties, amount (in cents), quantity_required (integer), and product_name (string).</p> <p>For example in the <strong>app.rb</strong>, we have two bulk discounts :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">discounts</span> <span class="o">=</span> <span class="p">[</span> <span class="no">BulkDiscount</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">amount: </span><span class="mi">100</span><span class="p">,</span> <span class="ss">quantity_required: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">product_name: </span><span class="s1">'apple'</span><span class="p">),</span> <span class="no">BulkDiscount</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">amount: </span><span class="mi">200</span><span class="p">,</span> <span class="ss">quantity_required: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">product_name: </span><span class="s1">'banana'</span><span class="p">)</span> <span class="p">]</span> <span class="n">cart</span> <span class="o">=</span> <span class="no">Cart</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">discounts: </span><span class="n">discounts</span><span class="p">,</span> <span class="ss">products: </span><span class="n">products</span><span class="p">)</span> </code></pre></div></div> <p>We want to give $1 (100 cents in amount) discount off the cart for every 3 Apple (product_name: ā€˜appleā€™) purchased. For example if the customer buys 7 Apple, the cart will be discounted $2 ($1 discount for every 3 apple, 6 apple = discount $2).</p> <p>For the code in <strong>app.rb</strong> , we want to give bulk discount for apple and banana. Say if the cart has 7 apple and 5 banana, it will be discounted with $6 ($1 x 2 + $2 x 2 = $6, discount of $1 x 2 for apple, $2 x 2 for banana).</p> <p>Back to <strong>bulk_discount.rb</strong>, the <strong>apply</strong> method accepts two parameter, first parameter <strong>total</strong> , which is the total of the carts, in cents (eg: $4 is 400 cents), before the current bulk discount is applied.</p> <p>The second parameter <strong>products</strong> in an array of product (you can check <strong>models/product.rb</strong>) that is currently in the cart.</p> <p>The <strong>apply</strong> method should go through all of the product in the products array, and check if there is product matching the <strong>product_name</strong> of the BulkDiscount, and then check if the quantity of the product is larger than the <strong>quantity_required</strong> , and calculate the amount to be deducted and save it to <strong>amount_to_deduct</strong>.</p> <p>Then the <strong>apply</strong> method will return the discounted total (total - amount_to_deduct).</p> <p>Each time you finish writing the code, you can run <code class="language-plaintext highlighter-rouge">rspec</code> to check if the test cases pass. If all of them pass, it means your implementation is correct.</p> <p>This feedback loop process should be a lot faster than opening the web browser and navigate to the web app, add a few products in cart, click ā€˜checkoutā€™ and see if the discount is displayed and if the displayed discount amount is correct.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/rspec.gif" alt="rspec" /></p> <p>Compared to opening web browser and press ā€˜add to cartā€™ and click ā€˜checkoutā€™ every time you want to test if your code is correct :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/example.gif" alt="example" /></p> <p>With automated test, you can check if an implementation is correct a lot faster than testing it manually.</p> <p>And with automated test in place, you can be confident that you didnā€™t break existing feature when you add new feature or make changes in the future, which makes refactoring safe.</p> <p>(Please work on the exercise yourself first before checking the answer šŸ™ˆ)</p> <p>You can compare your implementation with my implementation here (<a href="https://github.com/rubyyagi/cart/blob/answer/models/bulk_discount.rb">https://github.com/rubyyagi/cart/blob/answer/models/bulk_discount.rb</a>) for the bulk discount.</p> <h2 id="continuous-integration-ci">Continuous Integration (CI)</h2> <p>Once you have finished implementing the bulk discount code part and ensure the test passes, you can push the commit to your forked repo.</p> <p>And then you can create a new pull request from your branch to the original repo (the <strong>rubyyagi/cart</strong>) like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/pr1.png" alt="pull request" /></p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/pr2.png" alt="pull request 2" /></p> <p>After creating the pull request, you will notice that Github (Github Actions) automatically runs the test (same like when you run <code class="language-plaintext highlighter-rouge">rspec</code>) :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/ci1.png" alt="CI" /></p> <p>Once the test has passed, it will show this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/ci2.png" alt="CI 2" /></p> <p>Or if there is failing test, it will show ā€œfailā€.</p> <p>The idea is that when you submit a new pull request (containing new feature or fixes), an automated test suite will be run on some external service (Github Actions for this example, but thereā€™s also other CI service provider like CircleCI, SemaphoreCI etc). And you can configure the repository such that only pull request with passing test can be merged.</p> <p>You can take a look at <a href="https://github.com/rails/rails/pulls">pull requests in the official Rails repository</a>, each pull request will trigger an automated test run, to ensure they donā€™t break existing Rails feature, this inspires confidence on the overall code quality.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/ci3.png" alt="ci3" /></p> <p>In my understanding, <strong>Continuous Integration</strong> (CI) is the process of</p> <ol> <li>Create new branch to work on new feature or fixes</li> <li>Create pull request from the new branch to the main / master branch</li> <li>Running automated test suite (and also linting, security checks) on the branch to ensure everything works</li> <li>A senior dev / team lead/ repository owner will code review the new branch, and request changes if required, else they can approve the new branch and merge it to master/ main branch</li> <li>The new branch is merged to master/ main branch</li> <li>The master / main branch is deployed (to Heroku or AWS or some other service) manually or automatically, or if it is a library , push to RubyGems.org</li> </ol> <p>If you are using Heroku, you can configure to automatic deploy new merge to master / main branch only if the CI (automated test suite) passes :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/cd1.png" alt="cd" /></p> <p><img src="https://rubyyagi.s3.amazonaws.com/22-unit-test-start/cd2.png" alt="cd2" /></p> <p>I have already setup the automated test suite to run on Github actions for this repository, you can check the workflow file here if you are interested : <a href="https://github.com/rubyyagi/cart/blob/master/.github/workflows/test.yml">https://github.com/rubyyagi/cart/blob/master/.github/workflows/test.yml</a> .</p> <p>Different CI usually requires different configuration file on the repository, eg: for CircleCI, you need to write the configuration file in <code class="language-plaintext highlighter-rouge">/.circleci/config.yml</code> in your repository, these config are vendor specific.</p> <p>Hope this article has made you understand the importance of automated testing, and how it can speed up development process and inspire confidence in code quality!</p> <h2 id="further-reading">Further Reading</h2> <p><a href="https://rubyyagi.com/intro-rspec-capybara-testing/">Introduction to Rails testing with RSpec and Capybara</a></p> <p><a href="https://rubyyagi.com/rspec-request-spec/">Introduction to API testing using RSpec and Request Spec</a></p> <p><a href="https://www.codewithjason.com/rails-testing-guide/">The beginner guide to Rails testing, written by Jason Swett</a></p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">If you have been writing Ruby / Rails code for a while, but still canā€™t grasp on the ā€˜whyā€™ to write automated test, (eg. why do so many Ruby dev job posting requires automated test skill like Rspec, minitest etc? How does writing automated test makes me a better developer or help the company I work with? )</summary>
</entry>
<entry>
<title type="html">How to call methods dynamically using string of method name</title>
<link href="https://rubyyagi.com/call-dynamic-method-name/" rel="alternate" type="text/html" title="How to call methods dynamically using string of method name"/>
<published>2021-02-15T14:40:00+00:00</published>
<updated>2021-02-15T14:40:00+00:00</updated>
<id>https://rubyyagi.com/call-dynamic-method-name</id>
<content type="html" xml:base="https://rubyyagi.com/call-dynamic-method-name/"><p>I was working on a feature for my Rails app, which allows merchant to set fulfillment times on each day like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/20-call-dynamic-method-name/fulfillment_time.png" alt="fulfillment_time" /></p> <p>The main feature is that once an order is placed, the customer canā€™t cancel the placed order after the fulfillment time starts on that day (or on tomorrow).</p> <p>The fulfillment start times are saved in different columns like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">create_table</span> <span class="s2">"fulfillments"</span><span class="p">,</span> <span class="ss">force: :cascade</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"monday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"tuesday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"wednesday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"thursday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"friday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"saturday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">time</span> <span class="s2">"sunday_start_time"</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"2000-01-01 00:00:00"</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="k">end</span> </code></pre></div></div> <p>To check if the fulfillment time has started, my app will get the current day of week using <code class="language-plaintext highlighter-rouge">DateTime.current.strftime('%A').downcase</code> . If today is wednesday, then it will return <code class="language-plaintext highlighter-rouge">wednesday</code> .</p> <p>The question now is that <strong>how do I access value of different column based on different day of week</strong>? Say I want to get the value of <code class="language-plaintext highlighter-rouge">thursday_start_time</code> if today is <strong>thursday</strong> ; or the value of <code class="language-plaintext highlighter-rouge">sunday_start_time</code> is today is <strong>sunday</strong> .</p> <p>Fortunately, Rubyā€™s metaprogramming feature allows us to call methods dynamically by just passing the method name into <strong>public_send(method_name)</strong> or <strong>send(method_name)</strong> .</p> <p>Say we have a class like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Duck</span> <span class="k">def</span> <span class="nf">make_noise</span> <span class="nb">puts</span> <span class="s2">"quack"</span> <span class="k">end</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">jump</span> <span class="nb">puts</span> <span class="s2">"can't jump"</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>We can call the <strong>make_noise</strong> method by calling <code class="language-plaintext highlighter-rouge">Duck.new.public_send("make_noise")</code>, this is equivalent to calling Duck.new.make_noise .</p> <p>For the <strong>jump</strong> method, we would need to use <strong>send</strong> instead, as it is a private method, <code class="language-plaintext highlighter-rouge">Duck.new.send("jump")</code></p> <p>For the fulfillment start time, we can then pass in the day dynamically like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># this will return 'monday' if today is monday, or 'tuesday' if today is tuesday etc..</span> <span class="n">day_of_week</span> <span class="o">=</span> <span class="no">DateTime</span><span class="p">.</span><span class="nf">current</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%A'</span><span class="p">).</span><span class="nf">downcase</span> <span class="c1"># if today is monday, it will call 'monday_start_time', etc..</span> <span class="n">start_time</span> <span class="o">=</span> <span class="n">fulfillment</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">day_of_week</span><span class="si">}</span><span class="s2">_start_time"</span><span class="p">)</span> </code></pre></div></div> <p>Sweet!</p> <p>Alternatively, you can also pass in symbol instead of string : <strong>public_send(:make_noise)</strong> .</p> <h2 id="passing-parameters-to-public_send--send">Passing parameters to public_send / send</h2> <p>If you would like to send parameters to the method, you can pass them after the method name :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Animal</span> <span class="k">def</span> <span class="nf">make_noise</span><span class="p">(</span><span class="n">noise</span><span class="p">,</span> <span class="n">times</span><span class="p">)</span> <span class="mi">1</span><span class="o">..</span><span class="n">times</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="nb">puts</span> <span class="n">noise</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="no">Animal</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="s1">'make_noise'</span><span class="p">,</span> <span class="s1">'quack'</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="c1"># quack</span> <span class="c1"># quack</span> <span class="c1"># quack</span> </code></pre></div></div> <script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script> <h2 id="reference">Reference</h2> <p>Ruby Docā€™s <a href="https://ruby-doc.org/core-3.0.0/Object.html#method-i-public_send">public_send documentation</a></p> <p>Ruby Docā€™s <a href="https://ruby-doc.org/core-3.0.0/Object.html#method-i-send">send documentation</a></p></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">I was working on a feature for my Rails app, which allows merchant to set fulfillment times on each day like this :</summary>
</entry>
<entry>
<title type="html">How to run tests in parallel in Github Actions</title>
<link href="https://rubyyagi.com/how-to-run-tests-in-parallel-in-github-actions/" rel="alternate" type="text/html" title="How to run tests in parallel in Github Actions"/>
<published>2021-01-25T17:22:00+00:00</published>
<updated>2021-01-25T17:22:00+00:00</updated>
<id>https://rubyyagi.com/how-to-run-tests-in-parallel-in-github-actions</id>
<content type="html" xml:base="https://rubyyagi.com/how-to-run-tests-in-parallel-in-github-actions/"><h1 id="how-to-run-tests-in-parallel-in-github-actions">How to run tests in parallel in Github Actions</h1> <p>The company I work for has recently switched to Github Actions for CI, as they offer 2000 minutes per month which is quite adequate for our volume. We have been running tests in parallel (2 instances) on the previous CI to save time, and we wanted to do the same on Github Actions.</p> <p>Github Actions provides <a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix">strategy matrix</a> which lets you run tests in parallel, with different configuration for each matrix.</p> <p>For example with a matrix strategy below :</p> <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span> <span class="na">test</span><span class="pi">:</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Test</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">strategy</span><span class="pi">:</span> <span class="na">matrix</span><span class="pi">:</span> <span class="na">ruby</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">2.5.x'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">2.6.x'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">2.7.x'</span> <span class="na">activerecord</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">5.0'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">6.0'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">6.1'</span> </code></pre></div></div> <p>It will run 9 tests in parallel, 3 versions of Ruby x 3 versions of ActiveRecord = 9 tests</p> <p><img src="https://rubyyagi.s3.amazonaws.com/19-parallel-test-github-actions/9tests.png" alt="9 tests" /></p> <p>One downside of Github Actions is that they donā€™t have built-in split tests function to split test files across different CI instances, which many other CI have. For example CircleCI provides split testing command like this :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run rspec in parallel, and split the tests by timings</span> - run: <span class="nb">command</span>: | <span class="nv">TESTFILES</span><span class="o">=</span><span class="si">$(</span>circleci tests glob <span class="s2">"spec/**/*_spec.rb"</span> | circleci tests <span class="nb">split</span> <span class="nt">--split-by</span><span class="o">=</span>timings<span class="si">)</span> bundle <span class="nb">exec </span>rspec <span class="nv">$TESTFILES</span> </code></pre></div></div> <p>With split testing, we can run different test files on different CI instances to reduce the test execution time on each instance.</p> <p>Fortunately, we can write our own script to split the test files in Ruby.</p> <p>Before we proceed to writing split test script, lets configure the Github Actions workflow to run our tests in parallel.</p> <h1 id="configuring-github-actions-workflow-to-run-test-in-parallel">Configuring Github Actions workflow to run test in parallel</h1> <p>Letā€™s configure our Github Actions workflow to run test in parallel :</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/test.yml</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test</span> <span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span> <span class="na">jobs</span><span class="pi">:</span> <span class="na">test</span><span class="pi">:</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">strategy</span><span class="pi">:</span> <span class="na">fail-fast</span><span class="pi">:</span> <span class="no">false</span> <span class="na">matrix</span><span class="pi">:</span> <span class="c1"># Set N number of parallel jobs you want to run tests on.</span> <span class="c1"># Use higher number if you have slow tests to split them on more parallel jobs.</span> <span class="c1"># Remember to update ci_node_index below to 0..N-1</span> <span class="na">ci_node_total</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">2</span><span class="pi">]</span> <span class="c1"># set N-1 indexes for parallel jobs</span> <span class="c1"># When you run 2 parallel jobs then first job will have index 0, the second job will have index 1 etc</span> <span class="na">ci_node_index</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">0</span><span class="pi">,</span> <span class="nv">1</span><span class="pi">]</span> <span class="na">env</span><span class="pi">:</span> <span class="na">BUNDLE_JOBS</span><span class="pi">:</span> <span class="m">2</span> <span class="na">BUNDLE_RETRY</span><span class="pi">:</span> <span class="m">3</span> <span class="na">BUNDLE_PATH</span><span class="pi">:</span> <span class="s">vendor/bundle</span> <span class="na">PGHOST</span><span class="pi">:</span> <span class="s">127.0.0.1</span> <span class="na">PGUSER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">PGPASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">RAILS_ENV</span><span class="pi">:</span> <span class="s">test</span> <span class="na">services</span><span class="pi">:</span> <span class="na">postgres</span><span class="pi">:</span> <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:9.6-alpine</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">5432:5432"</span><span class="pi">]</span> <span class="na">options</span><span class="pi">:</span> <span class="pi">&gt;-</span> <span class="s">--health-cmd pg_isready</span> <span class="s">--health-interval 10s</span> <span class="s">--health-timeout 5s</span> <span class="s">--health-retries 5</span> <span class="na">env</span><span class="pi">:</span> <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">bookmarker_test</span> <span class="na">steps</span><span class="pi">:</span> <span class="c1"># ...</span> </code></pre></div></div> <p>The <strong>ci_node_total</strong> means the total number of parallel instances you want to spin up during the CI process. We are using 2 instances here so the value is [2]. If you would like to use more instances, say 4 instances, then you can change the value to [4] :</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">ci_node_total</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">4</span><span class="pi">]</span> </code></pre></div></div> <p>The <strong>ci_node_index</strong> means the index of the parallel instances you spin up during the CI process, this should match the ci_node_total you have defined earlier.</p> <p>For example, if you have 2 total nodes, your <strong>ci_node_index</strong> should be [0, 1]. If you have 4 total nodes, your <strong>ci_node_index</strong> should be [0, 1, 2, 3]. This is useful for when we write the script to split the tests later.</p> <p>The <strong>fail-fast: false</strong> means that we want to continue running the test on other instances even if there is failing test on one of the instance. The default value for fail-fast is true if we didnā€™t set it, which will stop all instances if there is even one failing test on one instance. Setting fail-fast: false would allow all instances to finish run their tests, and we can see which test has failed later on.</p> <p>Next, we add the testing steps in the workflow :</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">steps</span><span class="pi">:</span> <span class="c1"># ... Ruby / Database setup...</span> <span class="c1"># ...</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Make bin/ci executable</span> <span class="na">run</span><span class="pi">:</span> <span class="s">chmod +x ./bin/ci</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Rspec test</span> <span class="na">id</span><span class="pi">:</span> <span class="s">rspec-test</span> <span class="na">env</span><span class="pi">:</span> <span class="c1"># Specifies how many jobs you would like to run in parallel,</span> <span class="c1"># used for partitioning</span> <span class="na">CI_NODE_TOTAL</span><span class="pi">:</span> <span class="s">${{ matrix.ci_node_total }}</span> <span class="c1"># Use the index from matrix as an environment variable</span> <span class="na">CI_NODE_INDEX</span><span class="pi">:</span> <span class="s">${{ matrix.ci_node_index }}</span> <span class="na">continue-on-error</span><span class="pi">:</span> <span class="no">true</span> <span class="na">run </span><span class="pi">:</span> <span class="pi">|</span> <span class="s">./bin/ci</span> </code></pre></div></div> <p>We pass the <strong>ci_node_total</strong> and <strong>ci_node_index</strong> variables into the environment variables (<strong>env:</strong> ) for the test step.</p> <p>The <strong>continue-on-error: true</strong> will instruct the workflow to continue to the next step even if there is error on the test step (ie. when any of the test fails).</p> <p>And lastly we want to run the custom test script that split the test files and run them <strong>./bin/ci</strong> , remember to make this file executable in the previous step.</p> <p>We will look into how to write the <strong>bin/ci</strong> in the next section.</p> <h2 id="preparing-the-ruby-script-to-split-test-files-and-run-them">Preparing the ruby script to split test files and run them</h2> <p>In this section, we will write the <strong>bin/ci</strong> file.</p> <p>First, create a file named ā€œ<strong>ci</strong>ā€ (without file extension), and place it in the ā€œ<strong>bin</strong>ā€ folder inside your Rails app like this : <img src="https://rubyyagi.s3.amazonaws.com/19-parallel-test-github-actions/ci_file.png" alt="ci_file_location" /></p> <p>Hereā€™s the code for the <strong>ci</strong> file :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span> <span class="n">tests</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">[</span><span class="s2">"spec/**/*_spec.rb"</span><span class="p">].</span> <span class="nf">sort</span><span class="o">.</span> <span class="c1"># Add randomization seed based on SHA of each commit</span> <span class="n">shuffle</span><span class="p">(</span><span class="ss">random: </span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'GITHUB_SHA'</span><span class="p">].</span><span class="nf">to_i</span><span class="p">(</span><span class="mi">16</span><span class="p">))).</span> <span class="nf">select</span><span class="p">.</span> <span class="nf">with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">el</span><span class="p">,</span> <span class="n">i</span><span class="o">|</span> <span class="n">i</span> <span class="o">%</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"CI_NODE_TOTAL"</span><span class="p">].</span><span class="nf">to_i</span> <span class="o">==</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"CI_NODE_INDEX"</span><span class="p">].</span><span class="nf">to_i</span> <span class="k">end</span> <span class="nb">exec</span> <span class="s2">"bundle exec rspec </span><span class="si">#{</span><span class="n">tests</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Dir["spec/**/*_spec.rb"]</code> will return an array of path of test files, which are located inside the <strong>spec</strong> folder , and filename ends with <strong>_spec.rb</strong> . (eg: <em>[ā€œspec/system/bookmarks/create_spec.rbā€, ā€œspec/system/bookmarks/update_spec.rbā€]</em>) .</p> <p>You can change this to <code class="language-plaintext highlighter-rouge">Dir["test/**/*_test.rb"]</code> if you are using minitest instead of Rspec.</p> <p><strong>sort</strong> will sort the array of path of test files in alphabetical order.</p> <p><strong>shuffle</strong> will then randomize the order of the array, using the seed provided in the <strong>random</strong> parameter, so that the randomized order will differ based on the <strong>random</strong> parameter we passed in.</p> <p><strong>ENV[ā€˜GITHUB_SHAā€™]</strong> is the SHA hash of the commit that triggered the Github Actions workflow, which available as environment variables in Github Actions.</p> <p>As <strong>Random.new</strong> only accept integer for the seed parameter, we have to convert the SHA hash (in hexadecimal string, eg: fe5300b3733d69f0a187a5f3237eb05bb134e341 ) into integer using hexadecimal as base (<strong>.to_i(16)</strong>).</p> <p>Then we select the test files that will be run using <strong>select.with_index</strong>. For each CI instance, we will select the test files from the array, which the remainder of the index divided by total number of nodes equals to the CI instance index.</p> <p>For example, say CI_NODE_TOTAL = 2, CI_NODE_INDEX = 1, and we have four test files :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/19-parallel-test-github-actions/explanation.png" alt="explanation" /></p> <p>The test files will be split as shown above.</p> <p>The downside of this split is that the tests will not be splitted based on the duration of the test, which might make some instance running longer than the others. CircleCI does store historical data of each tests duration and can split the test files based on timing, hopefully Github Actions will implement this feature in the future.</p> <h2 id="example-repository">Example repository</h2> <p>I have created an example Rails repository with parallel tests for Github Actions here : <a href="https://github.com/rubyyagi/bookmarker">https://github.com/rubyyagi/bookmarker</a></p> <p>You can check out the <a href="https://github.com/rubyyagi/bookmarker/blob/main/.github/workflows/test.yml">full workflow .yml file here</a> , the <a href="https://github.com/rubyyagi/bookmarker/blob/main/bin/ci">bin/ci file here</a> , and an <a href="https://github.com/rubyyagi/bookmarker/actions/runs/507483357">example test run here</a>.</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">How to run tests in parallel in Github Actions</summary>
</entry>
<entry>
<title type="html">Devise only allow one session per user at the same time</title>
<link href="https://rubyyagi.com/devise-only-allow-one-session-per-user-at-the-same-time/" rel="alternate" type="text/html" title="Devise only allow one session per user at the same time"/>
<published>2021-01-16T15:42:00+00:00</published>
<updated>2021-01-16T15:42:00+00:00</updated>
<id>https://rubyyagi.com/devise-only-allow-one-session-per-user-at-the-same-time</id>
<content type="html" xml:base="https://rubyyagi.com/devise-only-allow-one-session-per-user-at-the-same-time/"><p>Sometimes for security purpose, or you just dont want users to share their accounts, it is useful to implement a check to ensure that only one login (session) is allowed per user at the same time.</p> <p>To check if there is only one login, we will need a column to store information of the current login information. We can use a string column to store a token, and this token will be randomly generated each time the user is logged in, and then we compare if the current sessionā€™s login token is equal to this token.</p> <p>Assuming your users info are stored in the <strong>users</strong> table (you can change to other name for the command below), create a ā€œcurrent_login_tokenā€ column for the table :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g migration AddCurrentLoginTokenToUsers current_login_token:string </code></pre></div></div> <p>This would generate migration file below :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AddCurrentLoginTokenToUsers</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.1</span><span class="p">]</span> <span class="k">def</span> <span class="nf">change</span> <span class="n">add_column</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">:current_login_token</span><span class="p">,</span> <span class="ss">:string</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Now run <strong>rails db:migrate</strong> to run this migration.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails db:migrate </code></pre></div></div> <p>Next, we would need to override the <strong>create</strong> method of SessionsController from Devise, so that we can generate a random string for the <strong>current_login_token</strong> column each time the user signs in.</p> <p>If you donā€™t already have a custom SessionsController created already, you can generate one using this command :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g devise:controllers users -c=sessions </code></pre></div></div> <p>(Assuming the model name is <strong>user</strong>)</p> <p>The -c flag means the controller to generate, as we only need to generate SessionsController here. (You can read more about the <a href="https://github.com/heartcombo/devise/wiki/Tool:-Generate-and-customize-controllers">devise generator command here</a>)</p> <p>This will generate a controller in <strong>app/controllers/users/sessions_controller.rb</strong> , and this will override the default Deviseā€™s SessionsController.</p> <p>Next, we are going to override the ā€œ<strong>create</strong>ā€ method (the method that will be called when user sign in successfully) inside <strong>app/controllers/users/sessions_controller.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Users::SessionsController</span> <span class="o">&lt;</span> <span class="no">Devise</span><span class="o">::</span><span class="no">SessionsController</span> <span class="n">skip_before_action</span> <span class="ss">:check_concurrent_session</span> <span class="c1"># POST /resource/sign_in</span> <span class="k">def</span> <span class="nf">create</span> <span class="k">super</span> <span class="c1"># after the user signs in successfully, set the current login token</span> <span class="n">set_login_token</span> <span class="k">end</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">set_login_token</span> <span class="n">token</span> <span class="o">=</span> <span class="no">Devise</span><span class="p">.</span><span class="nf">friendly_token</span> <span class="n">session</span><span class="p">[</span><span class="ss">:login_token</span><span class="p">]</span> <span class="o">=</span> <span class="n">token</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">current_login_token</span> <span class="o">=</span> <span class="n">token</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">save</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>We use <a href="https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise.rb#L492">Devise.friendly_token</a> to generate a random string for the token, then store it into the current session (<strong>session[:login_token]</strong>) and also save to the current userā€™s <strong>current_login_token</strong> column.</p> <p>The <strong>skip_before_action :check_concurrent_session</strong> will be explained later, put it there for now.</p> <p>Next, we will edit the <strong>application_controller.rb</strong> file (or whichever controller where most of the controllers that require authorization are inherited from).</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span> <span class="c1"># perform the check before each controller action are executed</span> <span class="n">before_action</span> <span class="ss">:check_concurrent_session</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">check_concurrent_session</span> <span class="k">if</span> <span class="n">already_logged_in?</span> <span class="c1"># sign out the previously logged in user, only left the newly login user</span> <span class="n">sign_out_and_redirect</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">already_logged_in?</span> <span class="c1"># if current user is logged in, but the user login token in session </span> <span class="c1"># doesn't match the login token in database,</span> <span class="c1"># which means that there is a new login for this user</span> <span class="n">current_user</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="p">(</span><span class="n">session</span><span class="p">[</span><span class="ss">:login_token</span><span class="p">]</span> <span class="o">==</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">current_login_token</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>As we have wrote <strong>before_action :check_concurrent_session</strong> in the application_controller.rb , all controllers inherited from this will run the <strong>check_concurrent_session</strong> method before the controller action method are executed, this will check if there is any new login session initiated for the same user.</p> <p>We want to exempt this check when user is on the login part, ie. sessions#create , hence we put a <strong>skip_before_action :check_concurrent_session</strong> to skip this check for the Users::SessionController class earlier.</p> <p>Hereā€™s a demo video of this code :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/18-devise-limit-one-user/single-login-demo.gif" alt="demo" /></p> <p>Notice that after the second login attempt, the previously logged in user gets logged out after clicking a link (executing a controller action).</p> <script async="" data-uid="4776ba93ea" src="https://rubyyagi.ck.page/4776ba93ea/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">Sometimes for security purpose, or you just dont want users to share their accounts, it is useful to implement a check to ensure that only one login (session) is allowed per user at the same time.</summary>
</entry>
<entry>
<title type="html">How to install TailwindCSS 2 on Rails 6</title>
<link href="https://rubyyagi.com/tailwindcss2-rails6/" rel="alternate" type="text/html" title="How to install TailwindCSS 2 on Rails 6"/>
<published>2020-12-10T00:00:00+00:00</published>
<updated>2020-12-10T00:00:00+00:00</updated>
<id>https://rubyyagi.com/tailwindcss2-rails6</id>
<content type="html" xml:base="https://rubyyagi.com/tailwindcss2-rails6/"><p><a href="https://blog.tailwindcss.com/tailwindcss-v2">TailwindCSS v2.0</a> was released on 19 November 2020 šŸŽ‰, but it had a few changes that requires some tweak to install and use it on Rails 6, such as the requirement of PostCSS 8 and Webpack 5. As of current (10 December 2020), Rails 6 comes with webpack 4 and PostCSS 7, which doesnā€™t match the requirement of TailwindCSS v2.</p> <p>If we install TailwindCSS v2 directly on a Rails 6 app using <code class="language-plaintext highlighter-rouge">yarn add tailwindcss </code>, we might get a error (PostCSS plugin tailwindcss requires PostCSS 8) like this when running the server :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/tailwind-postcss-error.png" alt="postcss8 error" /></p> <p><br /></p> <p>This tutorial will guide you on <strong>how to install and use TailwindCSS 2 on Rails 6 using new Webpacker version(v5+)</strong>. If webpack / webpacker v5+ breaks your current Rails app javascript stuff, I recommend using the compability build of Tailwind <a href="https://rubyyagi.com/tailwind-css-on-rails-6-intro/#installing-tailwind-css-on-your-rails-app">following this tutorial</a>.</p> <h2 id="update-webpacker-gem-and-npm-package">Update webpacker gem and npm package</h2> <p>Currently the latest <a href="https://www.npmjs.com/package/@rails/webpacker">Webpacker NPM package</a> is not up to date to the <a href="https://github.com/rails/webpacker">master branch of Webpacker repository</a>, we need to use the Github master version until Rails team decide to push the new version of webpacker into NPM registry.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/webpacker_release.png" alt="webpacker release" /></p> <p>To do this, we need to uninstall the current webpacker in our Rails app :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn remove @rails/webpacker </code></pre></div></div> <p>And install the latest webpacker directly from Github (<strong>notice without the ā€œ@ā€</strong>) :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add rails/webpacker </code></pre></div></div> <p>Hereā€™s the difference in package.json :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/package_compare.png" alt="package.json" /></p> <p>Next, we need to do the same for the Webpacker gem as well, as they havenā€™t push the latest webpacker code in Github to RubyGems registry.</p> <p>In your <strong>Gemfile</strong>, change the webpacker gem from :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'webpacker'</span><span class="p">,</span> <span class="s1">'~&gt; 4.0'</span> </code></pre></div></div> <p><strong>to:</strong></p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"webpacker"</span><span class="p">,</span> <span class="ss">github: </span><span class="s2">"rails/webpacker"</span> </code></pre></div></div> <p>then run <code class="language-plaintext highlighter-rouge">bundle install</code> to install the gem from its Github repository. You should see the webpacker gem is version 5+ now :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/17-tailwindcss2-rails6/webpacker-master.png" alt="webpacker master" /></p> <h2 id="install-tailwind-css-2">Install Tailwind CSS 2</h2> <p>After updating the webpacker gem, we can install TailwindCSS (and its peer dependency) using yarn :</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add tailwindcss postcss autoprefixer </code></pre></div></div> <p>We can use webpacker for handling Tailwind CSS in our Rails app, create a folder named <strong>stylesheets</strong> inside <strong>app/javascript</strong> (the full path would be <strong>app/javascript/stylesheets</strong>).</p> <p>Inside the app/javascript/stylesheets folder, create a file and name it ā€œ<strong>application.scss</strong>ā€, then import Tailwind related CSS inside this file :</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* application.scss */</span> <span class="k">@import</span> <span class="s1">"tailwindcss/base"</span><span class="p">;</span> <span class="k">@import</span> <span class="s1">"tailwindcss/components"</span><span class="p">;</span> <span class="k">@import</span> <span class="s1">"tailwindcss/utilities"</span><span class="p">;</span> </code></pre></div></div> <p>The file structure looks like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/6-tailwind-intro/tailwind_import.png" alt="application scss file structure" /></p> <p>Then in <strong>app/javascript/packs/application.js</strong> , require the application.scss :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/packs/application.js</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@rails/ujs</span><span class="dl">"</span><span class="p">).</span><span class="nx">start</span><span class="p">()</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbolinks</span><span class="dl">"</span><span class="p">).</span><span class="nx">start</span><span class="p">()</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">stylesheets/application.scss</span><span class="dl">"</span><span class="p">)</span> </code></pre></div></div> <p>Next, we have to add <strong>stylesheet_pack_tag</strong> that reference the app/javascript/stylesheets/application.scss file in the layout file (app/views/layouts/application.html.erb). So the layout can import the stylesheet there.</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/layouts/application.html.erb--&gt;</span> <span class="cp">&lt;!DOCTYPE html&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>Bootstraper<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">csrf_meta_tags</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">csp_meta_tag</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">stylesheet_link_tag</span> <span class="err">'</span><span class="na">application</span><span class="err">',</span> <span class="na">media:</span> <span class="err">'</span><span class="na">all</span><span class="err">',</span> <span class="err">'</span><span class="na">data-turbolinks-track</span><span class="err">'</span><span class="na">:</span> <span class="err">'</span><span class="na">reload</span><span class="err">'</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="c">&lt;!-- This refers to app/javascript/stylesheets/application.scss--&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">stylesheet_pack_tag</span> <span class="err">'</span><span class="na">application</span><span class="err">',</span> <span class="err">'</span><span class="na">data-turbolinks-track</span><span class="err">'</span><span class="na">:</span> <span class="err">'</span><span class="na">reload</span><span class="err">'</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">javascript_pack_tag</span> <span class="err">'</span><span class="na">application</span><span class="err">',</span> <span class="err">'</span><span class="na">data-turbolinks-track</span><span class="err">'</span><span class="na">:</span> <span class="err">'</span><span class="na">reload</span><span class="err">'</span> <span class="err">%</span><span class="nt">&gt;</span> <span class="nt">&lt;/head&gt;</span> .... </code></pre></div></div> <h2 id="add-tailwindcss-to-postcss-config">Add TailwindCSS to PostCSS config</h2> <p>Next, we will add TailwindCSS into the <strong>postcss.config.js</strong> file (located in your Rails app root) :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// postcss.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">tailwindcss</span><span class="dl">"</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-import</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-flexbugs-fixes</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-preset-env</span><span class="dl">'</span><span class="p">)({</span> <span class="na">autoprefixer</span><span class="p">:</span> <span class="p">{</span> <span class="na">flexbox</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-2009</span><span class="dl">'</span> <span class="p">},</span> <span class="na">stage</span><span class="p">:</span> <span class="mi">3</span> <span class="p">})</span> <span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>If you are not using the default file name for TailwindCSS configuration file (tailwind.config.js) , you need to specify it after the tailwindcss require :</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// postcss.config.js</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span> <span class="c1">// if you are using non default config filename</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">tailwindcss</span><span class="dl">"</span><span class="p">)(</span><span class="dl">"</span><span class="s2">tailwind.config.js</span><span class="dl">"</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-import</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-flexbugs-fixes</span><span class="dl">'</span><span class="p">),</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">postcss-preset-env</span><span class="dl">'</span><span class="p">)({</span> <span class="na">autoprefixer</span><span class="p">:</span> <span class="p">{</span> <span class="na">flexbox</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-2009</span><span class="dl">'</span> <span class="p">},</span> <span class="na">stage</span><span class="p">:</span> <span class="mi">3</span> <span class="p">})</span> <span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>Now we can use the Tailwind CSS classes on our HTML elements!</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1&gt;</span>This is static#index<span class="nt">&lt;/h1&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"bg-red-500 w-3/4 mx-auto p-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-white text-2xl"</span><span class="nt">&gt;</span>Test test test<span class="nt">&lt;/p&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p><img src="https://rubyyagi.s3.amazonaws.com/6-tailwind-intro/tailwind_preview.png" alt="demo" /></p> <p>You can refer to <a href="https://tailwindcss.com/docs">Tailwind CSS documentation</a> for the list of classes you can use.</p> <p>For further Tailwind CSS customization and configuring PurgeCSS, you can <a href="https://rubyyagi.com/tailwind-css-on-rails-6-intro/#changing-tailwind-css-default-config">refer this post</a>.</p> <h2 id="references">References</h2> <p>Thanks Kieran for writing the <a href="https://www.kieranklaassen.com/upgrade-rails-to-tailwind-css-2/">Tailwind CSS 2 upgrade guide</a>.</p> <script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">TailwindCSS v2.0 was released on 19 November 2020 šŸŽ‰, but it had a few changes that requires some tweak to install and use it on Rails 6, such as the requirement of PostCSS 8 and Webpack 5. As of current (10 December 2020), Rails 6 comes with webpack 4 and PostCSS 7, which doesnā€™t match the requirement of TailwindCSS v2.</summary>
</entry>
<entry>
<title type="html">How to implement Rails API authentication with Devise and Doorkeeper</title>
<link href="https://rubyyagi.com/rails-api-authentication-devise-doorkeeper/" rel="alternate" type="text/html" title="How to implement Rails API authentication with Devise and Doorkeeper"/>
<published>2020-12-06T00:00:00+00:00</published>
<updated>2020-12-06T00:00:00+00:00</updated>
<id>https://rubyyagi.com/rails-api-authentication-devise-doorkeeper</id>
<content type="html" xml:base="https://rubyyagi.com/rails-api-authentication-devise-doorkeeper/"><p>Most of the time when we implement API endpoints on our Rails app, we want to limit access of these API to authorized users only, thereā€™s a few strategy for authenticating user through API, ranging from a <a href="https://github.com/gonzalo-bulnes/simple_token_authentication">simple token authentication</a> to a fullblown OAuth provider with JWT.</p> <p>In this tutorial, we will implement an OAuth provider for API authentication on the same Rails app we serve the user, using Devise and <a href="https://github.com/doorkeeper-gem/doorkeeper">Doorkeeper</a> gem.</p> <p>After this tutorial, you would be able to implement Devise sign in/sign up on Rails frontend, and Doorkeeper OAuth (login, register) on the API side for mobile app client, or a separate frontend client like React etc.</p> <p>This tutorial assume that you have some experience using Devise and your Rails app will both have a frontend UI and API for users to register and sign in. We can also use Doorkeeper to allow third party to create their own OAuth application on our own Rails app platform, but that is out of the scope of this article, as this article will focus on creating our own OAuth application for self consumption only.</p> <p><strong>Table of contents</strong></p> <ol> <li><a href="#scaffold-a-model">Scaffold a model</a></li> <li><a href="#setup-devise-gem">Setup Devise gem</a></li> <li><a href="#setup-doorkeeper-gem">Setup Doorkeeper gem</a></li> <li><a href="#customize-doorkeeper-configuration">Customize Doorkeeper configuration</a></li> <li><a href="#create-your-own-oauth-application">Create your own OAuth application</a></li> <li><a href="#how-to-login--logout-and-refresh-token-using-api">How to login , logout and refresh token using API</a></li> <li><a href="#create-api-controllers-that-require-authentication">Create API controllers that require authentication</a></li> <li><a href="#create-an-endpoint-for-user-registration">Create an endpoint for user registration</a></li> <li><a href="#revoke-user-token-manually">Revoke user token manually</a></li> <li><a href="#references">References</a></li> </ol> <h2 id="scaffold-a-model">Scaffold a model</h2> <p>Letā€™s start with some scaffolding so we can have a model, controller and view for CRUD, you can skip this section if you already have an existing Rails app.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rails</span> <span class="n">g</span> <span class="n">scaffold</span> <span class="n">bookmarks</span> <span class="n">title</span><span class="ss">:string</span> <span class="n">url</span><span class="ss">:string</span> </code></pre></div></div> <p>then in <strong>routes.rb</strong> , set the root path to ā€˜bookmarks#indexā€™. Devise requires us to set a root path in routes to work.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span> <span class="n">root</span> <span class="s1">'bookmarks#index'</span> </code></pre></div></div> <p>Now we have a sample CRUD Rails app, we can move on to the next step.</p> <h2 id="setup-devise-gem">Setup Devise gem</h2> <p>Add devise gem in the <strong>Gemfile</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span> <span class="c1"># ...</span> <span class="n">gem</span> <span class="s1">'devise'</span><span class="p">,</span> <span class="s1">'~&gt; 4.7.3'</span> </code></pre></div></div> <p>and run <code class="language-plaintext highlighter-rouge">bundle install</code> to install it.</p> <p>Next, run the Devise installation generator :</p> <p><code class="language-plaintext highlighter-rouge">rails g devise:install</code></p> <p>Then we create the user model (or any other model name you are using like admin, staff etc) using Devise :</p> <p><code class="language-plaintext highlighter-rouge">rails g devise User</code></p> <p>You can customize the devise features you want in the generated migration file, and also in the User model file.</p> <p>Then run <code class="language-plaintext highlighter-rouge">rake db:migrate</code> to create the users table.</p> <p>Now we have the Devise user set up, we can add <strong>authenticate_user!</strong> to <strong>bookmarks_controller.rb</strong> so only logged in users can view the controller now.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/bookmarks_controller.rb</span> <span class="k">class</span> <span class="nc">BookmarksController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span> <span class="n">before_action</span> <span class="ss">:authenticate_user!</span> <span class="c1"># ....</span> <span class="k">end</span> </code></pre></div></div> <p>Next we will move to the main part, which is setting up authentication for the API using Doorkeeper gem.</p> <h2 id="setup-doorkeeper-gem">Setup Doorkeeper gem</h2> <p>Add doorkeeper gem in the <strong>Gemfile</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span> <span class="c1"># ...</span> <span class="n">gem</span> <span class="s1">'doorkeeper'</span><span class="p">,</span> <span class="s1">'~&gt; 5.4.0'</span> </code></pre></div></div> <p>and run <code class="language-plaintext highlighter-rouge">bundle install</code> to install it.</p> <p>Next, run the Doorkeeper installation generator :</p> <p><code class="language-plaintext highlighter-rouge">rails g doorkeeper:install</code></p> <p>This will generate the configuration file for Doorkeeper in <strong>config/initializers/doorkeeper.rb</strong>, which we will customize later.</p> <p>Next, run the Doorkeeper migration generator :</p> <p><code class="language-plaintext highlighter-rouge">rails g doorkeeper:migration</code></p> <p>This will generate a migration file for Doorkeeper in <strong>db/migrate/ā€¦_create_doorkeeper_tables.rb</strong> .</p> <p>We will customize the migration file as we wonā€™t need all the tables / attributes generated.</p> <p>Open the <strong>ā€¦._create_doorkeeper_tables.rb</strong> migration file, then edit to make it look like below :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span> <span class="k">class</span> <span class="nc">CreateDoorkeeperTables</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.0</span><span class="p">]</span> <span class="k">def</span> <span class="nf">change</span> <span class="n">create_table</span> <span class="ss">:oauth_applications</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:uid</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="c1"># Remove `null: false` if you are planning to use grant flows</span> <span class="c1"># that doesn't require redirect URI to be used during authorization</span> <span class="c1"># like Client Credentials flow or Resource Owner Password.</span> <span class="n">t</span><span class="p">.</span><span class="nf">text</span> <span class="ss">:redirect_uri</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:scopes</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s1">''</span> <span class="n">t</span><span class="p">.</span><span class="nf">boolean</span> <span class="ss">:confidential</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="kp">true</span> <span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span> <span class="ss">null: </span><span class="kp">false</span> <span class="k">end</span> <span class="n">add_index</span> <span class="ss">:oauth_applications</span><span class="p">,</span> <span class="ss">:uid</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span> <span class="n">create_table</span> <span class="ss">:oauth_access_tokens</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:resource_owner</span><span class="p">,</span> <span class="ss">index: </span><span class="kp">true</span> <span class="c1"># Remove `null: false` if you are planning to use Password</span> <span class="c1"># Credentials Grant flow that doesn't require an application.</span> <span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:application</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:refresh_token</span> <span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:expires_in</span> <span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:revoked_at</span> <span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:created_at</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:scopes</span> <span class="c1"># The authorization server MAY issue a new refresh token, in which case</span> <span class="c1"># *the client MUST discard the old refresh token* and replace it with the</span> <span class="c1"># new refresh token. The authorization server MAY revoke the old</span> <span class="c1"># refresh token after issuing a new refresh token to the client.</span> <span class="c1"># @see https://tools.ietf.org/html/rfc6749#section-6</span> <span class="c1">#</span> <span class="c1"># Doorkeeper implementation: if there is a `previous_refresh_token` column,</span> <span class="c1"># refresh tokens will be revoked after a related access token is used.</span> <span class="c1"># If there is no `previous_refresh_token` column, previous tokens are</span> <span class="c1"># revoked as soon as a new access token is created.</span> <span class="c1">#</span> <span class="c1"># Comment out this line if you want refresh tokens to be instantly</span> <span class="c1"># revoked after use.</span> <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:previous_refresh_token</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">""</span> <span class="k">end</span> <span class="n">add_index</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span> <span class="n">add_index</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:refresh_token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span> <span class="n">add_foreign_key</span><span class="p">(</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:oauth_applications</span><span class="p">,</span> <span class="ss">column: :application_id</span> <span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>The modification I did on the migration file :</p> <ol> <li>Remove <strong>null: false</strong> on the <strong>redirect_uri</strong> for oauth_applications table. As we are using the OAuth for API authentication, we wonā€™t need to redirect the user to a callback page (like after you sign in with Google / Apple / Facebook on an app, they will redirect to a page usually).</li> <li>Remove the creation of table <strong>oauth_access_grants</strong> , along with its related index and foreign key.</li> </ol> <p>The OAuth application table is used to keep track of the application we created to use for authentication. For example, we can create three application, one for Android app client, one for iOS app client and one for React frontend, this way we can know which clients the users are using. If you only need one client (eg: web frontend), it is fine too.</p> <p>Hereā€™s an example of Github OAuth applications :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/example_oauth.png" alt="github oauth" /></p> <p>Next, run <code class="language-plaintext highlighter-rouge">rake db:migrate</code> to add these tables into database.</p> <p>Next, we will customize the Doorkeeper configuration.</p> <h2 id="customize-doorkeeper-configuration">Customize Doorkeeper configuration</h2> <p>Open <strong>config/initializers/doorkeeper.rb</strong> , and edit the following.</p> <p>Comment out or remove the block for <strong>resource_owner_authenticator</strong> at the top of the file.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#config/initializers/doorkeeper.rb</span> <span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="c1"># Change the ORM that doorkeeper will use (requires ORM extensions installed).</span> <span class="c1"># Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="c1"># This block will be called to check whether the resource owner is authenticated or not.</span> <span class="c1"># resource_owner_authenticator do</span> <span class="c1"># raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"</span> <span class="c1"># Put your resource owner authentication logic here.</span> <span class="c1"># Example implementation:</span> <span class="c1"># User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)</span> <span class="c1"># end</span> </code></pre></div></div> <p>The <strong>resouce_owner_authenticator</strong> block is used to get the authenticated user information or redirect the user to login page from OAuth, for example like this Twitter OAuth page :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/example_oauth_redirect.png" alt="twitter oauth example" /></p> <p>As we are going to exchange OAuth token by using user login credentials (email + password) on the API, we donā€™t need to implement this block, so we can comment it out.</p> <p>To tell Doorkeeper we are using user credentials to login, we need to implement the <strong>resource_owner_from_credentials</strong> block like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#config/initializers/doorkeeper.rb</span> <span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="c1"># Change the ORM that doorkeeper will use (requires ORM extensions installed).</span> <span class="c1"># Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="c1"># This block will be called to check whether the resource owner is authenticated or not.</span> <span class="c1"># resource_owner_authenticator do</span> <span class="c1"># raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"</span> <span class="c1"># Put your resource owner authentication logic here.</span> <span class="c1"># Example implementation:</span> <span class="c1"># User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)</span> <span class="c1"># end</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="c1"># ...</span> </code></pre></div></div> <p>This will allow us to send the user email and password to the /oauth/token endpoint to authenticate user.</p> <p>Then we need to implement the <strong>authenticate</strong> class method on the <strong>app/models/user.rb</strong> model file.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/user.rb</span> <span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span> <span class="c1"># Include default devise modules. Others available are:</span> <span class="c1"># :confirmable, :lockable, :timeoutable, :trackable and :omniauthable</span> <span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:registerable</span><span class="p">,</span> <span class="ss">:recoverable</span><span class="p">,</span> <span class="ss">:rememberable</span><span class="p">,</span> <span class="ss">:validatable</span> <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">format: </span><span class="no">URI</span><span class="o">::</span><span class="no">MailTo</span><span class="o">::</span><span class="no">EMAIL_REGEXP</span> <span class="c1"># the authenticate method from devise documentation</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_for_authentication</span><span class="p">(</span><span class="ss">email: </span><span class="n">email</span><span class="p">)</span> <span class="n">user</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">valid_password?</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="p">?</span> <span class="n">user</span> <span class="p">:</span> <span class="kp">nil</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>You can read more on the authenticate method on <a href="https://github.com/heartcombo/devise/wiki/How-To:-Find-a-user-when-you-have-their-credentials">Deviseā€™s github Wiki page</a>.</p> <p>Next, enable password grant flow in <strong>config/initializers/doorkeeper.rb</strong> , this will allow us to send the user email and password to the /oauth/token endpoint and get OAuth token in return.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="c1"># enable password grant</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="c1"># ....</span> </code></pre></div></div> <p>You can search for ā€œgrant_flowsā€ in this file, and uncomment and edit it.</p> <p>Next, insert <strong>allow_blank_redirect_uri true</strong> into the configuration, so that we can create OAuth application with blank redirect URL (user wonā€™t get redirected after login, as we are using API).</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span> <span class="c1"># ....</span> </code></pre></div></div> <p>As the OAuth application we create is for our own use (not third part), we can skip authorization.</p> <p>Insert <strong>skip_authorization</strong> into the configuration like this :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span> <span class="n">skip_authorization</span> <span class="k">do</span> <span class="kp">true</span> <span class="k">end</span> <span class="c1"># ....</span> </code></pre></div></div> <p>The authorization we skipped is something like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/oauth_authorization.png" alt="authorization" /></p> <p>As we skipped authorization, user wonā€™t need to click the ā€œauthorizeā€ button to interact with our API.</p> <p>Optionally, if you want to enable refresh token mechanism in OAuth, you can insert the <strong>use_refresh_token</strong> into the configuration. This would allow the client app to request a new access token using the refresh token when the current access token is expired.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="n">orm</span> <span class="ss">:active_record</span> <span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="k">end</span> <span class="n">grant_flows</span> <span class="sx">%w[password]</span> <span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span> <span class="n">skip_authorization</span> <span class="k">do</span> <span class="kp">true</span> <span class="k">end</span> <span class="n">use_refresh_token</span> <span class="c1"># ...</span> </code></pre></div></div> <p>With this, we have finished configuring Doorkeeper authentication for our API.</p> <p>Next, we will add the doorkeeper route in <strong>routes.rb</strong> , this will add the /oauth/* routes.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span> <span class="n">use_doorkeeper</span> <span class="k">do</span> <span class="n">skip_controllers</span> <span class="ss">:authorizations</span><span class="p">,</span> <span class="ss">:applications</span><span class="p">,</span> <span class="ss">:authorized_applications</span> <span class="k">end</span> <span class="c1"># ...</span> <span class="k">end</span> </code></pre></div></div> <p>As we donā€™t need the app authorization, we can skip the authorizations and authorized_applications controller. We can also skip the applications controller, as users wonā€™t be able to create or delete OAuth application.</p> <p>Next, we need to create our own OAuth application manually in the console so we can use it for authentication.</p> <h2 id="create-your-own-oauth-application">Create your own OAuth application</h2> <p>Open up rails console, <code class="language-plaintext highlighter-rouge">rails console</code></p> <p>Then create an OAuth application using this command :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> </code></pre></div></div> <p>You can change the name to any name you want, and leave redirect_uri and scopes blank.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/create_client.png" alt="create_client" /></p> <p>This will create a record in the <strong>oauth_applications</strong> table. Keep note that the <strong>uid</strong> attribute and <strong>secret</strong> attribute, these are used for authentication on API later, uid = client_id and secret = client_secret.</p> <p>For production use, you can create a database seed for initial creation of the OAuth applications in <strong>db/seeds.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># db/seeds.rb</span> <span class="c1"># if there is no OAuth application created, create them</span> <span class="k">if</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">count</span><span class="p">.</span><span class="nf">zero?</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"iOS client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"React"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span> <span class="k">end</span> </code></pre></div></div> <p>Then run <code class="language-plaintext highlighter-rouge">rake db:seed</code> to create these applications.</p> <p><strong>Doorkeeper::Application</strong> is just a namespaced model name for the <strong>oauth_applications</strong> table, you can perform ActiveRecord query as usual :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># client_id of the application</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">).</span><span class="nf">uid</span> <span class="c1"># client_secret of the application</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">).</span><span class="nf">secret</span> </code></pre></div></div> <p>Now we have Doorkeeper application set up, we can try to login user in the next section.</p> <h2 id="how-to-login--logout-and-refresh-token-using-api">How to login , logout and refresh token using API</h2> <p>We will need a user created to be able to login / logout them using the OAuth endpoints, you can register a dummy user on the devise web UI if you havenā€™t already (eg: localhost:3000/users/sign_up) or create one via the rails console.</p> <p>The HTTP requests below can either send attributes using JSON format or URL-Encoded form.</p> <h3 id="login">Login</h3> <p>To login the user on the OAuth endpoint, we need to send a HTTP POST request to <strong>/oauth/token</strong>, with <strong>grant_type</strong>, <strong>email</strong>, <strong>password</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/login_oauth.png" alt="login oauth" /></p> <p>As we are using password in exchange for OAuth access and refresh token, the <strong>grant_type</strong> value should be <strong>password</strong>.</p> <p><strong>email</strong> and <strong>password</strong> is the login credential of the user.</p> <p><strong>client_id</strong> is the <strong>uid</strong> of the Doorkeeper::Application (OAuth application) we created earlier, with this we can identify which client the user has used to log in.</p> <p><strong>client_secret</strong> is the <strong>secret</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p> <p>On successful login attempt, the API will return <strong>access_token</strong>, <strong>refresh_token</strong>, <strong>token_type</strong>, <strong>expires_in</strong> and <strong>created_at</strong> attributes.</p> <p>We can then use <strong>access_token</strong> to call protected API that requires user authentication.</p> <p><strong>refresh_token</strong> can be used to generate and retrieve a new access token after the current access_token has expired.</p> <p><strong>expires_in</strong> is the time until expiry for the access_token, starting from the UNIX timestamp of <strong>created_at</strong>, the default value is 7200 (seconds), which is around 2 hours.</p> <h3 id="logout">Logout</h3> <p>To log out a user, we can revoke the access token, so that the same access token cannot be used anymore.</p> <p>To revoke an access token, we need to send a HTTP POST request to <strong>/oauth/revoke</strong>, with <strong>token</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p> <p>Other than these attributes, we also need to set Authorization header for the HTTP request to use Basic Auth, using <strong>client_id</strong> value for the <strong>username</strong> and <strong>client_password</strong> value for the <strong>password</strong>. (According to <a href="https://github.com/doorkeeper-gem/doorkeeper/issues/1412#issuecomment-631938006">this reply</a> in Doorkeeper gem repository)</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/logout_auth.png" alt="logout auth" /></p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/logout_body.png" alt="logout body" /></p> <p>After revoking a token, the token record will have a <strong>revoked_at</strong> column filled :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/revoked_token.png" alt="revoked_at" /></p> <h3 id="refresh-token">Refresh token</h3> <p>To retrieve a new access token when the current access token is (almost) expired, we can send a HTTP POST to <strong>/oauth/token</strong> , it is the same endpoint as login, but this time we are using ā€œrefresh_tokenā€ as the value for <strong>grant_type</strong>, and is sending the value of refresh token instead of login credentials.</p> <p>To refresh a token, we need to send <strong>grant_type</strong>, <strong>refresh_token</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p> <p><strong>grant_type</strong> needs to be equal to ā€œ<strong>refresh_token</strong>ā€ here as we are using refresh token to authenticate.</p> <p><strong>refresh_token</strong> should be the refresh token value you have retrieved during login.</p> <p><strong>client_id</strong> is the <strong>uid</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p> <p><strong>client_secret</strong> is the <strong>secret</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/refresh_token.png" alt="refresh_token" /></p> <p>On successful refresh attempt, the API return a new access_token and refresh_token, which we can use to call protected API that requires user authentication.</p> <h2 id="create-api-controllers-that-require-authentication">Create API controllers that require authentication</h2> <p>Now that we have user authentication set up, we can now create API controllers that require authentication.</p> <p>For this, I recommend creating a base API application controller, then subclass this controller for controllers that require authentication.</p> <p>Create a base API application controller (<strong>application_controller.rb</strong>) and place it in <strong>app/controllers/api/application_controller.rb</strong> .</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/application_controller.rb</span> <span class="k">module</span> <span class="nn">Api</span> <span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span> <span class="c1"># equivalent of authenticate_user! on devise, but this one will check the oauth token</span> <span class="n">before_action</span> <span class="ss">:doorkeeper_authorize!</span> <span class="kp">private</span> <span class="c1"># helper method to access the current user from the token</span> <span class="k">def</span> <span class="nf">current_user</span> <span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">id: </span><span class="n">doorkeeper_token</span><span class="p">[</span><span class="ss">:resource_owner_id</span><span class="p">])</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>The API application subclasses from <a href="https://api.rubyonrails.org/classes/ActionController/API.html">ActionController::API</a>, which is a lightweight version of ActionController::Base, and does not contain HTML layout and templating functionality (we dont need it for API anyway), and it doesnā€™t have CORS protection.</p> <p>We add the <strong>doorkeeper_authorize!</strong> method in the before_action callback, as this will check if the user is authenticated with a valid token before calling methods in the controller, this is similar to the authenticate_user! method on devise.</p> <p>We also add a <strong>current_user</strong> method to get the current user object, then we can attach the current user on some modelā€™s CRUD action.</p> <p>As example of a protected API controller, letā€™s create a bookmarks controller to retrieve all bookmarks.</p> <p><strong>app/controllers/api/bookmarks_controller.rb</strong></p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/bookmarks_controller.rb</span> <span class="k">module</span> <span class="nn">Api</span> <span class="k">class</span> <span class="nc">BookmarksController</span> <span class="o">&lt;</span> <span class="no">Api</span><span class="o">::</span><span class="no">ApplicationController</span> <span class="k">def</span> <span class="nf">index</span> <span class="vi">@bookmarks</span> <span class="o">=</span> <span class="no">Bookmark</span><span class="p">.</span><span class="nf">all</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">bookmarks: </span><span class="vi">@bookmarks</span> <span class="p">}</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>The bookmarks controller will inherit from the base API application controller we created earlier, and it will return all the bookmarks in JSON format.</p> <p>Donā€™t forget to add the route for it in <strong>routes.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span> <span class="c1"># ....</span> <span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span> <span class="n">resources</span> <span class="ss">:bookmarks</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index]</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Then we can retrieve the bookmarks by sending a HTTP GET request to <strong>/api/bookmarks</strong>, with the userā€™s access token in the Authorization Header (Authorization: Bearer [User Access Token])</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/get_bookmarks_authorized.png" alt="get boomarks api authorization" /></p> <p>To access protected API controllers, we will need to include the Authorization HTTP header, with the values of ā€œBearer [User Access Token]ā€.</p> <h2 id="create-an-endpoint-for-user-registration">Create an endpoint for user registration</h2> <p>It would be weird if we only allow user registration through website, we would also need to add an API endpoint for user to register an account .</p> <p>For this, letā€™s create a users controller and place it in <strong>app/controllers/api/users_controller.rb</strong>.</p> <p>The <strong>create</strong> action will create an user account from the supplied email and password.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Api</span> <span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">Api</span><span class="o">::</span><span class="no">ApplicationController</span> <span class="n">skip_before_action</span> <span class="ss">:doorkeeper_authorize!</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[create]</span> <span class="k">def</span> <span class="nf">create</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">email: </span><span class="n">user_params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="ss">password: </span><span class="n">user_params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="n">client_app</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">uid: </span><span class="n">params</span><span class="p">[</span><span class="ss">:client_id</span><span class="p">])</span> <span class="k">return</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid client ID'</span><span class="p">},</span> <span class="ss">status: </span><span class="mi">403</span><span class="p">)</span> <span class="k">unless</span> <span class="n">client_app</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">save</span> <span class="c1"># create access token for the user, so the user won't need to login again after registration</span> <span class="n">access_token</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="ss">resource_owner_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">application_id: </span><span class="n">client_app</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">refresh_token: </span><span class="n">generate_refresh_token</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">access_token_expires_in</span><span class="p">.</span><span class="nf">to_i</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s1">''</span> <span class="p">)</span> <span class="c1"># return json containing access token and refresh token</span> <span class="c1"># so that user won't need to call login API right after registration</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">user: </span><span class="p">{</span> <span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">email: </span><span class="n">user</span><span class="p">.</span><span class="nf">email</span><span class="p">,</span> <span class="ss">access_token: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">token</span><span class="p">,</span> <span class="ss">token_type: </span><span class="s1">'bearer'</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">expires_in</span><span class="p">,</span> <span class="ss">refresh_token: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">refresh_token</span><span class="p">,</span> <span class="ss">created_at: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">created_at</span><span class="p">.</span><span class="nf">to_time</span><span class="p">.</span><span class="nf">to_i</span> <span class="p">}</span> <span class="p">})</span> <span class="k">else</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span> <span class="p">},</span> <span class="ss">status: </span><span class="mi">422</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="kp">private</span> <span class="k">def</span> <span class="nf">user_params</span> <span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">generate_refresh_token</span> <span class="kp">loop</span> <span class="k">do</span> <span class="c1"># generate a random token string and return it, </span> <span class="c1"># unless there is already another token with the same string</span> <span class="n">token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span> <span class="k">break</span> <span class="n">token</span> <span class="k">unless</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">exists?</span><span class="p">(</span><span class="ss">refresh_token: </span><span class="n">token</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>As the user doesnā€™t have an account at this point, we want to exempt this action from requiring authentication information, so we added the line <strong>skip_before_action :doorkeeper_authorize!, only: %i[create]</strong> at the top. This will make the <strong>create</strong> method to skip running the <strong>doorkeeper_authorize!</strong> before_action method we defined in the base API controller, and the client app can call the user account creation API endpoint without authentication information.</p> <p>We then create an AccessToken on successful user registration using <strong>Doorkeeper::AccessToken.create()</strong> and return it in the HTTP response, so the user wonā€™t need to login right after registration.</p> <p>Remember to add a route for ths user registration action in <strong>routes.rb</strong> :</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span> <span class="c1"># ....</span> <span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span> <span class="n">resources</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[create]</span> <span class="n">resources</span> <span class="ss">:bookmarks</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index]</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Then we can call create user API by sending a HTTP POST request containing the userā€™s <strong>email</strong>, <strong>password</strong> and <strong>client_id</strong> to <strong>/api/users</strong> like this :</p> <p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/oauth_registration.png" alt="create user API" /></p> <p>The <strong>client_id</strong> is used to identify which client app the user is using for registration.</p> <h1 id="revoke-user-token-manually">Revoke user token manually</h1> <p>Say if you suspect a userā€™s access token has been misused or abused, you can revoke them manually using this function :</p> <p><strong>Doorkeeper::AccessToken.revoke_all_for(application_id, resource_owner)</strong></p> <p>application_id is the <strong>id</strong> of the Doorkeeper::Application (OAuth application) we want to revoke the user from.</p> <p>resource_owner is the <strong>user object</strong>.</p> <p>Example usage:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">client_app</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s1">'Android client'</span><span class="p">)</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">revoke_all_for</span><span class="p">(</span><span class="n">client_app</span><span class="p">.</span><span class="nf">id</span> <span class="p">,</span> <span class="n">user</span><span class="p">)</span> </code></pre></div></div> <h2 id="references">References</h2> <p><a href="https://github.com/heartcombo/devise/wiki/How-To:-Find-a-user-when-you-have-their-credentials">Devise wiki - How To: Find a user when you have their credentials</a></p> <p><a href="https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Resource-Owner-Password-Credentials-flow">Doorkeeper wiki - Using Resource Owner Password Credentials flow</a></p> <script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script></content>
<author>
<name>Axel Kee</name>
</author>
<summary type="html">Most of the time when we implement API endpoints on our Rails app, we want to limit access of these API to authorized users only, thereā€™s a few strategy for authenticating user through API, ranging from a simple token authentication to a fullblown OAuth provider with JWT.</summary>
</entry>
</feed>