<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="nmunro.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="nmunro.github.io/" rel="alternate" type="text/html" /><updated>2026-02-28T13:33:01+00:00</updated><id>nmunro.github.io/feed.xml</id><title type="html">nmunro.github.io</title><subtitle>Common Lisp and other programming related things from NMunro</subtitle><entry><title type="html">Ningle Tutorial 15: Pagination, Part 2</title><link href="nmunro.github.io/2026/02/28/ningle-15.html" rel="alternate" type="text/html" title="Ningle Tutorial 15: Pagination, Part 2" /><published>2026-02-28T08:00:00+00:00</published><updated>2026-02-28T08:00:00+00:00</updated><id>nmunro.github.io/2026/02/28/ningle-15</id><content type="html" xml:base="nmunro.github.io/2026/02/28/ningle-15.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li>Part 15 (Pagination, Part 2)</li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back! We will be revisiting the pagination from last time, however we are going to try and make this easier on ourselves, I built a package for pagination <a href="https://github.com/nmunro/mito-pager">mito-pager</a>, the idea is that much of what we looked at in the last lesson was very boiler plate and repetitive so we should look at removing this.</p>

<p>I will say, my <code class="language-plaintext highlighter-rouge">mito-pager</code> can do a little more than just what I show here, it has two modes, you can use <code class="language-plaintext highlighter-rouge">paginate-dao</code> (named this way so that it is familiar to <code class="language-plaintext highlighter-rouge">mito</code>) to paginate over simple models, however, if you need to perform complex queries there is a macro <code class="language-plaintext highlighter-rouge">with-pager</code> that you can use to paginate. It is this second form we will use in this tutorial.</p>

<p>There is one thing to bear in mind, when using <code class="language-plaintext highlighter-rouge">mito-pager</code>, you <em>must</em> implement your data retrieval functions in such a way to return a <code class="language-plaintext highlighter-rouge">values</code> object, as <code class="language-plaintext highlighter-rouge">mito-pager</code> relies on this to work.</p>

<p>I encourge you to try the library out in other use-cases and, of course, if you have ideas, please let me know.</p>

<h2 id="changes">Changes</h2>

<p>Most of our changes are quite limited in scope, really it’s just our controllers and models that need most of the edits.</p>

<h3 id="ningle-tutorial-projectasd">ningle-tutorial-project.asd</h3>

<p>We need to add the <code class="language-plaintext highlighter-rouge">mito-pager</code> package to our project asd file.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- :ningle-auth)
</span><span class="gi">+ :ningle-auth
+ :mito-pager)
</span></code></pre></div></div>

<h3 id="srccontrollerslisp">src/controllers.lisp</h3>

<p>Here is the real payoff! I almost dreaded writing the sheer volume of the change but then realised it’s so simple, we only need to change our index function, and it may be better to delete it all and write our new simplified version.</p>

<div class="language-common-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defun</span> <span class="nv">index</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
         <span class="p">(</span><span class="nv">req-page</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
         <span class="p">(</span><span class="nv">req-limit</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"limit"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"50"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">50</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">flet</span> <span class="p">((</span><span class="nv">get-posts</span> <span class="p">(</span><span class="nv">limit</span> <span class="nv">offset</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="nv">offset</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">)))</span>
      <span class="p">(</span><span class="nv">mito-pager:with-pager</span> <span class="p">((</span><span class="nv">posts</span> <span class="nv">pager</span> <span class="nf">#'</span><span class="nv">get-posts</span> <span class="ss">:page</span> <span class="nv">req-page</span> <span class="ss">:limit</span> <span class="nv">req-limit</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span> <span class="ss">:pager</span> <span class="nv">pager</span><span class="p">)))))</span>
</code></pre></div></div>

<p>This is <em>much</em> nicer, and in my opinion, the controller should be this simple.</p>

<h3 id="srcmainlisp">src/main.lisp</h3>

<p>We need to ensure we include the templates from <code class="language-plaintext highlighter-rouge">mito-pager</code>, this is a simple one line change.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code> (defun start (&amp;key (server :woo) (address "127.0.0.1") (port 8000))
    (djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
<span class="gi">+   (djula:add-template-directory (asdf:system-relative-pathname :mito-pager "src/templates/"))
</span></code></pre></div></div>

<h3 id="srcmodelslisp">src/models.lisp</h3>

<p>As mentioned at the top of this tutorial, we have to implement our data retrieval functions in a certain way. While there are some changes here, we ultimately end up with less code.</p>

<p>We can start by removing the <code class="language-plaintext highlighter-rouge">count</code> parameter, we wont be needing it in this implementation, and since we don’t need the <code class="language-plaintext highlighter-rouge">count</code> parameter anymore, the <code class="language-plaintext highlighter-rouge">:around</code> method can go too!</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- (defgeneric posts (user &amp;key offset limit count)
</span><span class="gi">+ (defgeneric posts (user &amp;key offset limit)
</span><span class="gd">-
- (defmethod posts :around (user &amp;key (offset 0) (limit 50) &amp;allow-other-keys)
-   (let ((count (mito:count-dao 'post))
-         (offset (max 0 offset))
-         (limit (max 1 limit)))
-     (if (and (&gt; count 0) (&gt;= offset count))
-       (let* ((page-count (max 1 (ceiling count limit)))
-              (corrected-offset (* (1- page-count) limit)))
-         (posts user :offset corrected-offset :limit limit))
-       (call-next-method user :offset offset :limit limit :count count))))
</span></code></pre></div></div>

<p>There’s two methods to look at, the first is when the type of user is <code class="language-plaintext highlighter-rouge">user</code>:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-
- (defmethod posts ((user user) &amp;key offset limit count)
</span><span class="gi">+ (defmethod posts ((user user) &amp;key offset limit)
</span><span class="err">...</span>
      (values
<span class="gd">-         (mito:retrieve-by-sql sql :binds params)
-         count
-         offset)))
</span><span class="gi">+         (mito:retrieve-by-sql sql :binds params)
+         (mito:count-dao 'post))))
</span></code></pre></div></div>

<p>The second is when the type of user is <code class="language-plaintext highlighter-rouge">null</code>:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-
- (defmethod posts ((user null) &amp;key offset limit count)
</span><span class="gi">+ (defmethod posts ((user null) &amp;key offset limit)
</span><span class="err">...</span>
    (values
<span class="gd">-       (mito:retrieve-by-sql sql)
-       count
-       offset)))
</span><span class="gi">+       (mito:retrieve-by-sql sql)
+       (mito:count-dao 'post))))
</span></code></pre></div></div>

<p>As you can see, all we are really doing is relying on mito to do the lions share of the work, right down to the count.</p>

<h3 id="srctemplatesmainindexhtml">src/templates/main/index.html</h3>

<p>The change here is quite simple, all we need to do is to change the path to the partial, we need to simply point to the partial provided by <code class="language-plaintext highlighter-rouge">mito-pager</code>.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
- {% include "partials/pager.html" with url="/" title="Posts" %}
<span class="gi">+ {% include "mito-pager/partials/pager.html" with url="/" title="Posts" %}
</span>
</code></pre></div></div>

<h3 id="srctemplatespartialspaginationhtml">src/templates/partials/pagination.html</h3>

<p>This one is easy, we can delete it! <code class="language-plaintext highlighter-rouge">mito-pager</code> provides its own template, and while you can override it (if you so wish), in this tutorial we do not need it anymore.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I hope you will agree that this time, using a prebuilt package takes a lot of the pain out of pagination. I don’t like to dictate what developers should, or shouldn’t use, so that’s why last time you were given the same information I had, so if you wish to build your own library, you can, or if you want to focus on getting things done, you are more than welcome to use mine, and of course, if you find issues please do let me know!</p>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Learning Outcome</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Understand</strong></td>
      <td>Understand how third-party pagination libraries like <code class="language-plaintext highlighter-rouge">mito-pager</code> abstract boilerplate pagination logic, and how <code class="language-plaintext highlighter-rouge">with-pager</code> expects a fetch function returning <code class="language-plaintext highlighter-rouge">(values items count)</code> to handle page clamping, offset calculation, and boundary correction automatically.</td>
    </tr>
    <tr>
      <td><strong>Apply</strong></td>
      <td>Apply <code class="language-plaintext highlighter-rouge">flet</code> to define a local adapter function that bridges the project’s <code class="language-plaintext highlighter-rouge">posts</code> generic function with <code class="language-plaintext highlighter-rouge">mito-pager</code>’s expected <code class="language-plaintext highlighter-rouge">(lambda (limit offset) ...)</code> interface, and use <code class="language-plaintext highlighter-rouge">with-pager</code> to reduce controller complexity to its essential logic.</td>
    </tr>
    <tr>
      <td><strong>Analyse</strong></td>
      <td>Analyse what responsibilities were transferred from the manual pagination implementation to <code class="language-plaintext highlighter-rouge">mito-pager</code> — count caching, boundary checking, offset calculation, page correction, and range generation — contrasting the complexity of both approaches.</td>
    </tr>
    <tr>
      <td><strong>Create</strong></td>
      <td>Refactor a manual pagination implementation to use <code class="language-plaintext highlighter-rouge">mito-pager</code> by simplifying model methods to return <code class="language-plaintext highlighter-rouge">(values items count)</code>, replacing complex multi-step controller calculations with <code class="language-plaintext highlighter-rouge">with-pager</code>, and delegating the pagination template partial to the library.</td>
    </tr>
  </tbody>
</table>

<h2 id="github">Github</h2>

<ul>
  <li>The link for the custom pagination part of the tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-14.2">here</a>.</li>
</ul>

<h3 id="common-lisp-hyperspec">Common Lisp HyperSpec</h3>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Type</th>
      <th>Why it appears in this lesson</th>
      <th>CLHS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defpackage</code></td>
      <td>Macro</td>
      <td>Define project packages like <code class="language-plaintext highlighter-rouge">ningle-tutorial-project/models</code>, <code class="language-plaintext highlighter-rouge">/forms</code>, <code class="language-plaintext highlighter-rouge">/controllers</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">in-package</code></td>
      <td>Macro</td>
      <td>Enter each package before defining models, controllers, and functions.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defgeneric</code></td>
      <td>Macro</td>
      <td>Define the simplified generic <code class="language-plaintext highlighter-rouge">posts</code> function signature with keyword parameters <code class="language-plaintext highlighter-rouge">offset</code> and <code class="language-plaintext highlighter-rouge">limit</code> (the <code class="language-plaintext highlighter-rouge">count</code> parameter is removed).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defmethod</code></td>
      <td>Macro</td>
      <td>Implement the simplified <code class="language-plaintext highlighter-rouge">posts</code> methods for <code class="language-plaintext highlighter-rouge">user</code> and <code class="language-plaintext highlighter-rouge">null</code> types (the <code class="language-plaintext highlighter-rouge">:around</code> validation method is removed).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">flet</code></td>
      <td>Special Operator</td>
      <td>Define the local <code class="language-plaintext highlighter-rouge">get-posts</code> adapter function that wraps <code class="language-plaintext highlighter-rouge">posts</code> to match <code class="language-plaintext highlighter-rouge">mito-pager</code>’s expected <code class="language-plaintext highlighter-rouge">(lambda (limit offset) ...)</code> interface.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_flet_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_flet_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let*</code></td>
      <td>Special Operator</td>
      <td>Sequentially bind <code class="language-plaintext highlighter-rouge">user</code>, <code class="language-plaintext highlighter-rouge">req-page</code>, and <code class="language-plaintext highlighter-rouge">req-limit</code> in the controller where each value is used in subsequent bindings.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">or</code></td>
      <td>Macro</td>
      <td>Provide fallback values when parsing <code class="language-plaintext highlighter-rouge">page</code> and <code class="language-plaintext highlighter-rouge">limit</code> parameters, defaulting to 1 and 50 respectively.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_or.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_or.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">multiple-value-bind</code></td>
      <td>Macro</td>
      <td>Capture the SQL string and bind parameters returned by <code class="language-plaintext highlighter-rouge">sxql:yield</code> in the model methods.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_multip.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_multip.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">values</code></td>
      <td>Function</td>
      <td>Return two values from <code class="language-plaintext highlighter-rouge">posts</code> methods — the list of results and the total count — as required by <code class="language-plaintext highlighter-rouge">mito-pager:with-pager</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/a_values.htm">http://www.lispworks.com/documentation/HyperSpec/Body/a_values.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parse-integer</code></td>
      <td>Function</td>
      <td>Convert string query parameters (<code class="language-plaintext highlighter-rouge">"1"</code>, <code class="language-plaintext highlighter-rouge">"50"</code>) to integers, with <code class="language-plaintext highlighter-rouge">:junk-allowed t</code> for safe parsing.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 14: Pagination, Part 1</title><link href="nmunro.github.io/2026/01/31/ningle-14.html" rel="alternate" type="text/html" title="Ningle Tutorial 14: Pagination, Part 1" /><published>2026-01-31T08:00:00+00:00</published><updated>2026-01-31T08:00:00+00:00</updated><id>nmunro.github.io/2026/01/31/ningle-14</id><content type="html" xml:base="nmunro.github.io/2026/01/31/ningle-14.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li>Part 14 (Pagination, Part 1)</li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Hello and welcome back, I hope you all had a good festive season, I took a break last month as I usually get very busy in December, but lest you think I had stopped posting, I have prepared a two part lesson this time: Pagination. We are first going to look at rolling your own pagination, but we will then look at integrating a package I wrote <a href="https://github.com/nmunro/ningle-pager">ningle-pager</a>, to simplify the code. This way if my package doesn’t fit your needs, you have the information required to build your own solution.</p>

<p>In practical terms, something like a microblogging app would use <a href="https://en.wikipedia.org/wiki/Infinite_scrolling">infinite scrolling</a>, but we don’t have anywhere enough data to present that as a lesson right now, and besides pagination has a lot of uses, Google and Amazon use it for their products, so it must be pretty useful!</p>

<h2 id="theory">Theory</h2>

<p>In SQL, there is the ability to <a href="https://www.geeksforgeeks.org/sql/sql-limit-clause/">LIMIT</a> results, but also, the ability to start from an <a href="https://www.datacamp.com/tutorial/sql-offset">OFFSET</a>, which ultimately does the heavy lifting for us. We have previously looked at <a href="https://github.com/fukamachi/sxql">SXQL</a>, which is a thin layer upon SQL, so we can use <code class="language-plaintext highlighter-rouge">(limit 50)</code> and <code class="language-plaintext highlighter-rouge">(offset 100)</code> (or whatever values we want) to interact with the database, we will also use <code class="language-plaintext highlighter-rouge">GET</code> parameters like <code class="language-plaintext highlighter-rouge">?page=2&amp;limit=50</code> (or something). So with this information we know the url patterns and we know what <code class="language-plaintext highlighter-rouge">SXQL</code> forms we want to use, we just have to design how our application will work internally.</p>

<p>Our solution will define an interface, any controller that needs to be paginated will:</p>

<ul>
  <li>Accept a <code class="language-plaintext highlighter-rouge">page</code> keyword parameter</li>
  <li>Accept a <code class="language-plaintext highlighter-rouge">limit</code> keyword parameter</li>
  <li>Return a <code class="language-plaintext highlighter-rouge">values</code> list that has 3 items, the results, the total count, and the offset.</li>
</ul>

<p>The work will touch the models, the controllers, and the templates:</p>

<h2 id="models">Models</h2>

<p>We are gonna get deep into the weeds with <a href="https://en.wikipedia.org/wiki/Common_Lisp_Object_System">clos</a> in how we implement our pagination in this part, there’s multiple methods so we will take each implementation one by one. You can learn more about how OOP is implemented in my older <a href="https://www.youtube.com/watch?v=ULzmjVTpINg">videos</a>.</p>

<h3 id="generic-method">generic Method</h3>

<p>We start with a generic definition, we already had one, but we are modifying it. Fun fact, the generic method defines all the parameters a method <em>might</em> use, but not all methods <em>must</em> use the arguments, which comes in real handy for us later:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">posts</span> <span class="p">(</span><span class="nv">user</span> <span class="k">&amp;key</span> <span class="nv">offset</span> <span class="nv">limit</span> <span class="nb">count</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Gets the posts"</span><span class="p">))</span>
</code></pre></div></div>

<p>Here we have a <code class="language-plaintext highlighter-rouge">generic</code> method, generic methods do nothing on their own, they help us define what a method should do, but of course under certain circumstances <em>how</em> a method does what it does may need to change, this allows us to implment different specialised concrete methods, we will look at this below.</p>

<p>What we have done with this generic method is add key arguments <code class="language-plaintext highlighter-rouge">offset</code>, <code class="language-plaintext highlighter-rouge">limit</code>, and <code class="language-plaintext highlighter-rouge">count</code>, as we saw previously, all this does is declare a <code class="language-plaintext highlighter-rouge">:documentation</code> form.</p>

<h3 id="around-method">:around method</h3>

<p>As you may, or may not know, the Common Lisp Object System (clos for short) allows us to define, as we have done previously <code class="language-plaintext highlighter-rouge">primary methods</code>, these are methods that specialise on one (or more) of the parameters. When passed arguments at the point the method is called, the method matching the parameter type of the arguments passed will trigger. That is why our <code class="language-plaintext highlighter-rouge">posts</code> method specifies user to be a user object, or null and handles the logic in different ways. It <em>also</em> allows us to define <code class="language-plaintext highlighter-rouge">auxiliary</code> methods, which are known as <code class="language-plaintext highlighter-rouge">:before</code>, <code class="language-plaintext highlighter-rouge">:after</code>, and <code class="language-plaintext highlighter-rouge">:around</code>. The <code class="language-plaintext highlighter-rouge">:before</code> methods will run, well, before the related primary method is called, with each <code class="language-plaintext highlighter-rouge">:before</code> method being called by its most specific signature to its least. <code class="language-plaintext highlighter-rouge">:after</code> methods are the total opposite, they run after a primary method is run, and they run from the least specific version to the most specific. They would be where we might want to add signals, or logging, we could have a <code class="language-plaintext highlighter-rouge">:before</code>, and <code class="language-plaintext highlighter-rouge">:after</code> around the <code class="language-plaintext highlighter-rouge">mito:save-dao</code> that we use and the <code class="language-plaintext highlighter-rouge">:before</code> method sends a <code class="language-plaintext highlighter-rouge">pre-save</code> signal while the <code class="language-plaintext highlighter-rouge">:after</code> sends a <code class="language-plaintext highlighter-rouge">post-save</code> signal.</p>

<p>It is not, however the <code class="language-plaintext highlighter-rouge">:before</code>/<code class="language-plaintext highlighter-rouge">:after</code> methods we care about here, we in fact will write an <code class="language-plaintext highlighter-rouge">:around</code>, which is a more fundamental building block. <code class="language-plaintext highlighter-rouge">:around</code> methods control, how, when, or even if, a primary method gets called, the other methods can’t control this. As previously discussed they have a specific order in which they run, so if we wanted to… say… capture arguments and do some processing on them because, I dunno, we should never trust user input, prior to running our primary method, an <code class="language-plaintext highlighter-rouge">:around</code> method is what we would need to use.</p>

<p>The real “magic” of how to do what we want to do is use an <code class="language-plaintext highlighter-rouge">:around</code> method. We will look at the complete implementation a little bit later, but we need to pause and ensure we really understand about method combination in Common Lisp.</p>

<p>As we mentioned in the <code class="language-plaintext highlighter-rouge">defgeneric</code>, not every <code class="language-plaintext highlighter-rouge">method</code> needs to use or specialise on every parameter, and in this <code class="language-plaintext highlighter-rouge">:around</code> method you will notice that the count is absent, that is by design, because the <code class="language-plaintext highlighter-rouge">:around</code> method will compute it and pass it onto the next method in the chain, instead it uses <code class="language-plaintext highlighter-rouge">&amp;allow-other-keys</code> to allow these key arguments to be accepted, but also since they are unnamed, the compiler won’t emit a warning that they’re not used.</p>

<p>Our implementation is here:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="ss">:around</span> <span class="p">(</span><span class="nv">user</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">offset</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nv">limit</span> <span class="mi">50</span><span class="p">)</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nb">count</span> <span class="p">(</span><span class="nv">mito:count-dao</span> <span class="ss">'post</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">offset</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">0</span> <span class="nv">offset</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="nv">limit</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nb">count</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nb">&gt;=</span> <span class="nv">offset</span> <span class="nb">count</span><span class="p">))</span>
      <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">page-count</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">ceiling</span> <span class="nb">count</span> <span class="nv">limit</span><span class="p">)))</span>
             <span class="p">(</span><span class="nv">corrected-offset</span> <span class="p">(</span><span class="nb">*</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page-count</span><span class="p">)</span> <span class="nv">limit</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="nv">corrected-offset</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">))</span>
      <span class="p">(</span><span class="nb">call-next-method</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="nv">offset</span> <span class="ss">:limit</span> <span class="nv">limit</span> <span class="ss">:count</span> <span class="nb">count</span><span class="p">))))</span>
</code></pre></div></div>

<p>The first thing to note is the obvious <code class="language-plaintext highlighter-rouge">:around</code> keyword that comes after the <code class="language-plaintext highlighter-rouge">posts</code> name, this is how we declare a method as an <code class="language-plaintext highlighter-rouge">:around</code> method. The next thing to notice is that the <code class="language-plaintext highlighter-rouge">count</code> parameter is not declared, instead we use the <code class="language-plaintext highlighter-rouge">&amp;allow-other-keys</code>, as discussed above. This method will modify some variables or recalculate the offset if it was invalid before either calling itself (to perform the recalculations) or call the next method with, well, <code class="language-plaintext highlighter-rouge">call-next-method</code>.</p>

<p>We begin with a <code class="language-plaintext highlighter-rouge">let</code> form that will get the number of items by using <code class="language-plaintext highlighter-rouge">mito:count-dao</code>, we determine the offset by getting the max of 0 or the offset, we also define limit as the max of 1 and limit.</p>

<p>The key check here is in the <code class="language-plaintext highlighter-rouge">if</code> form, which checks that both the count is larger than zero <code class="language-plaintext highlighter-rouge">(&gt; count zero)</code> and the offset is bigger than the count <code class="language-plaintext highlighter-rouge">(&gt;= offset count)</code>, this tells us that an invalid condition exists, we can’t request an offset to be larger than the number of items, so we have to handle it. Under these circumstances we need to get the new page-count by calculating <code class="language-plaintext highlighter-rouge">(max 1 (ceiling count limit))</code>, this will round up the result of dividing count by limit, and returns that, or 1.</p>

<p>Once we have that we can calculate a corrected offset by using the formula <code class="language-plaintext highlighter-rouge">(* (1- page-count) limit)</code>, to run through how this formula works, here are some examples, if we assume limit is defined as <code class="language-plaintext highlighter-rouge">50</code>, we can increment the page-count by one each time to see how this calculation works:</p>

<ul>
  <li>Page 1: <code class="language-plaintext highlighter-rouge">(* (1- 1) 50)</code> -&gt; <code class="language-plaintext highlighter-rouge">(* 0 50)</code> -&gt; 0</li>
  <li>Page 2: <code class="language-plaintext highlighter-rouge">(* (1- 2) 50)</code> -&gt; <code class="language-plaintext highlighter-rouge">(* 1 50)</code> -&gt; 50</li>
  <li>Page 3: <code class="language-plaintext highlighter-rouge">(* (1- 3) 50)</code> -&gt; <code class="language-plaintext highlighter-rouge">(* 2 50)</code> -&gt; 100</li>
</ul>

<p>With this calculation done we can recursively call the method again, this time with the correct values, which brings us to our base case, calling the next method via <code class="language-plaintext highlighter-rouge">call-next-method</code> with the appropriate values which handily brings us to the specific methods now. We can actually dramatically simplify our <code class="language-plaintext highlighter-rouge">primary methods</code> thanks to the <code class="language-plaintext highlighter-rouge">:around</code> method.</p>

<p>Something to bear in mind, our count here is real easy, cos we are just returning all posts, but a more complex application may need more complex logic to determine what and how you are counting.</p>

<h3 id="user-method">user method</h3>

<p>Since we don’t need to handle any error state or recovery (because the <code class="language-plaintext highlighter-rouge">:around</code> method handles it), we can actually write simple methods that perform a query and return the results. We have also simplified the way in which we run queries, turns out the <code class="language-plaintext highlighter-rouge">sxql:yield</code> returns multiple values, the first is the SQL string, the second is a list of params to be spliced into it (to avoid sql injection attacks), so we set up a <code class="language-plaintext highlighter-rouge">multiple-value-bind</code> form to capture these, and we put our SQL together, we previously used <code class="language-plaintext highlighter-rouge">:?</code> which was fine, as that is the character used to be a place holder, but this way is nicer to write. The things you learn, eh?</p>

<p>Please note however, where in our <code class="language-plaintext highlighter-rouge">:around</code> method we didn’t specify the <code class="language-plaintext highlighter-rouge">count</code> parameter that the generic method defines, in this primary method, we do!</p>

<p>All we do it use a <code class="language-plaintext highlighter-rouge">values</code> form to return the result of running the sql, with the parameters bound to it, the count (number of items total) and the offset from where it starts returning results from.</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">)</span> <span class="k">&amp;key</span> <span class="nv">offset</span> <span class="nv">limit</span> <span class="nb">count</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">sql</span> <span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
              <span class="p">(</span><span class="nv">sxql:select</span>
                  <span class="p">(</span><span class="ss">:post.*</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                  <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                            <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))</span>
                  <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:offset</span> <span class="nv">offset</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">sxql:limit</span> <span class="nv">limit</span><span class="p">)))</span>
      <span class="p">(</span><span class="nb">values</span>
          <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="nv">sql</span> <span class="ss">:binds</span> <span class="nv">params</span><span class="p">)</span>
          <span class="nb">count</span>
          <span class="nv">offset</span><span class="p">)))</span>
</code></pre></div></div>

<p>This makes our primary method much tighter, it runs a query and returns results, the <code class="language-plaintext highlighter-rouge">:around</code> method handles the recalculation logic (which is shared between this primary method and the next). Nice and simple.</p>

<h3 id="null-method">null method</h3>

<p>So having seen the form of our new primary methods above, we follow the same patern for the primary method where the user is <code class="language-plaintext highlighter-rouge">null</code>. As before this primary method accepts the count parameter.</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nb">null</span><span class="p">)</span> <span class="k">&amp;key</span> <span class="nv">offset</span> <span class="nv">limit</span> <span class="nb">count</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">sql</span><span class="p">)</span>
      <span class="p">(</span><span class="nv">sxql:yield</span>
        <span class="p">(</span><span class="nv">sxql:select</span>
            <span class="p">(</span><span class="ss">:post.*</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:limit</span> <span class="nv">limit</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:offset</span> <span class="nv">offset</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">values</span>
        <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="nv">sql</span><span class="p">)</span>
        <span class="nb">count</span>
        <span class="nv">offset</span><span class="p">)))</span>
</code></pre></div></div>

<p>The query is simpler, and we do not need to actually pass any variables into the SQL string, so we don’t need the params value returned from the <code class="language-plaintext highlighter-rouge">multiple-value-bind</code>, which means we also don’t need to use the <code class="language-plaintext highlighter-rouge">:binds</code> key argument into <code class="language-plaintext highlighter-rouge">mito:retrieve-by-sql</code>.</p>

<p>And that’s it, that’s our models done!</p>

<h2 id="controllers">Controllers</h2>

<p>Our controller will be the <code class="language-plaintext highlighter-rouge">index</code> controller we built previously, but we need to modify it quite a bit to parse and process the information we need, pagination has a lot of data, and we will need to ensure our templates can present the UI and data in a easy to use manner.</p>

<p>The controller will be so radically different as to be entirely new, it may be easier for you to delete the existing index controller and replace it with what we write here.</p>

<p>The first thing the controller needs to do is grab the <code class="language-plaintext highlighter-rouge">GET</code> parameters and validate them, we follow a basic formula to achieve this for the two parameters we need (page, and limit):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(or (parse-integer (or (ingle:get-param "page" params) "1") :junk-allowed t) 1)
(or (parse-integer (or (ingle:get-param "limit" params) "50") :junk-allowed t) 50)
</code></pre></div></div>

<p>As you can see these are basically identical the only thing that differs are the default values, in the case of page it is <code class="language-plaintext highlighter-rouge">"1"/1</code> for limit it is <code class="language-plaintext highlighter-rouge">"50"/50</code>. To run through the logic we have some basic possibilities we need to handle.</p>

<p>In the case where there is no parameter which will be the case if no <code class="language-plaintext highlighter-rouge">page=x</code> is in url, or the value of <code class="language-plaintext highlighter-rouge">page</code> is not numeric (such as a word, <code class="language-plaintext highlighter-rouge">page=hi</code> or something) the result of <code class="language-plaintext highlighter-rouge">(ingle:get-param "page" params)</code> will be <code class="language-plaintext highlighter-rouge">nil</code>.</p>

<p>In the case where page is provided and is a number, the process is the same, but <code class="language-plaintext highlighter-rouge">(ingle:get-param "page" params)</code> would return a number as a string.</p>

<p>We can see how that would evaluate here:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="no">nil</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="s">"1"</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">(</span><span class="nb">or</span> <span class="mi">1</span> <span class="mi">1</span><span class="p">)</span>
<span class="mi">1</span>
</code></pre></div></div>

<p>The process repeats for the “limit” parameter. It’s a lot of checking and handling, it would be nice if there were a library to handle this for us, but I have not yet found one, perhaps that will be our next topic!</p>

<p>NOTE! In this example we are permitting arbitrary <code class="language-plaintext highlighter-rouge">limit</code> values (we are learning), in practice, this should be limited to a maximum value to prevent users from requesting a page that may result in a Denial Of Service type event. What the exact value should be really depends on the data, it might be fine to get thousands of numbers in one go, but if your models are complicated, a smaller number may be better.</p>

<p>You could do something like this to limit… the limit: <code class="language-plaintext highlighter-rouge">(limit (min 100 (max 1 limit)))</code></p>

<p>The <code class="language-plaintext highlighter-rouge">let</code> binding will therefore look like this</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
      <span class="p">(</span><span class="nv">page</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
      <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"limit"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"50"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">50</span><span class="p">)))</span>
  <span class="o">...</span><span class="p">)</span>
</code></pre></div></div>

<p>With those parameters validated, we can focus on building our paginated controller. Thanks to the work we did in the models we can pull the values out of the <code class="language-plaintext highlighter-rouge">posts</code> method with <code class="language-plaintext highlighter-rouge">multiple-value-bind</code>:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
      <span class="p">(</span><span class="nv">page</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
      <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"limit"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"50"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">50</span><span class="p">)))</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">posts</span> <span class="nb">count</span> <span class="nv">offset</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="p">(</span><span class="nb">*</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)</span> <span class="nv">limit</span><span class="p">)</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">)</span>
    <span class="o">...</span><span class="p">))</span>
</code></pre></div></div>

<p>This enables us to now calculate the various values we need to pass through into a template to render the paginator, we need to generate 6 values.</p>

<h3 id="page">page</h3>

<p>The page variable is a way to determine what the current page is, it is calculated like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(1+ (floor offset limit))
</code></pre></div></div>

<p>From the <code class="language-plaintext highlighter-rouge">offset</code> we get from the <code class="language-plaintext highlighter-rouge">multiple-value-bind</code> we round down the value of dividing offset by the limit and add 1 to the value. If we assume, for example, an offset of 50 and a limit of 50, we can see how the page is determined.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(1+ (floor 50 50))
(1+ 1)
2
</code></pre></div></div>

<p>If we want to see something larger:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(1+ (floor 250 50))
(1+ 5)
6
</code></pre></div></div>

<h3 id="page-count">page count</h3>

<p>The page-count variable is a way to determine the total number of pages:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(max 1 (ceiling count limit))
</code></pre></div></div>

<p>Again, from the <code class="language-plaintext highlighter-rouge">multiple-value-bind</code> we get the <code class="language-plaintext highlighter-rouge">count</code> object, so we can expand this, assuing count is 250 and limit is 50.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(max 1 (ceiling 500 50))
(max 1 10)
10
</code></pre></div></div>

<p>In this manner, given a total number and a page size we want to split it into, we can see the total number of pages.</p>

<h3 id="previous-page-number">previous page number</h3>

<p>Unlike the previous two calculations, prev-page can legitiately be <code class="language-plaintext highlighter-rouge">nil</code>. In the case we are already on the first page there’s no way for there to be a previous page, so <code class="language-plaintext highlighter-rouge">nil</code> is fine. If we need to have some binary conditional logic where <code class="language-plaintext highlighter-rouge">nil</code> is acceptable <code class="language-plaintext highlighter-rouge">when</code> is our friend.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(when (&gt; page 1) (1- page))
</code></pre></div></div>

<p>Wwhen the page is bigger than one, return one less than the value of page, because this is a <code class="language-plaintext highlighter-rouge">when</code> <code class="language-plaintext highlighter-rouge">(1- page)</code> will be returned, or <code class="language-plaintext highlighter-rouge">nil</code> will be.</p>

<h3 id="page-number">page number</h3>

<p>The inverse of the above:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(when (&lt; page page-count) (1+ page))
</code></pre></div></div>

<p>When the page is smaller than the total number of pages, return one more than the value of page, or <code class="language-plaintext highlighter-rouge">nil</code>.</p>

<h3 id="range-start">range start</h3>

<p>Range start is to help the UI, typically in paginators, especially in large ones, there’s a first, last, and current location, but often the current location has some pages to the left and right, this is the range. Now there’s no real <em>right</em> number for the ranges, but I settled on 2.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(max 1 (- page 2))
</code></pre></div></div>

<p>Assuming page is 1, max will return 1, but if we are on, say, page 15, the location the range starts at is 13.</p>

<h3 id="range-end">range end</h3>

<p>Range end behaves like range start, except in the other direction, but we need to ensure we get the minimum of the page-count, in case we are on the last page.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(min page-count (+ page 2))
</code></pre></div></div>

<p>With these defined we can put them in a <code class="language-plaintext highlighter-rouge">let*</code> form.</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
      <span class="p">(</span><span class="nv">page</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
      <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"limit"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"50"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">50</span><span class="p">)))</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">posts</span> <span class="nb">count</span> <span class="nv">offset</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="p">(</span><span class="nb">*</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)</span> <span class="nv">limit</span><span class="p">)</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">page</span> <span class="p">(</span><span class="nb">1+</span> <span class="p">(</span><span class="nb">floor</span> <span class="nv">offset</span> <span class="nv">limit</span><span class="p">)))</span>
           <span class="p">(</span><span class="nv">page-count</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">ceiling</span> <span class="nb">count</span> <span class="nv">limit</span><span class="p">)))</span>
           <span class="p">(</span><span class="nv">prev-page</span> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nv">page</span> <span class="mi">1</span><span class="p">)</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)))</span>
           <span class="p">(</span><span class="nv">next-page</span> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&lt;</span> <span class="nv">page</span> <span class="nv">page-count</span><span class="p">)</span> <span class="p">(</span><span class="nb">1+</span> <span class="nv">page</span><span class="p">)))</span>
           <span class="p">(</span><span class="nv">range-start</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">-</span> <span class="nv">page</span> <span class="mi">2</span><span class="p">)))</span>
           <span class="p">(</span><span class="nv">range-end</span> <span class="p">(</span><span class="nb">min</span> <span class="nv">page-count</span> <span class="p">(</span><span class="nb">+</span> <span class="nv">page</span> <span class="mi">2</span><span class="p">))))</span>
        <span class="o">...</span><span class="p">)))</span>
</code></pre></div></div>

<p>The final thing we need to do is return the result of <code class="language-plaintext highlighter-rouge">djula:render-template*</code>, but there is still more data we need to pass through, build upon the variables we defined, there’s only 5 more.</p>

<h3 id="pages">pages</h3>

<p>Pages is simply a list of all the pages, which is easy enough to generate:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(loop :for idx :from range-start :to range-end :collect idx)
</code></pre></div></div>

<h3 id="show-start-gap">show-start-gap</h3>

<p>The show-start-gap is a boolean that tells the template to render part of the paginator UI.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(&gt; range-start 2)
</code></pre></div></div>

<p>This will return <code class="language-plaintext highlighter-rouge">t</code> or <code class="language-plaintext highlighter-rouge">nil</code> depending on if range-start is larger than 2.</p>

<h3 id="show-end-gap">show-end-gap</h3>

<p>The show-end-gap is the inverse:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(&lt; range-end (1- page-count))
</code></pre></div></div>

<p>This will return <code class="language-plaintext highlighter-rouge">t</code> or <code class="language-plaintext highlighter-rouge">nil</code> depending on if range-end is smaller than <code class="language-plaintext highlighter-rouge">(1- page-count)</code>.</p>

<h3 id="start-index">start-index</h3>

<p>To get the start-index, this is the number starting from the offset so we can display something like “Showing x - y of z”, x would be our start-index.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(if (&gt; count 0) (1+ offset) 0)
</code></pre></div></div>

<p>If the count is bigger than zero then we return one more than the offset, else we return 0 (the default starting offset being 0).</p>

<h3 id="end-index">end-index</h3>

<p>Again, this is the opposite of another thing, the start-index.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(if (&gt; count 0) (min count (+ offset (length posts))) 0)
</code></pre></div></div>

<p>If count is bigger than zero then what we need is the smallest (min) of the count and offset plus the number of posts, or 0. It’s possible there isn’t a complete pages worth of items, so we need to ensure that we don’t over run.</p>

<p>With all that being said, we can now see the complete controller with the values rendered by djula:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defun</span> <span class="nv">index</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">page</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"limit"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"50"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">50</span><span class="p">)))</span>
      <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">posts</span> <span class="nb">count</span> <span class="nv">offset</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="p">(</span><span class="nb">*</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)</span> <span class="nv">limit</span><span class="p">)</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">page</span> <span class="p">(</span><span class="nb">1+</span> <span class="p">(</span><span class="nb">floor</span> <span class="nv">offset</span> <span class="nv">limit</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">page-count</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">ceiling</span> <span class="nb">count</span> <span class="nv">limit</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">prev-page</span> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nv">page</span> <span class="mi">1</span><span class="p">)</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">next-page</span> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&lt;</span> <span class="nv">page</span> <span class="nv">page-count</span><span class="p">)</span> <span class="p">(</span><span class="nb">1+</span> <span class="nv">page</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">range-start</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">-</span> <span class="nv">page</span> <span class="mi">2</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">range-end</span> <span class="p">(</span><span class="nb">min</span> <span class="nv">page-count</span> <span class="p">(</span><span class="nb">+</span> <span class="nv">page</span> <span class="mi">2</span><span class="p">))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span>
            <span class="s">"main/index.html"</span>
            <span class="no">nil</span>
            <span class="ss">:title</span> <span class="s">"Home"</span>
            <span class="ss">:user</span> <span class="nv">user</span>
            <span class="ss">:posts</span> <span class="nv">posts</span>
            <span class="ss">:form</span> <span class="p">(</span><span class="k">if</span> <span class="nv">user</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)</span> <span class="no">nil</span><span class="p">)</span>
            <span class="ss">:count</span> <span class="nb">count</span>
            <span class="ss">:page</span> <span class="nv">page</span>
            <span class="ss">:limit</span> <span class="nv">limit</span>
            <span class="ss">:page-count</span> <span class="nv">page-count</span>
            <span class="ss">:prev-page</span> <span class="nv">prev-page</span>
            <span class="ss">:next-page</span> <span class="nv">next-page</span>
            <span class="ss">:pages</span> <span class="p">(</span><span class="nb">loop</span> <span class="ss">:for</span> <span class="nv">idx</span> <span class="ss">:from</span> <span class="nv">range-start</span> <span class="ss">:to</span> <span class="nv">range-end</span> <span class="ss">:collect</span> <span class="nv">idx</span><span class="p">)</span>
            <span class="ss">:show-start-gap</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nv">range-start</span> <span class="mi">2</span><span class="p">)</span>
            <span class="ss">:show-end-gap</span> <span class="p">(</span><span class="nb">&lt;</span> <span class="nv">range-end</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page-count</span><span class="p">))</span>
            <span class="ss">:start-index</span> <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nb">count</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nb">1+</span> <span class="nv">offset</span><span class="p">)</span> <span class="mi">0</span><span class="p">)</span>
            <span class="ss">:end-index</span> <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nb">count</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nb">min</span> <span class="nb">count</span> <span class="p">(</span><span class="nb">+</span> <span class="nv">offset</span> <span class="p">(</span><span class="nb">length</span> <span class="nv">posts</span><span class="p">)))</span> <span class="mi">0</span><span class="p">))))))</span>
</code></pre></div></div>

<p>I would have thought that having an invalid number would have triggered a 404, or perhaps a 400, but having tested this with Google, it seems that the convention is to default to page 1. So with that said and the controller in place, we can now write our templates.</p>

<h2 id="templates">Templates</h2>

<h3 id="index-srctemplatesmainindexhtml">index (src/templates/main/index.html)</h3>

<p>Our index template doesn’t require much change at all, we need to only add an <code class="language-plaintext highlighter-rouge">include</code> (from djula) to include the contents of one template inside another. Of course we have still to write the pagination template, but that is just below.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code> {% extends "base.html" %}

{% block content %}
 &lt;div class="container"&gt;
    &lt;!-- Post form --&gt;
    &lt;div class="row mb-4"&gt;
        &lt;div class="col"&gt;
            {% if form %}
                {% form form %}
            {% endif %}
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Posts Section --&gt;
<span class="gi">+   {% include "partials/pagination.html" with url="/" title="Posts" %}
</span>    &lt;div class="row"&gt;
    ...
    &lt;/div&gt;
<span class="gi">+   {% include "partials/pagination.html" with url="/" title="Posts" %}
</span> &lt;/div&gt;
 {% endblock %}
</code></pre></div></div>

<p>Something to bear in mind here is the way this is designed is that if you need to pass in some data, in our case <code class="language-plaintext highlighter-rouge">url</code>, and <code class="language-plaintext highlighter-rouge">title</code>, we can pass through these things, we will use these in the pagination html partial.</p>

<h3 id="pagination-srctemplatespartialspaginationhtml">pagination (src/templates/partials/pagination.html)</h3>

<p>Partials are a way to include reusable parts of html presentation in a template, they help us build isolated pieces of presentation logic that we might want to use over and over again all over our application, this is why we save them in a partials folder, because they are a partial piece of presentation logic.</p>

<p>This is the magic that makes the UI work, while we showed were it would be used in the <code class="language-plaintext highlighter-rouge">index.html</code> page, we need to look into what it does. I do use bootstrap to make things look nice, but I’m very much NOT a frontend engineer, so I can’t speak to how to make something look good without it, so inevitably much of the classes and UI come from <a href="https://getbootstrap.com/docs/5.3/components/pagination/">Bootstrap</a>.</p>

<p>I will have to break the html down piece by piece to explain what it’s all doing, but look at the final listing to see the complete file.</p>

<p>From the values we calculated though, we start by checking if the page count is bigger than <code class="language-plaintext highlighter-rouge">1</code>, because if we have less than two pages, we can’t paginate, therefore the whole UI is wrapped in:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
{% if page-count &gt; 1%}
    ...
{% endif %}

</code></pre></div></div>

<p>With that we can use the <code class="language-plaintext highlighter-rouge">start-index</code>, <code class="language-plaintext highlighter-rouge">end-index</code>, and <code class="language-plaintext highlighter-rouge">count</code>, to display the human readable part of the paginator.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
{% if page-count &gt; 1%}
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"table-pagination"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"pagination-summary"</span><span class="nt">&gt;</span>
      Showing {{ start-index }}-{{ end-index }} of {{ count }}
    <span class="nt">&lt;/div&gt;</span>
    ...
{% endif %}

</code></pre></div></div>
<p>We then setup a <code class="language-plaintext highlighter-rouge">nav</code>, with a single <code class="language-plaintext highlighter-rouge">ul</code> object in it, with which we define our parts of the paginator as <code class="language-plaintext highlighter-rouge">li</code> tags.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
{% if page-count &gt; 1%}
    ...
    <span class="nt">&lt;nav</span> <span class="na">aria-label=</span><span class="s">"{{ title }} pagination"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"pagination"</span><span class="nt">&gt;</span>
    ...
{% endif %}

</code></pre></div></div>

<p>Within this <code class="language-plaintext highlighter-rouge">ul</code>, we have to put all of our <code class="language-plaintext highlighter-rouge">li</code> elements which will contain the aspects of the UI. The first such item is:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    ...
    <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"pagination"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if not prev-page %} disabled{% endif %}"</span><span class="nt">&gt;</span>
          {% if prev-page %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ prev-page }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>Prev<span class="nt">&lt;/a&gt;</span>
          {% else %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>Prev<span class="nt">&lt;/span&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>
        ...
    <span class="nt">&lt;/ul&gt;</span>
</code></pre></div></div>

<p>This first <code class="language-plaintext highlighter-rouge">li</code> will set the <code class="language-plaintext highlighter-rouge">disabled</code> css class if the <code class="language-plaintext highlighter-rouge">prev-page</code> is not <code class="language-plaintext highlighter-rouge">nil</code>. It will again rely on <code class="language-plaintext highlighter-rouge">prev-page</code> to either render an <code class="language-plaintext highlighter-rouge">a</code> tag building the url up, including the <code class="language-plaintext highlighter-rouge">prev-page</code>, and <code class="language-plaintext highlighter-rouge">limit</code>, else a <code class="language-plaintext highlighter-rouge">span</code> is rendered. This sets up the first element in the pagination UI.</p>

<p>The second <code class="language-plaintext highlighter-rouge">li</code> item checks the <code class="language-plaintext highlighter-rouge">page</code>, and if it is the first page, it sets the <code class="language-plaintext highlighter-rouge">active</code> class and renders a <code class="language-plaintext highlighter-rouge">span</code>, if it is NOT 1 then a link to the first page is rendered with a <code class="language-plaintext highlighter-rouge">a</code> tag, building up the url as we did before.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if page == 1 %} active{% endif %}"</span><span class="nt">&gt;</span>
          {% if page == 1 %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>1<span class="nt">&lt;/span&gt;</span>
          {% else %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page=1&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>1<span class="nt">&lt;/a&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>
...
</code></pre></div></div>

<p>Now that we have gotten the beginning of the paginator with a “Prev” <code class="language-plaintext highlighter-rouge">li</code> element and the first <code class="language-plaintext highlighter-rouge">li</code> element, we <em>might</em> need to render an elipsis (…) if the number of our pages is too large. We will repeat this pattern later on, in reverse, we will use the <code class="language-plaintext highlighter-rouge">show-start-gap</code> boolean to render the <code class="language-plaintext highlighter-rouge">...</code>.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
        {% if show-start-gap %}
          <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item disabled"</span><span class="nt">&gt;&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/span&gt;&lt;/li&gt;</span>
        {% endif %}
...
</code></pre></div></div>

<p>With that done, we can now render the page numbers:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        {% for p in pages %}
          {% if p != 1 and p != page-count %}
            <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if p == page %} active{% endif %}"</span><span class="nt">&gt;</span>
              {% if p == page %}
                <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>{{ p }}<span class="nt">&lt;/span&gt;</span>
              {% else %}
                <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ p }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>{{ p }}<span class="nt">&lt;/a&gt;</span>
              {% endif %}
            <span class="nt">&lt;/li&gt;</span>
          {% endif %}
        {% endfor %}
</code></pre></div></div>

<p>We loop over the list of page numbers we passed into the template as <code class="language-plaintext highlighter-rouge">pages</code>, if the loop iteration is NOT the first page (remember that this is a list of page numbers and starts from 1, not 0) and the loop iteration is not the current page, then we will render the <code class="language-plaintext highlighter-rouge">li</code> tag. If we just so happen to be on the loop iteration that is the current page (<code class="language-plaintext highlighter-rouge">page</code>), we render a <code class="language-plaintext highlighter-rouge">span</code> tag and not a link, else we render a link so that we can directly navigate to this element in the paginator.</p>

<p>We then render the <code class="language-plaintext highlighter-rouge">show-end-gap</code>, using the pattern we used above:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
        {% if show-end-gap %}
          <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item disabled"</span><span class="nt">&gt;&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/span&gt;&lt;/li&gt;</span>
        {% endif %}
...
</code></pre></div></div>

<p>This will render an elipsis (…) where needed.</p>

<p>Now to the final page in the paginator, we must check if we are on the final page, which, as we have seen before, we do in the class line, and to determine if we render a <code class="language-plaintext highlighter-rouge">span</code> tag if we are on the final page, or a <code class="language-plaintext highlighter-rouge">a</code> tag if we are not.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
      <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if page == page-count %} active{% endif %}"</span><span class="nt">&gt;</span>
          {% if page == page-count %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>{{ page-count }}<span class="nt">&lt;/span&gt;</span>
          {% else %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ page-count }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>{{ page-count }}<span class="nt">&lt;/a&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>
...
</code></pre></div></div>

<p>And finally, we must render the “Next” part of the pagination:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if not next-page %} disabled{% endif %}"</span><span class="nt">&gt;</span>
          {% if next-page %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ next-page }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>Next<span class="nt">&lt;/a&gt;</span>
          {% else %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>Next<span class="nt">&lt;/span&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>
...
</code></pre></div></div>

<p>If there is NOT a next page we add the disabled class, we then, as we have seen before use the <code class="language-plaintext highlighter-rouge">next-page</code> variable to determine if we render an <code class="language-plaintext highlighter-rouge">a</code> tag, or a <code class="language-plaintext highlighter-rouge">span</code> tag.</p>

<h2 id="full-listings">Full Listings</h2>

<p>To see how all of this comes together here are the files in their entirety.</p>

<h4 id="modelslisp">models.lisp</h4>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:ningle-auth/models</span> <span class="ss">#:user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:post</span>
           <span class="ss">#:id</span>
           <span class="ss">#:content</span>
           <span class="ss">#:comments</span>
           <span class="ss">#:likes</span>
           <span class="ss">#:user</span>
           <span class="ss">#:liked-post-p</span>
           <span class="ss">#:posts</span>
           <span class="ss">#:parent</span>
           <span class="ss">#:toggle-like</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/models</span><span class="p">)</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">post</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span>    <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span>    <span class="ss">:accessor</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">parent</span>  <span class="ss">:col-type</span> <span class="p">(</span><span class="nb">or</span> <span class="ss">:post</span> <span class="ss">:null</span><span class="p">)</span>        <span class="ss">:initarg</span> <span class="ss">:parent</span>  <span class="ss">:reader</span> <span class="nv">parent</span> <span class="ss">:initform</span> <span class="no">nil</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">content</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">140</span><span class="p">)</span>          <span class="ss">:initarg</span> <span class="ss">:content</span> <span class="ss">:accessor</span> <span class="nv">content</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">likes</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span> <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span> <span class="ss">:reader</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">post</span> <span class="ss">:col-type</span> <span class="nv">post</span>                    <span class="ss">:initarg</span> <span class="ss">:post</span> <span class="ss">:reader</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">likes</span> <span class="p">(</span><span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns the number of likes a post has"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">likes</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:count-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">comments</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Gets the comments for a logged in user"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">comments</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">)</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
            <span class="p">(</span><span class="nv">sxql:select</span>
                <span class="p">(</span><span class="ss">:post.*</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:parent</span> <span class="ss">:?</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="ss">:?</span><span class="p">)))</span>
                <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
            <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">comments</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">)</span> <span class="p">(</span><span class="nv">user</span> <span class="nb">null</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
        <span class="p">(</span><span class="nv">sxql:select</span>
            <span class="p">(</span><span class="ss">:post.*</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:parent</span> <span class="ss">:?</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
        <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">toggle-like</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Toggles the like of a user to a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">toggle-like</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">liked-post</span> <span class="p">(</span><span class="nv">liked-post-p</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="nv">liked-post</span>
        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">liked-post</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">not</span> <span class="nv">liked-post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">liked-post-p</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns true if a user likes a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">liked-post-p</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'likes</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">posts</span> <span class="p">(</span><span class="nv">user</span> <span class="k">&amp;key</span> <span class="nv">offset</span> <span class="nv">limit</span> <span class="nb">count</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Gets the posts"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="ss">:around</span> <span class="p">(</span><span class="nv">user</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">offset</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nv">limit</span> <span class="mi">50</span><span class="p">)</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nb">count</span> <span class="p">(</span><span class="nv">mito:count-dao</span> <span class="ss">'post</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">offset</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">0</span> <span class="nv">offset</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="nv">limit</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nb">count</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nb">&gt;=</span> <span class="nv">offset</span> <span class="nb">count</span><span class="p">))</span>
      <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">page-count</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">ceiling</span> <span class="nb">count</span> <span class="nv">limit</span><span class="p">)))</span>
             <span class="p">(</span><span class="nv">corrected-offset</span> <span class="p">(</span><span class="nb">*</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page-count</span><span class="p">)</span> <span class="nv">limit</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="nv">corrected-offset</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">))</span>
      <span class="p">(</span><span class="nb">call-next-method</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="nv">offset</span> <span class="ss">:limit</span> <span class="nv">limit</span> <span class="ss">:count</span> <span class="nb">count</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">)</span> <span class="k">&amp;key</span> <span class="nv">offset</span> <span class="nv">limit</span> <span class="nb">count</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">sql</span> <span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
              <span class="p">(</span><span class="nv">sxql:select</span>
                  <span class="p">(</span><span class="ss">:post.*</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                  <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                            <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))</span>
                  <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                  <span class="p">(</span><span class="nv">sxql:offset</span> <span class="nv">offset</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">sxql:limit</span> <span class="nv">limit</span><span class="p">)))</span>
      <span class="p">(</span><span class="nb">values</span>
          <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="nv">sql</span> <span class="ss">:binds</span> <span class="nv">params</span><span class="p">)</span>
          <span class="nb">count</span>
          <span class="nv">offset</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nb">null</span><span class="p">)</span> <span class="k">&amp;key</span> <span class="nv">offset</span> <span class="nv">limit</span> <span class="nb">count</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">sql</span><span class="p">)</span>
      <span class="p">(</span><span class="nv">sxql:yield</span>
        <span class="p">(</span><span class="nv">sxql:select</span>
            <span class="p">(</span><span class="ss">:post.*</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:limit</span> <span class="nv">limit</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:offset</span> <span class="nv">offset</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">values</span>
        <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="nv">sql</span><span class="p">)</span>
        <span class="nb">count</span>
        <span class="nv">offset</span><span class="p">)))</span>
</code></pre></div></div>

<h4 id="controllerslisp">controllers.lisp</h4>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/controllers</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:ningle-tutorial-project/forms</span>
                <span class="ss">#:post</span>
                <span class="ss">#:content</span>
                <span class="ss">#:parent</span>
                <span class="ss">#:comment</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:index</span>
           <span class="ss">#:post-likes</span>
           <span class="ss">#:single-post</span>
           <span class="ss">#:post-content</span>
           <span class="ss">#:post-comment</span>
           <span class="ss">#:logged-in-profile</span>
           <span class="ss">#:unauthorized-profile</span>
           <span class="ss">#:people</span>
           <span class="ss">#:person</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/controllers</span><span class="p">)</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">index</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">page</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"page"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"1"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">limit</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="s">"limit"</span> <span class="nv">params</span><span class="p">)</span> <span class="s">"50"</span><span class="p">)</span> <span class="ss">:junk-allowed</span> <span class="no">t</span><span class="p">)</span> <span class="mi">50</span><span class="p">)))</span>
      <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">posts</span> <span class="nb">count</span> <span class="nv">offset</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:posts</span> <span class="nv">user</span> <span class="ss">:offset</span> <span class="p">(</span><span class="nb">*</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)</span> <span class="nv">limit</span><span class="p">)</span> <span class="ss">:limit</span> <span class="nv">limit</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">page</span> <span class="p">(</span><span class="nb">1+</span> <span class="p">(</span><span class="nb">floor</span> <span class="nv">offset</span> <span class="nv">limit</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">page-count</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">ceiling</span> <span class="nb">count</span> <span class="nv">limit</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">prev-page</span> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nv">page</span> <span class="mi">1</span><span class="p">)</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">next-page</span> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&lt;</span> <span class="nv">page</span> <span class="nv">page-count</span><span class="p">)</span> <span class="p">(</span><span class="nb">1+</span> <span class="nv">page</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">range-start</span> <span class="p">(</span><span class="nb">max</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">-</span> <span class="nv">page</span> <span class="mi">2</span><span class="p">)))</span>
               <span class="p">(</span><span class="nv">range-end</span> <span class="p">(</span><span class="nb">min</span> <span class="nv">page-count</span> <span class="p">(</span><span class="nb">+</span> <span class="nv">page</span> <span class="mi">2</span><span class="p">))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span>
            <span class="s">"main/index.html"</span>
            <span class="no">nil</span>
            <span class="ss">:title</span> <span class="s">"Home"</span>
            <span class="ss">:user</span> <span class="nv">user</span>
            <span class="ss">:posts</span> <span class="nv">posts</span>
            <span class="ss">:form</span> <span class="p">(</span><span class="k">if</span> <span class="nv">user</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)</span> <span class="no">nil</span><span class="p">)</span>
            <span class="ss">:count</span> <span class="nb">count</span>
            <span class="ss">:page</span> <span class="nv">page</span>
            <span class="ss">:limit</span> <span class="nv">limit</span>
            <span class="ss">:page-count</span> <span class="nv">page-count</span>
            <span class="ss">:prev-page</span> <span class="nv">prev-page</span>
            <span class="ss">:next-page</span> <span class="nv">next-page</span>
            <span class="ss">:pages</span> <span class="p">(</span><span class="nb">loop</span> <span class="ss">:for</span> <span class="nv">idx</span> <span class="ss">:from</span> <span class="nv">range-start</span> <span class="ss">:to</span> <span class="nv">range-end</span> <span class="ss">:collect</span> <span class="nv">idx</span><span class="p">)</span>
            <span class="ss">:show-start-gap</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nv">range-start</span> <span class="mi">2</span><span class="p">)</span>
            <span class="ss">:show-end-gap</span> <span class="p">(</span><span class="nb">&lt;</span> <span class="nv">range-end</span> <span class="p">(</span><span class="nb">1-</span> <span class="nv">page-count</span><span class="p">))</span>
            <span class="ss">:start-index</span> <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nb">count</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nb">1+</span> <span class="nv">offset</span><span class="p">)</span> <span class="mi">0</span><span class="p">)</span>
            <span class="ss">:end-index</span> <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="nb">count</span> <span class="mi">0</span><span class="p">)</span> <span class="p">(</span><span class="nb">min</span> <span class="nb">count</span> <span class="p">(</span><span class="nb">+</span> <span class="nv">offset</span> <span class="p">(</span><span class="nb">length</span> <span class="nv">posts</span><span class="p">)))</span> <span class="mi">0</span><span class="p">))))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-likes</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
         <span class="p">(</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
         <span class="p">(</span><span class="nv">res</span> <span class="p">(</span><span class="nb">make-hash-table</span> <span class="ss">:test</span> <span class="ss">'equal</span><span class="p">)))</span>
    <span class="c1">;; Bail out if post does not exist</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">post</span>
      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">getf</span> <span class="p">(</span><span class="nv">lack.response:response-headers</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="ss">:content-type</span><span class="p">)</span> <span class="s">"application/json"</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"error"</span> <span class="nv">res</span><span class="p">)</span> <span class="s">"post not found"</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
      <span class="p">(</span><span class="k">return-from</span> <span class="nv">post-likes</span> <span class="p">(</span><span class="nv">com.inuoe.jzon.stringify</span> <span class="nv">res</span><span class="p">)))</span>

    <span class="c1">;; success, continue</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"post"</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"liked"</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:toggle-like</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"likes"</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">getf</span> <span class="p">(</span><span class="nv">lack.response:response-headers</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="ss">:content-type</span><span class="p">)</span> <span class="s">"application/json"</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">201</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">com.inuoe.jzon:stringify</span> <span class="nv">res</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">single-post</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">handler-case</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
              <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'comment</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">cl-forms:set-field-value</span> <span class="nv">form</span> <span class="ss">'ningle-tutorial-project/forms:parent</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/post.html"</span> <span class="no">nil</span>
                                  <span class="ss">:title</span> <span class="s">"Post"</span>
                                  <span class="ss">:post</span> <span class="nv">post</span>
                                  <span class="ss">:comments</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:comments</span> <span class="nv">post</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
                                  <span class="ss">:likes</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">)</span>
                                  <span class="ss">:form</span> <span class="nv">form</span>
                                  <span class="ss">:user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>

        <span class="p">(</span><span class="kt">parse-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-content</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
                <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span><span class="p">)</span> <span class="nv">form</span>
                            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:parent</span> <span class="no">nil</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-comment</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'comment</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
                <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span> <span class="nv">parent</span><span class="p">)</span> <span class="nv">form</span>
                            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:parent</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="nv">parent</span><span class="p">))</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">logged-in-profile</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">unauthorized-profile</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">people</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">person</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">username-or-email</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">person</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                            <span class="ss">'ningle-auth/models:user</span>
                            <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username-or-email</span><span class="p">)</span>
                                        <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">username-or-email</span><span class="p">)))))))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:person</span> <span class="nv">person</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">))))</span>
</code></pre></div></div>

<h4 id="indexhtml">index.html</h4>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="c">&lt;!-- Post form --&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
            {% if form %}
                {% form form %}
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="c">&lt;!-- Posts Section --&gt;</span>
    {% include "partials/pagination.html" with url="/" title="Posts" %}
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            {% for post in posts %}
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card post mb-3"</span> <span class="na">data-href=</span><span class="s">"/post/{{ post.id }}"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title mb-2"</span><span class="nt">&gt;</span>{{ post.content }}<span class="nt">&lt;/h5&gt;</span>
                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-subtitle text-muted mb-0"</span><span class="nt">&gt;</span>@{{ post.username }}<span class="nt">&lt;/p&gt;</span>
                <span class="nt">&lt;/div&gt;</span>

                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-footer d-flex justify-content-between align-items-center"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"button"</span>
                        <span class="na">class=</span><span class="s">"btn btn-sm btn-outline-primary like-button"</span>
                        <span class="na">data-post-id=</span><span class="s">"{{ post.id }}"</span>
                        <span class="na">data-logged-in=</span><span class="s">"{% if user.username != "</span><span class="err">"</span> <span class="err">%}</span><span class="na">true</span><span class="err">{%</span> <span class="na">else</span> <span class="err">%}</span><span class="na">false</span><span class="err">{%</span> <span class="na">endif</span> <span class="err">%}"</span>
                        <span class="na">data-liked=</span><span class="s">"{% if post.liked-by-user == 1 %}1{% else %}0{% endif %}"</span>
                        <span class="na">aria-label=</span><span class="s">"Like post {{ post.id }}"</span><span class="nt">&gt;</span>
                    {% if post.liked-by-user == 1 %}
                      <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up-fill text-primary"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                    {% else %}
                      <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up text-muted"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                    {% endif %}
                    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"ms-1 like-count"</span><span class="nt">&gt;</span>{{ post.like-count }}<span class="nt">&lt;/span&gt;</span>
                <span class="nt">&lt;/button&gt;</span>

                <span class="nt">&lt;small</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>Posted on: {{ post.created-at }}<span class="nt">&lt;/small&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
            {% endfor %}

            {% if not posts %}
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>No posts to display.<span class="nt">&lt;/p&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    {% include "partials/pagination.html" with url="/" title="Posts" %}
<span class="nt">&lt;/div&gt;</span>
{% endblock %}

{% block js %}
document.querySelectorAll(".like-button").forEach(btn =&gt; {
  btn.addEventListener("click", function (e) {
    e.stopPropagation();
    e.preventDefault();

    // Check login
    if (btn.dataset.loggedIn !== "true") {
      alert("You must be logged in to like posts.");
      return;
    }

    const postId = btn.dataset.postId;
    const countSpan = btn.querySelector(".like-count");
    const icon = btn.querySelector("i");
    const liked = Number(btn.dataset.liked) === 1;
    const previous = parseInt(countSpan.textContent, 10) || 0;
    const url = `/post/${postId}/likes`;

    // Optimistic UI toggle
    countSpan.textContent = liked ? previous - 1 : previous + 1;
    btn.dataset.liked = liked ? "false" : "true";

    // Toggle icon classes optimistically
    if (liked) {
      // Currently liked, so unlike it
      icon.className = "bi bi-hand-thumbs-up text-muted";
    } else {
      // Currently not liked, so like it
      icon.className = "bi bi-hand-thumbs-up-fill text-primary";
    }

    const csrfTokenMeta = document.querySelector('meta[name="csrf-token"]');
    const headers = { "Content-Type": "application/json" };
    if (csrfTokenMeta) headers["X-CSRF-Token"] = csrfTokenMeta.getAttribute("content");

    fetch(url, {
      method: "POST",
      headers: headers,
      body: JSON.stringify({ toggle: true })
    })
    .then(resp =&gt; {
      if (!resp.ok) {
        // Revert optimistic changes on error
        countSpan.textContent = previous;
        btn.dataset.liked = liked ? 1 : 0;
        if (liked) {
          icon.className = "bi bi-hand-thumbs-up-fill text-primary";
        } else {
          icon.className = "bi bi-hand-thumbs-up text-muted";
        }
        throw new Error("Network response was not ok");
      }
      return resp.json();
    })
    .then(data =&gt; {
      if (data <span class="err">&amp;&amp;</span> typeof data.likes !== "undefined") {
        countSpan.textContent = data.likes;
        btn.dataset.liked = data.liked ? "true" : "false";

        // Update icon based on server response
        if (data.liked) {
          icon.className = "bi bi-hand-thumbs-up-fill text-primary";
        } else {
          icon.className = "bi bi-hand-thumbs-up text-muted";
        }
      }
    })
    .catch(err =&gt; {
      console.error("Like failed:", err);
      // Revert optimistic changes on error
      countSpan.textContent = previous;
      btn.dataset.liked = liked ? 1 : 0;
      if (liked) {
        icon.className = "bi bi-hand-thumbs-up-fill text-primary";
      } else {
        icon.className = "bi bi-hand-thumbs-up text-muted";
      }
    });
  });
});

document.querySelectorAll(".card.post").forEach(card =&gt; {
  card.addEventListener("click", function () {
    const href = card.dataset.href;
    if (href) {
      window.location.href = href;
    }
  });
});
{% endblock %}
</code></pre></div></div>

<h4 id="paginationhtml">pagination.html</h4>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% if page-count &gt; 1 %}
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"table-pagination"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"pagination-summary"</span><span class="nt">&gt;</span>
      Showing {{ start-index }}-{{ end-index }} of {{ count }}
    <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;nav</span> <span class="na">aria-label=</span><span class="s">"{{ title }} pagination"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"pagination"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if not prev-page %} disabled{% endif %}"</span><span class="nt">&gt;</span>
          {% if prev-page %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ prev-page }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>Prev<span class="nt">&lt;/a&gt;</span>
          {% else %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>Prev<span class="nt">&lt;/span&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>

        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if page == 1 %} active{% endif %}"</span><span class="nt">&gt;</span>
          {% if page == 1 %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>1<span class="nt">&lt;/span&gt;</span>
          {% else %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page=1&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>1<span class="nt">&lt;/a&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>

        {% if show-start-gap %}
          <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item disabled"</span><span class="nt">&gt;&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/span&gt;&lt;/li&gt;</span>
        {% endif %}

        {% for p in pages %}
          {% if p != 1 and p != page-count %}
            <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if p == page %} active{% endif %}"</span><span class="nt">&gt;</span>
              {% if p == page %}
                <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>{{ p }}<span class="nt">&lt;/span&gt;</span>
              {% else %}
                <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ p }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>{{ p }}<span class="nt">&lt;/a&gt;</span>
              {% endif %}
            <span class="nt">&lt;/li&gt;</span>
          {% endif %}
        {% endfor %}

        {% if show-end-gap %}
          <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item disabled"</span><span class="nt">&gt;&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/span&gt;&lt;/li&gt;</span>
        {% endif %}

        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if page == page-count %} active{% endif %}"</span><span class="nt">&gt;</span>
          {% if page == page-count %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>{{ page-count }}<span class="nt">&lt;/span&gt;</span>
          {% else %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ page-count }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>{{ page-count }}<span class="nt">&lt;/a&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>

        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"page-item{% if not next-page %} disabled{% endif %}"</span><span class="nt">&gt;</span>
          {% if next-page %}
            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"page-link"</span> <span class="na">href=</span><span class="s">"{{ url }}?page={{ next-page }}&amp;limit={{ limit }}"</span><span class="nt">&gt;</span>Next<span class="nt">&lt;/a&gt;</span>
          {% else %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"page-link"</span><span class="nt">&gt;</span>Next<span class="nt">&lt;/span&gt;</span>
          {% endif %}
        <span class="nt">&lt;/li&gt;</span>
      <span class="nt">&lt;/ul&gt;</span>
    <span class="nt">&lt;/nav&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
{% endif %}
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>

<p>Phew, that was a long one, and honestly it kinda got into the weeds a bit, thank you for persisting with it and following it to the end. It took quite a while to study and get right. As you no doubt felt while writing it, there was a LOT of calculations and data being passed into the template, and it would be awful to have to repeat that everywhere you wanted to perform pagination, but don’t worry in part 2, this is what we want to try and solve. A more generalised pagination system that doesn’t require quite so much logic in the controllers.</p>

<p>If you found this lesson helpful, consider experimenting with different page sizes or adding pagination to the comments on individual posts. The patterns we’ve established here are reusable throughout your application.</p>

<p>If you found bugs or issues, please do let me know, I correct things when told and I try to fix things as quickly as possible.</p>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Learning Outcome</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Understand</strong></td>
      <td>Understand how SQL <code class="language-plaintext highlighter-rouge">LIMIT</code> and <code class="language-plaintext highlighter-rouge">OFFSET</code> work together to enable pagination, and how query parameters like <code class="language-plaintext highlighter-rouge">?page=2&amp;limit=50</code> map to database queries through SXQL’s <code class="language-plaintext highlighter-rouge">(sxql:limit n)</code> and <code class="language-plaintext highlighter-rouge">(sxql:offset n)</code> forms.</td>
    </tr>
    <tr>
      <td><strong>Apply</strong></td>
      <td>Apply CLOS method combination (<code class="language-plaintext highlighter-rouge">:around</code> methods with <code class="language-plaintext highlighter-rouge">call-next-method</code>) to implement parameter validation and error recovery, ensuring offset never exceeds total count and calculating corrected page numbers when needed.</td>
    </tr>
    <tr>
      <td><strong>Analyse</strong></td>
      <td>Analyse the mathematical relationships in pagination (page-to-offset conversion, range calculations, gap detection) and trace how values flow through the <code class="language-plaintext highlighter-rouge">:around</code> method, primary methods, controller calculations, and template rendering.</td>
    </tr>
    <tr>
      <td><strong>Create</strong></td>
      <td>Create a complete pagination system by combining <code class="language-plaintext highlighter-rouge">:around</code> methods, SQL queries with LIMIT/OFFSET, controller calculations (page/offset conversions, range calculations), and reusable template partials that handle edge cases like invalid page numbers and single-page results.</td>
    </tr>
  </tbody>
</table>

<h2 id="github">Github</h2>

<ul>
  <li>The link for the custom pagination part of the tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-14.1">here</a>.</li>
</ul>

<h3 id="common-lisp-hyperspec">Common Lisp HyperSpec</h3>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Type</th>
      <th>Why it appears in this lesson</th>
      <th>CLHS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defpackage</code></td>
      <td>Macro</td>
      <td>Define project packages like <code class="language-plaintext highlighter-rouge">ningle-tutorial-project/models</code>, <code class="language-plaintext highlighter-rouge">/forms</code>, <code class="language-plaintext highlighter-rouge">/controllers</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">in-package</code></td>
      <td>Macro</td>
      <td>Enter each package before defining models, controllers, and functions.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defgeneric</code></td>
      <td>Macro</td>
      <td>Define the generic <code class="language-plaintext highlighter-rouge">posts</code> function signature with keyword parameters <code class="language-plaintext highlighter-rouge">offset</code>, <code class="language-plaintext highlighter-rouge">limit</code>, and <code class="language-plaintext highlighter-rouge">count</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defmethod</code></td>
      <td>Macro</td>
      <td>Implement specialized <code class="language-plaintext highlighter-rouge">posts</code> methods for <code class="language-plaintext highlighter-rouge">user</code> and <code class="language-plaintext highlighter-rouge">null</code> types, and the <code class="language-plaintext highlighter-rouge">:around</code> method for validation.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">call-next-method</code></td>
      <td>Function</td>
      <td>Invoke the next most specific method from within the <code class="language-plaintext highlighter-rouge">:around</code> method after validating parameters.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_call_n.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_call_n.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let</code></td>
      <td>Special Operator</td>
      <td>Bind local variables in the <code class="language-plaintext highlighter-rouge">:around</code> method (<code class="language-plaintext highlighter-rouge">count</code>, <code class="language-plaintext highlighter-rouge">offset</code>, <code class="language-plaintext highlighter-rouge">limit</code>) and controller (<code class="language-plaintext highlighter-rouge">user</code>, <code class="language-plaintext highlighter-rouge">page</code>, <code class="language-plaintext highlighter-rouge">limit</code>).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let*</code></td>
      <td>Special Operator</td>
      <td>Sequentially bind pagination calculations (<code class="language-plaintext highlighter-rouge">page</code>, <code class="language-plaintext highlighter-rouge">page-count</code>, <code class="language-plaintext highlighter-rouge">prev-page</code>, etc.) where each depends on previous values.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">if</code></td>
      <td>Special Operator</td>
      <td>Check conditions like whether offset exceeds count, or whether count is greater than zero.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_if.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_if.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">when</code></td>
      <td>Macro</td>
      <td>Calculate <code class="language-plaintext highlighter-rouge">prev-page</code> and <code class="language-plaintext highlighter-rouge">next-page</code> only when the condition is true, returning <code class="language-plaintext highlighter-rouge">nil</code> otherwise.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">or</code></td>
      <td>Macro</td>
      <td>Provide fallback values when parsing <code class="language-plaintext highlighter-rouge">page</code> and <code class="language-plaintext highlighter-rouge">limit</code> parameters, defaulting to 1 and 50 respectively.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_or.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_or.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">and</code></td>
      <td>Macro</td>
      <td>Check multiple conditions in the <code class="language-plaintext highlighter-rouge">:around</code> method (count &gt; 0 AND offset &gt;= count) before recalculating.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_and.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_and.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">multiple-value-bind</code></td>
      <td>Macro</td>
      <td>Capture the three return values from <code class="language-plaintext highlighter-rouge">posts</code> (<code class="language-plaintext highlighter-rouge">posts</code>, <code class="language-plaintext highlighter-rouge">count</code>, <code class="language-plaintext highlighter-rouge">offset</code>) and from <code class="language-plaintext highlighter-rouge">sxql:yield</code> (<code class="language-plaintext highlighter-rouge">sql</code>, <code class="language-plaintext highlighter-rouge">params</code>).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_multip.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_multip.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">values</code></td>
      <td>Function</td>
      <td>Return multiple values from <code class="language-plaintext highlighter-rouge">posts</code> methods (results, count, offset) to the caller.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/a_values.htm">http://www.lispworks.com/documentation/HyperSpec/Body/a_values.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">loop</code></td>
      <td>Macro</td>
      <td>Generate the list of page numbers from <code class="language-plaintext highlighter-rouge">range-start</code> to <code class="language-plaintext highlighter-rouge">range-end</code> for template rendering.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_loop.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_loop.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parse-integer</code></td>
      <td>Function</td>
      <td>Convert string query parameters (<code class="language-plaintext highlighter-rouge">"1"</code>, <code class="language-plaintext highlighter-rouge">"50"</code>) to integers, with <code class="language-plaintext highlighter-rouge">:junk-allowed t</code> for safe parsing.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">floor</code></td>
      <td>Function</td>
      <td>Round down the result of <code class="language-plaintext highlighter-rouge">offset / limit</code> to calculate the current page number.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_floorc.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_floorc.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ceiling</code></td>
      <td>Function</td>
      <td>Round up the result of <code class="language-plaintext highlighter-rouge">count / limit</code> to calculate the total number of pages.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_floorc.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_floorc.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max</code></td>
      <td>Function</td>
      <td>Ensure <code class="language-plaintext highlighter-rouge">offset</code> and <code class="language-plaintext highlighter-rouge">limit</code> never go below their minimum valid values (0 and 1), and calculate <code class="language-plaintext highlighter-rouge">range-start</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_max_m.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_max_m.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">min</code></td>
      <td>Function</td>
      <td>Ensure <code class="language-plaintext highlighter-rouge">range-end</code> doesn’t exceed <code class="language-plaintext highlighter-rouge">page-count</code> and calculate <code class="language-plaintext highlighter-rouge">end-index</code> correctly.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_max_m.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_max_m.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1+</code> / <code class="language-plaintext highlighter-rouge">1-</code></td>
      <td>Function</td>
      <td>Increment/decrement page numbers for navigation (next/previous page, page number conversions).</td>
      <td>[http://www.lispworks.com/documentation/HyperSpec/Body/f_1pl_1<em>.htm](http://www.lispworks.com/documentation/HyperSpec/Body/f_1pl_1</em>.htm)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">length</code></td>
      <td>Function</td>
      <td>Get the count of posts returned to calculate <code class="language-plaintext highlighter-rouge">end-index</code> accurately.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_length.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_length.htm</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 13: Adding Comments</title><link href="nmunro.github.io/2025/11/20/ningle-13.html" rel="alternate" type="text/html" title="Ningle Tutorial 13: Adding Comments" /><published>2025-11-20T08:00:00+00:00</published><updated>2025-11-20T08:00:00+00:00</updated><id>nmunro.github.io/2025/11/20/ningle-13</id><content type="html" xml:base="nmunro.github.io/2025/11/20/ningle-13.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li>Part 13 (Adding Comments)</li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Hello and welcome back, I hope you are well! In this tutorial we will be exploring how to work with comments, I originally didn’t think I would add too many Twitter like features, but I realised that having a self-referential model would actually be a useful lesson. In addition to demonstrating how to achieve this, we can look at how to complete a migration successfully.</p>

<p>This will involve us adjusting our models, adding a form (and respective validator), improving and expanding our controllers, adding the appropriate controller to our app and tweak our templates to accomodate the changes.</p>

<p>Note: There is also an improvement to be made in our models code, <code class="language-plaintext highlighter-rouge">mito</code> provides a convenience method to get the <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">created-at</code>, and <code class="language-plaintext highlighter-rouge">updated-at</code> slots. We will integrate it as we alter our models.</p>

<h3 id="srcmodelslisp">src/models.lisp</h3>

<p>When it comes to changes to the <code class="language-plaintext highlighter-rouge">post</code> model it is <em>very</em> important that the <code class="language-plaintext highlighter-rouge">:col-type</code> is set to <code class="language-plaintext highlighter-rouge">(or :post :null)</code> and that <code class="language-plaintext highlighter-rouge">:initform nil</code> is also set. This is because when you run the migrations, existing rows will not have data for the <code class="language-plaintext highlighter-rouge">parent</code> column and so in the process of migration we have to provide a default. It should be possible to use <code class="language-plaintext highlighter-rouge">(or :post :integer)</code> and set <code class="language-plaintext highlighter-rouge">:initform 0</code> if you so wished, but I chose to use <code class="language-plaintext highlighter-rouge">:null</code> and <code class="language-plaintext highlighter-rouge">nil</code> as my migration pattern.</p>

<p>This also ensures that new posts default to having no parent, which is the right design choice here.</p>

<h4 id="package-and-post-model">Package and Post model</h4>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defpackage</span> ningle-tutorial-project/models
  (:use :cl :mito :sxql)
  (:import-from :ningle-auth/models #:user)
  (:export #:post
           #:id
           #:content
<span class="gi">+          #:comments
</span>           #:likes
           #:user
           #:liked-post-p
<span class="gd">-          #:logged-in-posts
-          #:not-logged-in-posts
</span><span class="gi">+          #:posts
+          #:parent
</span>           #:toggle-like))

(in-package ningle-tutorial-project/models)

(deftable post ()
  ((user    :col-type ningle-auth/models:user :initarg :user    :accessor user)
<span class="gi">+  (parent  :col-type (or :post :null)        :initarg :parent  :reader parent :initform nil)
</span>   (content :col-type (:varchar 140)          :initarg :content :accessor content)))
</code></pre></div></div>

<h4 id="comments">Comments</h4>

<p>Comments are really a specialist type of post that happens to have a non-nil parent value, we will take what we previously learned from working with post objects and extend it. In reality the only real difference is <code class="language-plaintext highlighter-rouge">(sxql:where (:= parent :?))</code>, perhaps I shall see if this could support conditionals inside it, but that’s another experiment for another day.</p>

<p>I want to briefly remind you of what the <code class="language-plaintext highlighter-rouge">:?</code> does, as security is important!</p>

<p>The <code class="language-plaintext highlighter-rouge">:?</code> is a placeholder, it is a way to ensure that values are not placed in the SQL without being escaped, this prevents <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL Injection</a> attacks, the <code class="language-plaintext highlighter-rouge">retrieve-by-sql</code> takes a key argument <code class="language-plaintext highlighter-rouge">:binds</code> which takes a list of values that will be interpolated into the right parts of the SQL query with the correct quoting.</p>

<p>We used this previously, but I want to remind you to not just inject values into a SQL query without quoting them.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defmethod</span> likes ((post post))
  (mito:count-dao 'likes :post post))

+(defgeneric comments (post user)
<span class="gi">+ (:documentation "Gets the comments for a logged in user"))
+
+(defmethod comments ((post post) (user user))
+    (mito:retrieve-by-sql
+        (sxql:yield
+            (sxql:select
+                (:post.*
+                    (:as :user.username :username)
+                    (:as (:count :likes.id) :like_count)
+                    (:as (:count :user_likes.id) :liked_by_user))
+                (sxql:from :post)
+                (sxql:where (:= :parent :?))
+                (sxql:left-join :user :on (:= :post.user_id :user.id))
+                (sxql:left-join :likes :on (:= :post.id :likes.post_id))
+                (sxql:left-join (:as :likes :user_likes)
+                                :on (:and (:= :post.id :user_likes.post_id)
+                                          (:= :user_likes.user_id :?)))
+                (sxql:group-by :post.id)
+                (sxql:order-by (:desc :post.created_at))
+                (sxql:limit 50)))
+            :binds (list (mito:object-id post) (mito:object-id user))))
+
+(defmethod comments ((post post) (user null))
+    (mito:retrieve-by-sql
+       (sxql:yield
+       (sxql:select
+           (:post.*
+             (:as :user.username :username)
+             (:as (:count :likes.id) :like_count))
+           (sxql:from :post)
+           (sxql:where (:= :parent :?))
+           (sxql:left-join :user :on (:= :post.user_id :user.id))
+           (sxql:left-join :likes :on (:= :post.id :likes.post_id))
+           (sxql:group-by :post.id)
+           (sxql:order-by (:desc :post.created_at))
+           (sxql:limit 50)))
+       :binds (list (mito:object-id post))))
</span></code></pre></div></div>

<h4 id="posts-refactor">Posts refactor</h4>

<p>I had not originally planned on this, but as I was writing the comments code it became clear that I was creating lots of duplication, and maybe I still am, but I hit upon a way to simplify the model interface, at least. Ideally it makes no difference if a user is logged in or not at the point the route is hit, the api should be to give the user object (whatever that might be, because it may be nil) and let a specialised method figure out what to do there. So in addition to adding comments (which is what prompted this change) we will also slightly refactor the posts <code class="language-plaintext highlighter-rouge">logged-in-posts</code> and <code class="language-plaintext highlighter-rouge">not-logged-in-posts</code> into a single, unified <code class="language-plaintext highlighter-rouge">posts</code> method cos it’s silly of me to have split them like that.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defmethod</span> liked-post-p ((ningle-auth/models:user user) (post post))
  (mito:find-dao 'likes :user user :post post))

-(defgeneric logged-in-posts (user)
<span class="gd">-  (:documentation "Gets the posts for a logged in user"))
</span><span class="gi">+(defgeneric posts (user)
+  (:documentation "Gets the posts"))
+
</span><span class="gd">-(defmethod logged-in-posts ((user user))
-  (let ((uuid (slot-value user 'mito.dao.mixin::id)))
</span><span class="gi">+(defmethod posts ((user user))
+   (mito:retrieve-by-sql
+        (sxql:yield
+            (sxql:select
+                (:post.*
+                  (:as :user.username :username)
+                  (:as (:count :likes.id) :like_count)
+                  (:as (:count :user_likes.id) :liked_by_user))
+                (sxql:from :post)
+                (sxql:left-join :user :on (:= :post.user_id :user.id))
+                (sxql:left-join :likes :on (:= :post.id :likes.post_id))
+                (sxql:left-join (:as :likes :user_likes)
+                                :on (:and (:= :post.id :user_likes.post_id)
+                                          (:= :user_likes.user_id :?)))
+                (sxql:group-by :post.id)
+                (sxql:order-by (:desc :post.created_at))
+                (sxql:limit 50)))
+            :binds (list (mito:object-id user))))
+
</span><span class="gd">-(defun not-logged-in-posts ()
</span><span class="gi">+(defmethod posts ((user null))
+    (mito:retrieve-by-sql
+        (sxql:yield
+        (sxql:select
+            (:post.*
+              (:as :user.username :username)
+              (:as (:count :likes.id) :like_count))
+            (sxql:from :post)
+            (sxql:left-join :user :on (:= :post.user_id :user.id))
+            (sxql:left-join :likes :on (:= :post.id :likes.post_id))
+            (sxql:group-by :post.id)
+            (sxql:order-by (:desc :post.created_at))
+            (sxql:limit 50)))))
</span></code></pre></div></div>

<p>There is also another <em>small</em> fix in this code, turns out there’s a set of convenience methods that mito provides:</p>

<ul>
  <li>(mito:object-at …)</li>
  <li>(mito:created-at …)</li>
  <li>(mito:updated-at …)</li>
</ul>

<p>Previously we used <code class="language-plaintext highlighter-rouge">mito.dao.mixin::id</code> (and could have done the same for create-at, and updated-at), in combination with <code class="language-plaintext highlighter-rouge">slot-value</code>, which means <code class="language-plaintext highlighter-rouge">(slot-value user 'mito.dao.mixin::id')</code> simply becomes <code class="language-plaintext highlighter-rouge">(mito:object-id user)</code>, which is <em>much</em> nicer!</p>

<h4 id="full-listing">Full Listing</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:ningle-auth/models</span> <span class="ss">#:user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:post</span>
           <span class="ss">#:id</span>
           <span class="ss">#:content</span>
           <span class="ss">#:comments</span>
           <span class="ss">#:likes</span>
           <span class="ss">#:user</span>
           <span class="ss">#:liked-post-p</span>
           <span class="ss">#:posts</span>
           <span class="ss">#:parent</span>
           <span class="ss">#:toggle-like</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/models</span><span class="p">)</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">post</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span>    <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span>    <span class="ss">:accessor</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">parent</span>  <span class="ss">:col-type</span> <span class="p">(</span><span class="nb">or</span> <span class="ss">:post</span> <span class="ss">:null</span><span class="p">)</span>        <span class="ss">:initarg</span> <span class="ss">:parent</span>  <span class="ss">:reader</span> <span class="nv">parent</span> <span class="ss">:initform</span> <span class="no">nil</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">content</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">140</span><span class="p">)</span>          <span class="ss">:initarg</span> <span class="ss">:content</span> <span class="ss">:accessor</span> <span class="nv">content</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">likes</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span> <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span> <span class="ss">:reader</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">post</span> <span class="ss">:col-type</span> <span class="nv">post</span>                    <span class="ss">:initarg</span> <span class="ss">:post</span> <span class="ss">:reader</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">likes</span> <span class="p">(</span><span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns the number of likes a post has"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">likes</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:count-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">comments</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Gets the comments for a logged in user"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">comments</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">)</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
            <span class="p">(</span><span class="nv">sxql:select</span>
                <span class="p">(</span><span class="ss">:post.*</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:parent</span> <span class="ss">:?</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="ss">:?</span><span class="p">)))</span>
                <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
            <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">comments</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">)</span> <span class="p">(</span><span class="nv">user</span> <span class="nb">null</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
        <span class="p">(</span><span class="nv">sxql:select</span>
            <span class="p">(</span><span class="ss">:post.*</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:parent</span> <span class="ss">:?</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
        <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">toggle-like</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Toggles the like of a user to a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">toggle-like</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">liked-post</span> <span class="p">(</span><span class="nv">liked-post-p</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="nv">liked-post</span>
        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">liked-post</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">not</span> <span class="nv">liked-post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">liked-post-p</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns true if a user likes a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">liked-post-p</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'likes</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">posts</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Gets the posts"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
            <span class="p">(</span><span class="nv">sxql:select</span>
                <span class="p">(</span><span class="ss">:post.*</span>
                  <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
                  <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span>
                  <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="ss">:?</span><span class="p">)))</span>
                <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
            <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nb">null</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
        <span class="p">(</span><span class="nv">sxql:select</span>
            <span class="p">(</span><span class="ss">:post.*</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="ss">:user.username</span> <span class="ss">:username</span><span class="p">)</span>
              <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:user</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.user_id</span> <span class="ss">:user.id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="srcformslisp">src/forms.lisp</h3>

<p>All we have to do here is define our <code class="language-plaintext highlighter-rouge">form</code> and <code class="language-plaintext highlighter-rouge">validators</code> and ensure they are exported, not really a lot of work!</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defpackage</span> ningle-tutorial-project/forms
  (:use :cl :cl-forms)
  (:export #:post
           #:content
<span class="gd">-          #:submit))
</span><span class="gi">+          #:submit
+          #:comment
+          #:parent))
</span>
(in-package ningle-tutorial-project/forms)

(defparameter *post-validator* (list (clavier:not-blank)
                                     (clavier:is-a-string)
                                     (clavier:len :max 140)))

+(defparameter *post-parent-validator* (list (clavier:not-blank)
<span class="gi">+                                            (clavier:fn (lambda (x) (&gt; (parse-integer x) 0)) "Checks positive integer")))
</span>
(defform post (:id "post" :csrf-protection t :csrf-field-name "csrftoken" :action "/post")
  ((content  :string   :value "" :constraints *post-validator*)
   (submit   :submit   :label "Post")))

+(defform comment (:id "post" :csrf-protection t :csrf-field-name "csrftoken" :action "/post/comment")
<span class="gi">+  ((content  :string   :value "" :constraints *post-validator*)
+   (parent   :hidden   :value 0  :constraints *post-parent-validator*)
+   (submit   :submit   :label "Post")))
</span></code></pre></div></div>

<p>In our <code class="language-plaintext highlighter-rouge">*post-parent-validator*</code> we validate that the content of the parent field is not blank (as it is a comment and needs a reference to a parent) and we used a custom validator using <code class="language-plaintext highlighter-rouge">clavier:fn</code> and passing a lambda to verify the item is a positive integer.</p>

<p>We then create our <code class="language-plaintext highlighter-rouge">comment</code> form, which is very similar to our existing <code class="language-plaintext highlighter-rouge">post</code> form, with the difference of pointing to a different http endpoint <code class="language-plaintext highlighter-rouge">/post/comment</code> rather than just <code class="language-plaintext highlighter-rouge">/post</code>, and we have a hidden <code class="language-plaintext highlighter-rouge">parent</code> slot, which we set to <code class="language-plaintext highlighter-rouge">0</code> by default, so by default the form will be invalid, but that’s ok, because we can’t possibly know what the parent id would be until the form is rendered and we can set the parent id value at the point we render the form, so it really is nothing to worry about.</p>

<h4 id="full-listing-1">Full Listing</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/forms</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:cl-forms</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:post</span>
           <span class="ss">#:content</span>
           <span class="ss">#:submit</span>
           <span class="ss">#:comment</span>
           <span class="ss">#:parent</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/forms</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*post-validator*</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:not-blank</span><span class="p">)</span>
                                     <span class="p">(</span><span class="nv">clavier:is-a-string</span><span class="p">)</span>
                                     <span class="p">(</span><span class="nv">clavier:len</span> <span class="ss">:max</span> <span class="mi">140</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*post-parent-validator*</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:not-blank</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">clavier:fn</span> <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">x</span><span class="p">)</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="nv">x</span><span class="p">)</span> <span class="mi">0</span><span class="p">))</span> <span class="s">"Checks positive integer"</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">defform</span> <span class="nv">post</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"post"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span> <span class="ss">:action</span> <span class="s">"/post"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">content</span>  <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*post-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span>   <span class="ss">:submit</span>   <span class="ss">:label</span> <span class="s">"Post"</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">defform</span> <span class="nv">comment</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"post"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span> <span class="ss">:action</span> <span class="s">"/post/comment"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">content</span>  <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*post-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">parent</span>   <span class="ss">:hidden</span>   <span class="ss">:value</span> <span class="mi">0</span>  <span class="ss">:constraints</span> <span class="vg">*post-parent-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span>   <span class="ss">:submit</span>   <span class="ss">:label</span> <span class="s">"Post"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="srccontrollerslisp">src/controllers.lisp</h3>

<p>Having simplified the models, we can also simplify the controllers!</p>

<p>Let’s start by setting up our package information:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defpackage</span> ningle-tutorial-project/controllers
<span class="gd">- (:use :cl :sxql :ningle-tutorial-project/forms)
</span><span class="gi">+ (:use :cl :sxql)
+ (:import-from :ningle-tutorial-project/forms
+               #:post
+               #:content
+               #:parent
+               #:comment)
</span><span class="gd">- (:export #:logged-in-index
-          #:index
</span><span class="gi">+ (:export #:index
</span>           #:post-likes
           #:single-post
           #:post-content
<span class="gi">+          #:post-comment
</span>           #:logged-in-profile
           #:unauthorized-profile
           #:people
           #:person))

(in-package ningle-tutorial-project/controllers)
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">index</code> and <code class="language-plaintext highlighter-rouge">logged-in-index</code> can now be consolidated:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-(defun logged-in-index (params)
</span><span class="gi">+(defun index (params)
</span><span class="err">(let*</span> ((user (gethash :user ningle:*session*))
<span class="gd">-     (form (cl-forms:find-form 'post))
-     (posts (ningle-tutorial-project/models:logged-in-posts user)))
-  (djula:render-template* "main/index.html" nil :title "Home" :user user :posts posts :form form)))
-
-
-(defun index (params))
-(let ((posts (ningle-tutorial-project/models:not-logged-in-posts)))
-  (djula:render-template* "main/index.html" nil :title "Home" :user (gethash :user ningle:*session*) :posts posts)))
</span><span class="gi">+      (posts (ningle-tutorial-project/models:posts user))
+  (djula:render-template* "main/index.html" nil :title "Home" :user user :posts posts :form (if user (cl-forms:find-form 'post) nil))))
</span></code></pre></div></div>

<p>Our <code class="language-plaintext highlighter-rouge">post-likes</code> controller comes next:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defun</span> post-likes (params)
  (let* ((user (gethash :user ningle:*session*))
         (post (mito:find-dao 'ningle-tutorial-project/models:post :id (parse-integer (ingle:get-param :id params))))
         (res (make-hash-table :test 'equal)))
<span class="gd">-    (setf (gethash :post res) (parse-integer (ingle:get-param :id params)) )
-    (setf (gethash :likes res) (ningle-tutorial-project/models:likes post))
-    (setf (gethash :liked res) (ningle-tutorial-project/models:toggle-like user post))
</span><span class="gi">+   ;; Bail out if post does not exist
+   (unless post
+     (setf (gethash "error" res) "post not found")
+     (setf (getf (lack.response:response-headers ningle:*response*) :content-type) "application/json")
+     (setf (lack.response:response-status ningle:*response*) 404)
+     (return-from post-likes (com.inuoe.jzon.stringify res)))
+
+   (setf (gethash "post" res) (mito:object-id post))
+   (setf (gethash "liked" res) (ningle-tutorial-project/models:toggle-like user post))
+   (setf (gethash "likes" res) (ningle-tutorial-project/models:likes post))
+   (setf (getf (lack.response:response-headers ningle:*response*) :content-type) "application/json")
+   (setf (lack.response:response-status ningle:*response*) 201)
+   (com.inuoe.jzon:stringify res)))
</span></code></pre></div></div>

<p>Here we begin by first checking that the post exists, if for some reason someone sent a request to our server without a valid post an error might be thrown and no response would be sent at all, which is not good, so we use <code class="language-plaintext highlighter-rouge">unless</code> as our “if not” check to return the standard http code for not found, the good old 404!</p>

<p>If however there is no error (a post matching the id exists) we can continue, we build up the hash-table, including the “post”, “liked”, and “likes” properties of a post. Remember these are not direct properties of a post model, but calculated based on information in other tables, especially the <code class="language-plaintext highlighter-rouge">toggle-like</code> (actually it’s very important to ensure you call <code class="language-plaintext highlighter-rouge">toggle-like</code> first, as it changes the db state that calling <code class="language-plaintext highlighter-rouge">likes</code> will depend on), as it returns the toggled status, that is, if a user clicks it once it will like the post, but if they click it again it will “unlike” the post.</p>

<p>Now, with our single post, we have implemented a lot more information, comments, likes, our new comment form, etc so we have to really build up a more comprehensive <code class="language-plaintext highlighter-rouge">single-post</code> controller.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defun</span> single-post (params)
    (handler-case
<span class="gd">-       (let ((post (mito:find-dao 'ningle-tutorial-project/models:post :id (parse-integer (ingle:get-param :id params)))))
-           (djula:render-template* "main/post.html" nil :title "Post" :post post))
</span><span class="gi">+
+       (let* ((post-id (parse-integer (ingle:get-param :id params)))
+              (post (mito:find-dao 'ningle-tutorial-project/models:post :id post-id))
+              (comments (ningle-tutorial-project/models:comments post (gethash :user ningle:*session*)))
+              (likes (ningle-tutorial-project/models:likes post))
+              (form (cl-forms:find-form 'comment))
+              (user (gethash :user ningle:*session*)))
+         (cl-forms:set-field-value form 'ningle-tutorial-project/forms:parent post-id)
+         (djula:render-template* "main/post.html" nil
+                                 :title "Post"
+                                 :post post
+                                 :comments comments
+                                 :likes likes
+                                 :form form
+                                 :user user))
</span>
        (parse-error (err)
            (setf (lack.response:response-status ningle:*response*) 404)
            (djula:render-template* "error.html" nil :title "Error" :error err))))
</code></pre></div></div>

<p>Where previously we just rendered the template, we now do a lot more! We can get the likes, comments etc which is a massive step up in functionality.</p>

<p>The next function to look at is <code class="language-plaintext highlighter-rouge">post-content</code>, thankfully there isn’t too much to change here, all we need to do is ensure we pass through the parent (which will be nil).</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(when</span> valid
    (cl-forms:with-form-field-values (content) form
<span class="gd">-       (mito:create-dao 'ningle-tutorial-project/models:post :content content :user user)
</span><span class="gi">+       (mito:create-dao 'ningle-tutorial-project/models:post :content content :user user :parent nil)
</span>        (ingle:redirect "/")))))
</code></pre></div></div>

<p>Now, finally in our controllers we add the <code class="language-plaintext highlighter-rouge">post-comment</code> controller.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gi">+(defun post-comment (params)
+   (let ((user (gethash :user ningle:*session*))
+         (form (cl-forms:find-form 'comment)))
+       (handler-case
+           (progn
+               (cl-forms:handle-request form) ; Can throw an error if CSRF fails
+
+               (multiple-value-bind (valid errors)
+                   (cl-forms:validate-form form)
+
+                   (when errors
+                       (format t "Errors: ~A~%" errors))
+
+                   (when valid
+                       (cl-forms:with-form-field-values (content parent) form
+                           (mito:create-dao 'ningle-tutorial-project/models:post :content content :user user :parent (parse-integer parent))
+                           (ingle:redirect "/")))))
+
+           (simple-error (err)
+               (setf (lack.response:response-status ningle:*response*) 403)
+               (djula:render-template* "error.html" nil :title "Error" :error err)))))
</span></code></pre></div></div>

<p>We have seen this pattern before, but with some minor differences in which form to load (<code class="language-plaintext highlighter-rouge">comment</code> instead of <code class="language-plaintext highlighter-rouge">post</code>), and setting the parent from the value injected into the form at the point the form is rendered.</p>

<h4 id="full-listing-2">Full Listing</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/controllers</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:ningle-tutorial-project/forms</span>
                <span class="ss">#:post</span>
                <span class="ss">#:content</span>
                <span class="ss">#:parent</span>
                <span class="ss">#:comment</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:index</span>
           <span class="ss">#:post-likes</span>
           <span class="ss">#:single-post</span>
           <span class="ss">#:post-content</span>
           <span class="ss">#:post-comment</span>
           <span class="ss">#:logged-in-profile</span>
           <span class="ss">#:unauthorized-profile</span>
           <span class="ss">#:people</span>
           <span class="ss">#:person</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/controllers</span><span class="p">)</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">index</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:posts</span> <span class="nv">user</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span> <span class="ss">:form</span> <span class="p">(</span><span class="k">if</span> <span class="nv">user</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)</span> <span class="no">nil</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-likes</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
         <span class="p">(</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
         <span class="p">(</span><span class="nv">res</span> <span class="p">(</span><span class="nb">make-hash-table</span> <span class="ss">:test</span> <span class="ss">'equal</span><span class="p">)))</span>
    <span class="c1">;; Bail out if post does not exist</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">post</span>
      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">getf</span> <span class="p">(</span><span class="nv">lack.response:response-headers</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="ss">:content-type</span><span class="p">)</span> <span class="s">"application/json"</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"error"</span> <span class="nv">res</span><span class="p">)</span> <span class="s">"post not found"</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
      <span class="p">(</span><span class="k">return-from</span> <span class="nv">post-likes</span> <span class="p">(</span><span class="nv">com.inuoe.jzon.stringify</span> <span class="nv">res</span><span class="p">)))</span>

    <span class="c1">;; success, continue</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"post"</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"liked"</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:toggle-like</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="s">"likes"</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">getf</span> <span class="p">(</span><span class="nv">lack.response:response-headers</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="ss">:content-type</span><span class="p">)</span> <span class="s">"application/json"</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">201</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">com.inuoe.jzon:stringify</span> <span class="nv">res</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">single-post</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">handler-case</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
              <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'comment</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">cl-forms:set-field-value</span> <span class="nv">form</span> <span class="ss">'ningle-tutorial-project/forms:parent</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">post</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/post.html"</span> <span class="no">nil</span>
                                  <span class="ss">:title</span> <span class="s">"Post"</span>
                                  <span class="ss">:post</span> <span class="nv">post</span>
                                  <span class="ss">:comments</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:comments</span> <span class="nv">post</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
                                  <span class="ss">:likes</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">)</span>
                                  <span class="ss">:form</span> <span class="nv">form</span>
                                  <span class="ss">:user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>

        <span class="p">(</span><span class="kt">parse-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-content</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
                <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span><span class="p">)</span> <span class="nv">form</span>
                            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:parent</span> <span class="no">nil</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-comment</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'comment</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
                <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span> <span class="nv">parent</span><span class="p">)</span> <span class="nv">form</span>
                            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:parent</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="nv">parent</span><span class="p">))</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">logged-in-profile</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">unauthorized-profile</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">people</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">person</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">username-or-email</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">person</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                            <span class="ss">'ningle-auth/models:user</span>
                            <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username-or-email</span><span class="p">)</span>
                                        <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">username-or-email</span><span class="p">)))))))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:person</span> <span class="nv">person</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="srcmainlisp">src/main.lisp</h3>

<p>The change to our <code class="language-plaintext highlighter-rouge">main.lisp</code> file is a single line that connects our controller to the urls we have declared we are using.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(setf</span> (ningle:route *app* "/post" :method :POST :logged-in-p t) #'post-content)
<span class="gi">+(setf (ningle:route *app* "/post/comment" :method :POST :logged-in-p t) #'post-comment)
</span><span class="err">(setf</span> (ningle:route *app* "/profile" :logged-in-p t) #'logged-in-profile)
</code></pre></div></div>

<h4 id="full-listing-3">Full Listing</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:ningle-tutorial-project/controllers</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="c1">;; requirements</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:requirement</span> <span class="vg">*app*</span> <span class="ss">:logged-in-p</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">value</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span> <span class="nv">value</span><span class="p">)))</span>

<span class="c1">;; routes</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">index</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id/likes"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">post-likes</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">single-post</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">post-content</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/comment"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">post-comment</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">logged-in-profile</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">unauthorized-profile</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">people</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">person</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="srctemplatesmainindexhtml">src/templates/main/index.html</h3>

<p>There are some small changes needed in the index.html file, they’re largely just optimisations. The first is changing a boolean around likes to integer, this gets into the weeds of JavaScript types, and ensuring things were of the <code class="language-plaintext highlighter-rouge">Number</code> type in JS just made things easier. Some of the previous code even treated booleans as strings, which was pretty bad, I don’t write JS in any real capacity, so I often make mistakes with it, because it so very often <em>appears</em> to work instead of just throwing an error.</p>

<p>~ Lines 28 - 30</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    data-logged-in="true"
<span class="gd">-   data-liked="false"
</span><span class="gi">+   data-liked="0"
</span>    aria-label="Like post "&gt;
</code></pre></div></div>

<p>~ Lines 68 - 70</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    const icon = btn.querySelector("i");
<span class="gd">-   const liked = btn.dataset.liked === "true";
</span><span class="gi">+   const liked = Number(btn.dataset.liked) === 1;
</span>    const previous = parseInt(countSpan.textContent, 10) || 0;
</code></pre></div></div>

<p>~ Lines 96 - 100</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    if (!resp.ok) {
        // Revert optimistic changes on error
        countSpan.textContent = previous;
        countSpan.textContent = previous;
<span class="gd">-       btn.dataset.liked = liked ? "true" : "false";
</span><span class="gi">+       btn.dataset.liked = liked ? 1 : 0;
</span>        if (liked) {
</code></pre></div></div>

<p>~ Lines 123 - 129</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      console.error("Like failed:", err);
      // Revert optimistic changes on error
      countSpan.textContent = previous;
<span class="gd">-     btn.dataset.liked = liked ? "true" : "false";
</span><span class="gi">+     btn.dataset.liked = liked ? 1 : 0;
</span>      if (liked) {
        icon.className = "bi bi-hand-thumbs-up-fill text-primary";
      } else {
</code></pre></div></div>

<h3 id="srctemplatesmainposthtml">src/templates/main/post.html</h3>

<p>The changes to this file as so substantial that the file might as well be brand new, so in the interests of clarity, I will simply show the file in full.</p>

<h4 id="full-listing-4">Full Listing</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card post mb-3"</span> <span class="na">data-href=</span><span class="s">"/post/{{ post.id }}"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title mb-2"</span><span class="nt">&gt;</span>{{ post.content }}<span class="nt">&lt;/h5&gt;</span>
                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-subtitle text-muted mb-0"</span><span class="nt">&gt;</span>@{{ post.user.username }}<span class="nt">&lt;/p&gt;</span>
                <span class="nt">&lt;/div&gt;</span>

                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-footer d-flex justify-content-between align-items-center"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"button"</span>
                        <span class="na">class=</span><span class="s">"btn btn-sm btn-outline-primary like-button"</span>
                        <span class="na">data-post-id=</span><span class="s">"{{ post.id }}"</span>
                        <span class="na">data-logged-in=</span><span class="s">"{% if user.username != "</span><span class="err">"</span> <span class="err">%}</span><span class="na">true</span><span class="err">{%</span> <span class="na">else</span> <span class="err">%}</span><span class="na">false</span><span class="err">{%</span> <span class="na">endif</span> <span class="err">%}"</span>
                        <span class="na">data-liked=</span><span class="s">"{% if post.liked-by-user == 1 %}1{% else %}0{% endif %}"</span>
                        <span class="na">aria-label=</span><span class="s">"Like post {{ post.id }}"</span><span class="nt">&gt;</span>
                    {% if post.liked-by-user == 1 %}
                      <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up-fill text-primary"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                    {% else %}
                      <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up text-muted"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                    {% endif %}
                    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"ms-1 like-count"</span><span class="nt">&gt;</span>{{ likes }}<span class="nt">&lt;/span&gt;</span>
                <span class="nt">&lt;/button&gt;</span>

                <span class="nt">&lt;small</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>Posted on: {{ post.created-at }}<span class="nt">&lt;/small&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="c">&lt;!-- Post form --&gt;</span>
    {% if user %}
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
                {% if form %}
                    {% form form %}
                {% endif %}
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    {% endif %}

    {% if comments %}
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h2&gt;</span>Comments<span class="nt">&lt;/h2&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    {% endif %}

    {% for comment in comments %}
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card post mb-3"</span> <span class="na">data-href=</span><span class="s">"/post/{{ comment.id }}"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title mb-2"</span><span class="nt">&gt;</span>{{ comment.content }}<span class="nt">&lt;/h5&gt;</span>
                        <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-subtitle text-muted mb-0"</span><span class="nt">&gt;</span>@{{ comment.username }}<span class="nt">&lt;/p&gt;</span>
                    <span class="nt">&lt;/div&gt;</span>

                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-footer d-flex justify-content-between align-items-center"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"button"</span>
                                <span class="na">class=</span><span class="s">"btn btn-sm btn-outline-primary like-button"</span>
                                <span class="na">data-post-id=</span><span class="s">"{{ comment.id }}"</span>
                                <span class="na">data-logged-in=</span><span class="s">"{% if user.username != "</span><span class="err">"</span> <span class="err">%}</span><span class="na">true</span><span class="err">{%</span> <span class="na">else</span> <span class="err">%}</span><span class="na">false</span><span class="err">{%</span> <span class="na">endif</span> <span class="err">%}"</span>
                                <span class="na">data-liked=</span><span class="s">"{% if comment.liked-by-user == 1 %}1{% else %}0{% endif %}"</span>
                                <span class="na">aria-label=</span><span class="s">"Like post {{ comment.id }}"</span><span class="nt">&gt;</span>
                            {% if comment.liked-by-user == 1 %}
                                <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up-fill text-primary"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                            {% else %}
                                <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up text-muted"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                            {% endif %}
                            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"ms-1 like-count"</span><span class="nt">&gt;</span>{{ comment.like-count }}<span class="nt">&lt;/span&gt;</span>
                        <span class="nt">&lt;/button&gt;</span>
                        <span class="nt">&lt;small</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>Posted on: {{ comment.created-at }}<span class="nt">&lt;/small&gt;</span>
                    <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    {% endfor %}
<span class="nt">&lt;/div&gt;</span>
{% endblock %}

{% block js %}
document.querySelectorAll(".like-button").forEach(btn =&gt; {
  btn.addEventListener("click", function (e) {
    e.stopPropagation();
    e.preventDefault();

    // Check login
    if (btn.dataset.loggedIn !== "true") {
      alert("You must be logged in to like posts.");
      return;
    }

    const postId = btn.dataset.postId;
    const countSpan = btn.querySelector(".like-count");
    const icon = btn.querySelector("i");
    const liked = Number(btn.dataset.liked) === 1;
    const previous = parseInt(countSpan.textContent, 10) || 0;
    const url = `/post/${postId}/likes`;

    // Optimistic UI toggle
    countSpan.textContent = liked ? previous - 1 : previous + 1;
    btn.dataset.liked = liked ? 0 : 1;

    // Toggle icon classes optimistically
    if (liked) {
      // Currently liked, so unlike it
      icon.className = "bi bi-hand-thumbs-up text-muted";
    } else {
      // Currently not liked, so like it
      icon.className = "bi bi-hand-thumbs-up-fill text-primary";
    }

    const csrfTokenMeta = document.querySelector('meta[name="csrf-token"]');
    const headers = { "Content-Type": "application/json" };
    if (csrfTokenMeta) headers["X-CSRF-Token"] = csrfTokenMeta.getAttribute("content");

    fetch(url, {
      method: "POST",
      headers: headers,
      body: JSON.stringify({ toggle: true })
    })
    .then(resp =&gt; {
      if (!resp.ok) {
        // Revert optimistic changes on error
        countSpan.textContent = previous;
        btn.dataset.liked = liked ? 1 : 0;
        icon.className = liked ? "bi bi-hand-thumbs-up-fill text-primary" : "bi bi-hand-thumbs-up text-muted";
        throw new Error("Network response was not ok");
      }
      return resp.json();
    })
    .then(data =&gt; {
      if (data <span class="err">&amp;&amp;</span> typeof data.likes !== "undefined") {
        countSpan.textContent = data.likes;
        btn.dataset.liked = data.liked ? 1 : 0;
        icon.className = data.liked ? "bi bi-hand-thumbs-up-fill text-primary" : "bi bi-hand-thumbs-up text-muted";
      }
    })
    .catch(err =&gt; {
      console.error("Like failed:", err);
      // Revert optimistic changes on error
      countSpan.textContent = previous;
      btn.dataset.liked = liked ? 1 : 0;
      icon.className = liked ? "bi bi-hand-thumbs-up-fill text-primary" : "bi bi-hand-thumbs-up text-muted";
    });
  });
});

document.querySelectorAll(".card.post").forEach(card =&gt; {
  card.addEventListener("click", function () {
    const href = card.dataset.href;
    if (href) {
      window.location.href = href;
    }
  });
});
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h2 id="conclusion">Conclusion</h2>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Learning Outcome</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Understand</strong></td>
      <td>Understand how to model a self-referential <code class="language-plaintext highlighter-rouge">post</code> table in Mito (using a nullable <code class="language-plaintext highlighter-rouge">parent</code> column) and why <code class="language-plaintext highlighter-rouge">(or :post :null)</code>/<code class="language-plaintext highlighter-rouge">:initform nil</code> are important for safe migrations and representing “top-level” posts versus comments.</td>
    </tr>
    <tr>
      <td><strong>Apply</strong></td>
      <td>Apply Mito, SXQL, and cl-forms to implement a comment system end-to-end: defining <code class="language-plaintext highlighter-rouge">comments</code>/<code class="language-plaintext highlighter-rouge">posts</code> generics, adding validators (including a custom <code class="language-plaintext highlighter-rouge">clavier:fn</code>), wiring controllers and routes, and rendering comments and like-buttons in templates.</td>
    </tr>
    <tr>
      <td><strong>Analyse</strong></td>
      <td>Analyse and reduce duplication in the models/controllers layer by consolidating separate code paths (logged-in vs anonymous) into generic functions specialised on <code class="language-plaintext highlighter-rouge">user</code>/<code class="language-plaintext highlighter-rouge">null</code>, and by examining how SQL joins and binds shape the returned data.</td>
    </tr>
    <tr>
      <td><strong>Evaluate</strong></td>
      <td>Evaluate different design and safety choices in the implementation (nullable vs sentinel parents, optimistic UI vs server truth, HTTP status codes, SQL placeholders, CSRF and login checks) and judge which approaches are more robust and maintainable.</td>
    </tr>
  </tbody>
</table>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-13">here</a>.</li>
</ul>

<h3 id="common-lisp-hyperspec">Common Lisp HyperSpec</h3>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Type</th>
      <th>Why it appears in this lesson</th>
      <th>CLHS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defpackage</code></td>
      <td>Macro</td>
      <td>Define project packages like <code class="language-plaintext highlighter-rouge">ningle-tutorial-project/models</code>, <code class="language-plaintext highlighter-rouge">/forms</code>, <code class="language-plaintext highlighter-rouge">/controllers</code>, and the main system package.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">in-package</code></td>
      <td>Macro</td>
      <td>Enter each package before defining tables, forms, controllers, and the main app functions.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defvar</code></td>
      <td>Special Operator</td>
      <td>Define <code class="language-plaintext highlighter-rouge">*app*</code> as a global Ningle application object.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_defvar.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_defvar.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defparameter</code></td>
      <td>Special Operator</td>
      <td>Define validator configuration variables like <code class="language-plaintext highlighter-rouge">*post-validator*</code> and <code class="language-plaintext highlighter-rouge">*post-parent-validator*</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_defpar.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_defpar.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defgeneric</code></td>
      <td>Macro</td>
      <td>Declare generic functions such as <code class="language-plaintext highlighter-rouge">likes</code>, <code class="language-plaintext highlighter-rouge">comments</code>, <code class="language-plaintext highlighter-rouge">toggle-like</code>, <code class="language-plaintext highlighter-rouge">liked-post-p</code>, and <code class="language-plaintext highlighter-rouge">posts</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defmethod</code></td>
      <td>Macro</td>
      <td>Specialise behaviour for <code class="language-plaintext highlighter-rouge">likes</code>, <code class="language-plaintext highlighter-rouge">comments</code>, <code class="language-plaintext highlighter-rouge">toggle-like</code>, <code class="language-plaintext highlighter-rouge">liked-post-p</code>, <code class="language-plaintext highlighter-rouge">posts</code>, and <code class="language-plaintext highlighter-rouge">ningle:not-found</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defun</code></td>
      <td>Macro</td>
      <td>Define controller functions like <code class="language-plaintext highlighter-rouge">index</code>, <code class="language-plaintext highlighter-rouge">post-likes</code>, <code class="language-plaintext highlighter-rouge">single-post</code>, <code class="language-plaintext highlighter-rouge">post-content</code>, <code class="language-plaintext highlighter-rouge">post-comment</code>, <code class="language-plaintext highlighter-rouge">people</code>, <code class="language-plaintext highlighter-rouge">person</code>, <code class="language-plaintext highlighter-rouge">start</code>, etc.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defun.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defun.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">make-instance</code></td>
      <td>Generic Function</td>
      <td>Create the Ningle app object: <code class="language-plaintext highlighter-rouge">(make-instance 'ningle:app)</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_ins.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_ins.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let</code> / <code class="language-plaintext highlighter-rouge">let*</code></td>
      <td>Special Operator</td>
      <td>Introduce local bindings like <code class="language-plaintext highlighter-rouge">user</code>, <code class="language-plaintext highlighter-rouge">posts</code>, <code class="language-plaintext highlighter-rouge">post</code>, <code class="language-plaintext highlighter-rouge">comments</code>, <code class="language-plaintext highlighter-rouge">likes</code>, <code class="language-plaintext highlighter-rouge">form</code>, and <code class="language-plaintext highlighter-rouge">res</code> in controllers.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lambda</code></td>
      <td>Special Operator</td>
      <td>Used for the <code class="language-plaintext highlighter-rouge">:logged-in-p</code> requirement: <code class="language-plaintext highlighter-rouge">(lambda (value) (and (cu-sith:logged-in-p) value))</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_fn_lam.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_fn_lam.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setf</code></td>
      <td>Macro</td>
      <td>Set routes, response headers/status codes, and update hash-table entries in the JSON response.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_setf.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_setf.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gethash</code></td>
      <td>Function</td>
      <td>Access session values (e.g. the <code class="language-plaintext highlighter-rouge">:user</code> from <code class="language-plaintext highlighter-rouge">ningle:*session*</code>) and JSON keys in result hash-tables.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_gethas.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_gethas.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">make-hash-table</code></td>
      <td>Function</td>
      <td>Build the hash-table used as the JSON response body in <code class="language-plaintext highlighter-rouge">post-likes</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_has.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_has.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">equal</code></td>
      <td>Function</td>
      <td>Used as the <code class="language-plaintext highlighter-rouge">:test</code> function for the JSON response hash-table.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_equal.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_equal.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">list</code></td>
      <td>Function</td>
      <td>Build the <code class="language-plaintext highlighter-rouge">:binds</code> list for <code class="language-plaintext highlighter-rouge">mito:retrieve-by-sql</code> and other list values.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_list.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_list.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">first</code></td>
      <td>Accessor</td>
      <td>Take the first result from <code class="language-plaintext highlighter-rouge">mito:select-dao</code> in the <code class="language-plaintext highlighter-rouge">person</code> controller.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_firstc.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_firstc.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">slot-value</code></td>
      <td>Function</td>
      <td>Discussed when explaining the old pattern <code class="language-plaintext highlighter-rouge">(slot-value user '…:id)</code> that was replaced by <code class="language-plaintext highlighter-rouge">mito:object-id</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_slot__.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_slot__.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parse-integer</code></td>
      <td>Function</td>
      <td>Convert route params and hidden form <code class="language-plaintext highlighter-rouge">parent</code> values into integers (<code class="language-plaintext highlighter-rouge">post-id</code>, <code class="language-plaintext highlighter-rouge">parent</code>, etc.).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">format</code></td>
      <td>Function</td>
      <td>Print validation error information in the controllers (<code class="language-plaintext highlighter-rouge">(format t "Errors: ~A~%" errors)</code>).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_format.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_format.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">handler-case</code></td>
      <td>Macro</td>
      <td>Handle <code class="language-plaintext highlighter-rouge">parse-error</code> for invalid ids and <code class="language-plaintext highlighter-rouge">simple-error</code> for CSRF failures, mapping them to 404 / 403 responses.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_hand_1.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_hand_1.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parse-error</code></td>
      <td>Condition Type</td>
      <td>Signalled when parsing fails (e.g. malformed <code class="language-plaintext highlighter-rouge">:id</code> route parameters), caught in <code class="language-plaintext highlighter-rouge">single-post</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/e_parse_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/e_parse_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">simple-error</code></td>
      <td>Condition Type</td>
      <td>Used to represent CSRF and similar failures caught in <code class="language-plaintext highlighter-rouge">post-content</code> and <code class="language-plaintext highlighter-rouge">post-comment</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/e_smp_er.htm">http://www.lispworks.com/documentation/HyperSpec/Body/e_smp_er.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">multiple-value-bind</code></td>
      <td>Macro</td>
      <td>Bind the <code class="language-plaintext highlighter-rouge">(valid errors)</code> results from <code class="language-plaintext highlighter-rouge">cl-forms:validate-form</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_mpv_bn.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_mpv_bn.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">progn</code></td>
      <td>Special Operator</td>
      <td>Group side-effecting calls (handle request, validate, then create/redirect) under a single handler in <code class="language-plaintext highlighter-rouge">handler-case</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_progn.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_progn.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">when</code></td>
      <td>Macro</td>
      <td>Conditionally log validation errors and perform DAO creation only when the form is valid.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">unless</code></td>
      <td>Macro</td>
      <td>Early-exit error path in <code class="language-plaintext highlighter-rouge">post-likes</code> when the post cannot be found (<code class="language-plaintext highlighter-rouge">(unless post … (return-from …))</code>).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">return-from</code></td>
      <td>Special Operator</td>
      <td>Non-locally return from <code class="language-plaintext highlighter-rouge">post-likes</code> after sending a 404 JSON response.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_ret_fr.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_ret_fr.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">declare</code></td>
      <td>Special Operator</td>
      <td>Used with <code class="language-plaintext highlighter-rouge">(declare (ignore app))</code> in the <code class="language-plaintext highlighter-rouge">ningle:not-found</code> method to silence unused-argument warnings.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_declar.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_declar.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">and</code> / <code class="language-plaintext highlighter-rouge">or</code></td>
      <td>Macro</td>
      <td>Logical composition in the login requirement and in the <code class="language-plaintext highlighter-rouge">where</code> clause for username/email matching.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/a_and.htm">http://www.lispworks.com/documentation/HyperSpec/Body/a_and.htm</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 12: Clean Up &amp;amp; Bug Fix</title><link href="nmunro.github.io/2025/10/29/ningle-12.html" rel="alternate" type="text/html" title="Ningle Tutorial 12: Clean Up &amp;amp; Bug Fix" /><published>2025-10-29T09:00:00+00:00</published><updated>2025-10-29T09:00:00+00:00</updated><id>nmunro.github.io/2025/10/29/ningle-12</id><content type="html" xml:base="nmunro.github.io/2025/10/29/ningle-12.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li>Part 12 (Clean Up &amp; Bug Fix)</li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Hello, and welcome back! We have done some pretty hefy work lately, so as we are drawing towards the end of the year we will be taking it a <em>bit</em> easier, we will be looking, at better organising and structuring our project. There is also a small bug we shall fix, which is in fact where we will start!</p>

<h3 id="fixing-a-bug">Fixing a bug</h3>

<p>An oversight on my part last month was that a change stopped the username from appearing on posts. The solution is quite simple, little more than another join on our query.</p>

<p>In our <code class="language-plaintext highlighter-rouge">logged-in-posts</code> and <code class="language-plaintext highlighter-rouge">not-logged-in-posts</code> controllers, we need to make a small change, they’re basically the same two line change in both.</p>

<p>I will be testing out the ability to simulate the output of git diff here, so if you have feedback on this change, let me know!</p>

<h4 id="logged-in-posts">logged-in-posts</h4>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defmethod</span> logged-in-posts ((user user))
  (let ((uid (slot-value user 'mito.dao.mixin::id)))
    (mito:retrieve-by-sql
        (sxql:yield
            (sxql:select
                (:post.*
<span class="gi">+                 (:as :user.username :username)                        ;; Add this line
</span>                  (:as (:count :likes.id) :like_count)
                  (:as (:count :user_likes.id) :liked_by_user))
                (sxql:from :post)
<span class="gi">+               (sxql:left-join :user :on (:= :post.user_id :user.id))  ;; Add this line
</span>                (sxql:left-join :likes :on (:= :post.id :likes.post_id))
                (sxql:left-join (:as :likes :user_likes)
                                :on (:and (:= :post.id :user_likes.post_id)
                                          (:= :user_likes.user_id :?)))
                (sxql:group-by :post.id)
                (sxql:order-by (:desc :post.created_at))
                (sxql:limit 50)))
            :binds (list uid))))
</code></pre></div></div>

<h4 id="not-logged-in-posts">not-logged-in-posts</h4>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(defun</span> not-logged-in-posts ()
    (mito:retrieve-by-sql
        (sxql:yield
        (sxql:select
            (:post.*
<span class="gi">+             (:as :user.username :username)                        ;; Add this line
</span>              (:as (:count :likes.id) :like_count))
            (sxql:from :post)
<span class="gi">+           (sxql:left-join :user :on (:= :post.user_id :user.id))  ;; Add this line
</span>            (sxql:left-join :likes :on (:= :post.id :likes.post_id))
            (sxql:group-by :post.id)
            (sxql:order-by (:desc :post.created_at))
            (sxql:limit 50)))))
</code></pre></div></div>

<p>This should now allow the usernames to come through. The reason for this is that although the “user” column would come back, it only contains a number, since it is a foreign key, so to get the rest of the actual information we must perform an sql <code class="language-plaintext highlighter-rouge">join</code>, so we can “join” information from different tables together.</p>

<p>As a result of this change though, we do need to change two template.</p>

<h4 id="srctemplatesmainindexhtml">src/templates/main/index.html</h4>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- &lt;p class="card-subtitle text-muted mb-0"&gt;@{{ post.user.username }}&lt;/p&gt;
</span><span class="gi">+ &lt;p class="card-subtitle text-muted mb-0"&gt;@{{ post.username }}&lt;/p&gt;
</span></code></pre></div></div>

<h4 id="srctemplatesmainposthtml">src/templates/main/post.html</h4>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- &lt;h2&gt;{{ post.user.username }}
</span><span class="gi">+ &lt;h2&gt;{{ post.username }}
</span></code></pre></div></div>

<p>That should be everything we need, so onto cleaning up our project!</p>

<h3 id="cleaning-up-project">Cleaning up project</h3>

<p>The clean up process is rather simple, but I find it helps. Our <code class="language-plaintext highlighter-rouge">main.lisp</code> file has gotten quite large and busy and it contains conceptually two things, our routing, and our controllers and while it’s certainly possible to have both in the same file, it can perhaps make the routing difficult to see, so we will be creating a new <code class="language-plaintext highlighter-rouge">controllers.lisp</code> file and putting our functions in there, and simply attaching the function name to the route.</p>

<h4 id="srccontrollerslisp">src/controllers.lisp</h4>

<p>We will be taking each of the functions from our <code class="language-plaintext highlighter-rouge">main.lisp</code> and declaring them as real functions here, of course remembering to export them from this package so that they can be accessed externally.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/controllers</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span> <span class="ss">:ningle-tutorial-project/forms</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:logged-in-index</span>
           <span class="ss">#:index</span>
           <span class="ss">#:post-likes</span>
           <span class="ss">#:single-post</span>
           <span class="ss">#:post-content</span>
           <span class="ss">#:logged-in-profile</span>
           <span class="ss">#:unauthorized-profile</span>
           <span class="ss">#:people</span>
           <span class="ss">#:person</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/controllers</span><span class="p">)</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">logged-in-index</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:logged-in-posts</span> <span class="nv">user</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">index</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:not-logged-in-posts</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-likes</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
           <span class="p">(</span><span class="nv">res</span> <span class="p">(</span><span class="nb">make-hash-table</span> <span class="ss">:test</span> <span class="ss">'equal</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:post</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))</span>
        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:likes</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">))</span>
        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:liked</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:toggle-like</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">com.inuoe.jzon:stringify</span> <span class="nv">res</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">single-post</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">handler-case</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">)))))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/post.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Post"</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

        <span class="p">(</span><span class="kt">parse-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">post-content</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
                <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span><span class="p">)</span> <span class="nv">form</span>
                            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">logged-in-profile</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">unauthorized-profile</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">people</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">))))</span>


<span class="p">(</span><span class="nb">defun</span> <span class="nv">person</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">username-or-email</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">person</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                            <span class="ss">'ningle-auth/models:user</span>
                            <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username-or-email</span><span class="p">)</span>
                                        <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">username-or-email</span><span class="p">)))))))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:person</span> <span class="nv">person</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>With the exception of the <code class="language-plaintext highlighter-rouge">defpackage</code> and <code class="language-plaintext highlighter-rouge">in-package</code>, the only thing that changes here is that we are giving these functions a name, the params is unchanged from when there were in <code class="language-plaintext highlighter-rouge">main.lisp</code>.</p>

<h4 id="srcmainlisp">src/main.lisp</h4>

<p>This allows <code class="language-plaintext highlighter-rouge">main.lisp</code> to be flattened down.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:ningle-tutorial-project/controllers</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="c1">;; requirements</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:requirement</span> <span class="vg">*app*</span> <span class="ss">:logged-in-p</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">value</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span> <span class="nv">value</span><span class="p">)))</span>

<span class="c1">;; routes</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">logged-in-index</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">index</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id/likes"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">post-likes</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">single-post</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">post-content</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">logged-in-profile</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">unauthorized-profile</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">people</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">person</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>I hope you agree that seeing <code class="language-plaintext highlighter-rouge">main.lisp</code> like this helps us focus principally on the routing without worrying about the exact implementation.</p>

<h4 id="ningle-tutorial-projectasd">ningle-tutorial-project.asd</h4>

<p>As always, since we have added a new file to our project we must ensure it gets included and compiled into our project.asd file.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">:components</span> ((:module "src"
              :components
              ((:file "contrib")
               (:file "middleware")
               (:file "config")
               (:file "models")
               (:file "forms")
               (:file "migrations")
<span class="gi">+              (:file "controllers")
</span>               (:file "main"))))
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>I appreciate that this is a very short lesson this time, but after the last few lessons (and next times lesson) I think we might both appreciate a small break. It is also important to look at refactoring projects and structuring them correctly before they get <em>too</em> unwieldily. There isn’t a lot of information out there about style guides or best practice so it was best to introduce some in our own project while we had a chance.</p>

<p>Next time we will be looking at adding comments to our system, I had thought perhaps the application was good enough as an example, but there’s still some areas we might want to look at, such as self referential models, which is where comments come in, cos a comment is technically a post after all!</p>

<p>As always, I hope you found this helpful, and thanks for reading.</p>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Learning Outcome</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Understand</strong></td>
      <td>Explain how separating routing and controller logic improves readability and maintainability. Describe how <code class="language-plaintext highlighter-rouge">defpackage</code> and symbol exports control what functions are visible across modules. Summarize why refactoring helps prevent future complexity in growing projects.</td>
    </tr>
    <tr>
      <td><strong>Apply</strong></td>
      <td>Move controller functions from <code class="language-plaintext highlighter-rouge">main.lisp</code> into a new package file, update <code class="language-plaintext highlighter-rouge">main.lisp</code> to call them via route bindings, and modify the <code class="language-plaintext highlighter-rouge">.asd</code> file to include the new component. Implement a small bug fix involving SQL joins and template references.</td>
    </tr>
    <tr>
      <td><strong>Analyse</strong></td>
      <td>Compare a monolithic <code class="language-plaintext highlighter-rouge">main.lisp</code> file with a modular project layout in terms of structure and debugging clarity. Identify how exported symbols, package imports, and route bindings interact across files. Evaluate the trade-offs of consolidating or splitting functions by purpose.</td>
    </tr>
    <tr>
      <td><strong>Evaluate</strong></td>
      <td>Assess the maintainability and clarity of the refactored code. Recommend naming or packaging conventions that could further streamline the project.</td>
    </tr>
  </tbody>
</table>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-12">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<h3 id="common-lisp-hyperspec">Common Lisp HyperSpec</h3>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Type</th>
      <th>Why it appears in this lesson</th>
      <th>CLHS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defpackage</code></td>
      <td>Macro</td>
      <td>Define <code class="language-plaintext highlighter-rouge">ningle-tutorial-project/controllers</code> and <code class="language-plaintext highlighter-rouge">ningle-tutorial-project</code> packages with <code class="language-plaintext highlighter-rouge">:export</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defpac.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">in-package</code></td>
      <td>Macro</td>
      <td>Enter the package before definitions.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defvar</code></td>
      <td>Special Operator</td>
      <td>Define <code class="language-plaintext highlighter-rouge">*app*</code> as a global.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_defvar.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_defvar.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defun</code></td>
      <td>Macro</td>
      <td>Define controller functions like <code class="language-plaintext highlighter-rouge">index</code>, <code class="language-plaintext highlighter-rouge">post-content</code>, etc.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defun.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defun.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defmethod</code></td>
      <td>Macro</td>
      <td>Specialize <code class="language-plaintext highlighter-rouge">ningle:not-found</code> and <code class="language-plaintext highlighter-rouge">logged-in-posts</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">make-instance</code></td>
      <td>Generic Function</td>
      <td>Create the Ningle app object: <code class="language-plaintext highlighter-rouge">(make-instance 'ningle:app)</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_ins.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_ins.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let</code> / <code class="language-plaintext highlighter-rouge">let*</code></td>
      <td>Special Operator</td>
      <td>Local bindings for <code class="language-plaintext highlighter-rouge">user</code>, <code class="language-plaintext highlighter-rouge">form</code>, <code class="language-plaintext highlighter-rouge">posts</code>, etc.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lambda</code></td>
      <td>Special Operator</td>
      <td>Inline route requirement: <code class="language-plaintext highlighter-rouge">(lambda (value) …)</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_fn_lam.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_fn_lam.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setf</code></td>
      <td>Macro</td>
      <td>Assign route table entries and response status; generalized places.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_setf.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_setf.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gethash</code></td>
      <td>Function</td>
      <td>Pull <code class="language-plaintext highlighter-rouge">:user</code> from <code class="language-plaintext highlighter-rouge">ningle:*session*</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_gethas.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_gethas.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">make-hash-table</code></td>
      <td>Function</td>
      <td>Build JSON-ish response map in <code class="language-plaintext highlighter-rouge">post-likes</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_has.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_has.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">equal</code></td>
      <td>Function</td>
      <td>Hash table <code class="language-plaintext highlighter-rouge">:test 'equal</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_equal.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_equal.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">list</code></td>
      <td>Function</td>
      <td>Build <code class="language-plaintext highlighter-rouge">:binds</code> list for SQL and other lists.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_list.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_list.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">first</code></td>
      <td>Accessor</td>
      <td>Take first result from <code class="language-plaintext highlighter-rouge">select-dao</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_firstc.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_firstc.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">slot-value</code></td>
      <td>Function</td>
      <td>Access <code class="language-plaintext highlighter-rouge">user</code> id (<code class="language-plaintext highlighter-rouge">(slot-value user '…:id)</code> in the bug-fix snippet).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_slot__.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_slot__.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parse-integer</code></td>
      <td>Function</td>
      <td>Convert <code class="language-plaintext highlighter-rouge">:id</code> param to integer.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">format</code></td>
      <td>Function</td>
      <td>Debug-print validation errors.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_format.htm">http://www.lispworks.com/documentation/HyperSpec/Body/f_format.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">handler-case</code></td>
      <td>Macro</td>
      <td>Trap <code class="language-plaintext highlighter-rouge">parse-error</code>/<code class="language-plaintext highlighter-rouge">simple-error</code> for 404/403 pages.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_hand_1.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_hand_1.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parse-error</code></td>
      <td>Condition Type</td>
      <td>Caught when parsing route params fails.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/e_parse_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/e_parse_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">simple-error</code></td>
      <td>Condition Type</td>
      <td>Used for CSRF or general failures.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/e_smp_er.htm">http://www.lispworks.com/documentation/HyperSpec/Body/e_smp_er.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">multiple-value-bind</code></td>
      <td>Macro</td>
      <td>Unpack <code class="language-plaintext highlighter-rouge">(valid errors)</code> from <code class="language-plaintext highlighter-rouge">validate-form</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_mpv_bn.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_mpv_bn.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">progn</code></td>
      <td>Special Operator</td>
      <td>Group side effects before error handling.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_progn.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_progn.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">when</code></td>
      <td>Macro</td>
      <td>Conditional steps after validation (<code class="language-plaintext highlighter-rouge">when errors</code> / <code class="language-plaintext highlighter-rouge">when valid</code>).</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm">http://www.lispworks.com/documentation/HyperSpec/Body/m_when_.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">declare</code></td>
      <td>Special Operator</td>
      <td><code class="language-plaintext highlighter-rouge">(declare (ignore app))</code> inside <code class="language-plaintext highlighter-rouge">not-found</code>.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_declar.htm">http://www.lispworks.com/documentation/HyperSpec/Body/s_declar.htm</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">and</code> / <code class="language-plaintext highlighter-rouge">or</code></td>
      <td>Macro</td>
      <td>Logical composition in route requirements and user lookup.</td>
      <td><a href="http://www.lispworks.com/documentation/HyperSpec/Body/a_and.htm">http://www.lispworks.com/documentation/HyperSpec/Body/a_and.htm</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 11: Posting Tweets &amp;amp; Advanced Database Queries</title><link href="nmunro.github.io/2025/09/30/ningle-11.html" rel="alternate" type="text/html" title="Ningle Tutorial 11: Posting Tweets &amp;amp; Advanced Database Queries" /><published>2025-09-30T11:30:00+00:00</published><updated>2025-09-30T11:30:00+00:00</updated><id>nmunro.github.io/2025/09/30/ningle-11</id><content type="html" xml:base="nmunro.github.io/2025/09/30/ningle-11.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li>Part 11 (Posting Tweets &amp; Advanced Database Queries)</li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back! I hope you are well, this tutorial will be have us writing code to integrate the concept of “posts” into our tutorial app, up until now we had a list of posts displayed as an example of how the page <em>might</em> look, well, this changes now. In the course of this tutorial we will be adding new models and forms (like we did in <a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a>), we will be exploring a new concept in <a href="https://github.com/fukamachi/ningle">Ningle</a> that allows us to define and use our own requirements, we will also be using some advanced <a href="https://github.com/fukamachi/sxql">SXQL</a> to perform somewhat more complicated collection of data than we have previously used, and finally, we can add an honest to goodness json library for returning some responses as something other than html.</p>

<p>With any luck that all sounds exciting! We can broadly split our work this month into three sections, which should make the task easier.</p>

<h3 id="db-schema-and-forms">DB Schema and forms</h3>

<p>Here we will be defining our new models, but unlike before, not every model will be getting a form, some models will be used behind the scenes and users wont <em>directly</em> interact with. This is one of those areas where data has to be very carefully thought of, the example here is <code class="language-plaintext highlighter-rouge">likes</code>, in social media platforms, each <code class="language-plaintext highlighter-rouge">post</code> has some sort of interaction (likes, reactions, thumbsup, etc), and it <em>looks</em> like this is a property of a <code class="language-plaintext highlighter-rouge">post</code>, and indeed it might make sense to assume that a <code class="language-plaintext highlighter-rouge">post</code> “has” likes, but this isn’t actually true, what we will have is a <code class="language-plaintext highlighter-rouge">likes model</code> that relates to both a <code class="language-plaintext highlighter-rouge">post</code> and a <code class="language-plaintext highlighter-rouge">user</code>. The user is just presented with visual information that makes it <em>look</em> like <code class="language-plaintext highlighter-rouge">likes</code> are something a <code class="language-plaintext highlighter-rouge">post</code> <em>has</em>.</p>

<h4 id="srcmodelslisp">src/models.lisp</h4>

<p>Our models file will include more than just model definitions, we have some methods and functions we need to write to access or alter our data, we will have two models our <code class="language-plaintext highlighter-rouge">posts</code> and <code class="language-plaintext highlighter-rouge">likes</code>, we will use <code class="language-plaintext highlighter-rouge">likes</code> to link a <code class="language-plaintext highlighter-rouge">post</code> to a <code class="language-plaintext highlighter-rouge">user</code> (from our ningle-auth package).</p>

<p>Let’s start by defining our package and models, we will look at the other methods and functions we are exporting a little further down.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:ningle-auth/models</span> <span class="ss">#:user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:post</span>
           <span class="ss">#:id</span>
           <span class="ss">#:content</span>
           <span class="ss">#:likes</span>
           <span class="ss">#:user</span>
           <span class="ss">#:liked-post-p</span>
           <span class="ss">#:logged-in-posts</span>
           <span class="ss">#:not-logged-in-posts</span>
           <span class="ss">#:toggle-like</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/models</span><span class="p">)</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">post</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span>    <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span>    <span class="ss">:accessor</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">content</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">140</span><span class="p">)</span>          <span class="ss">:initarg</span> <span class="ss">:content</span> <span class="ss">:accessor</span> <span class="nv">content</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">likes</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span> <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span> <span class="ss">:reader</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">post</span> <span class="ss">:col-type</span> <span class="nv">post</span>                    <span class="ss">:initarg</span> <span class="ss">:post</span> <span class="ss">:reader</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span></code></pre></figure>

<p>Our post has a user and some content, we don’t have comments or reposts or anything (this is a tutorial after all!), what we want to ensure with the <code class="language-plaintext highlighter-rouge">likes</code> model though, is that there’s a unique constraint between user and post, this ensures that a user can like a specific post only once. Otherwise our like count would be unreliable.</p>

<p>In our exports list you will see we export the id, user, content, likes, post etc, but there’s more!</p>

<p>Recall that Common Lisp is a <a href="https://en.wikipedia.org/wiki/Common_Lisp#The_function_namespace">lisp-2</a> and as such we can have function/method names as the same as objects, and because of this, we will defined some methods with the name “likes” which are different from our <code class="language-plaintext highlighter-rouge">class</code> called “likes”.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">likes</span> <span class="p">(</span><span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns the number of likes a post has"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">likes</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:count-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span></code></pre></figure>

<p>Here we define a method that will accept a post and return the total number of likes it has, which will give us our likes count when we render the main page.</p>

<p>The next method we are going to write is a way to toggle the user like of a post, if they don’t like it, clicking it will like the post, if they do already like the post, clicking the like button will undo the like.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defmethod</span> <span class="nv">toggle-like</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">liked-post</span> <span class="p">(</span><span class="nv">liked-post-p</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="nv">liked-post</span>
        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">liked-post</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">not</span> <span class="nv">liked-post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">liked-post-p</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns true if a user likes a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">liked-post-p</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'likes</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span></code></pre></figure>

<p>The <code class="language-plaintext highlighter-rouge">toggle-like</code> tries to be as simple as possible, by calling the <code class="language-plaintext highlighter-rouge">liked-post-p</code> method to query if a user likes a post, and if the post <em>is</em> liked, the record of the like is deleted, if not it is created. The final thing the function does is return the <code class="language-plaintext highlighter-rouge">not</code> of <code class="language-plaintext highlighter-rouge">liked-post-p</code>, so if the post was liked at first, it will return <code class="language-plaintext highlighter-rouge">nil</code>, if the post wasn’t liked, it’ll return <code class="language-plaintext highlighter-rouge">t</code>. This will become important later, but if your function can be written in a way that can return helpful information, I suggest doing so, you may not always, or ever use the data it returns, but it’s there if you need to, it forms a usable interface.</p>

<p>Now to SQL!</p>

<p>If you are unfamiliar with SQL this part might look complicated, but in terms of SQL, it isn’t, SQL is a language used for a very specific purpose; querying and manipulating data! If you have not used SQL before/much, I highly encourage you to do so, it’s nearly 50 years old, it’s a very well tested and proven technology. It’s not going anywhere (despite what you may read online NoSQL isn’t going to replace it), and will be great for your career.</p>

<p><a href="https://github.com/fukamachi/mito">Mito</a> is a pretty thin wrapper around SQL, unlike something like Django, Rails, or Larvel (comprehensive web frameworks), Mito doesn’t have a complex <a href="https://en.wikipedia.org/wiki/Domain-specific_language">DSL</a> for abstracting the SQL details away, instead it has the user use an SQL generator <a href="https://github.com/fukamachi/sxql">SXQL</a>, so, for things beyond the simplest of things, we’re gonna have to get into SQL, which is fine.</p>

<p>We have two things we want to do:</p>

<ol>
  <li>Retrieve 50 posts ordered in descending order, with an extra column for the like count.</li>
  <li>Retrieve 50 posts ordered in descending order, with two extra columns, one for the like count, and a second indicating if the logged in user liked the post.</li>
</ol>

<p>Let’s start with the first case, a user has loaded the website, but they are not logged in. The best place to start is with the SQL query we want to run:</p>

<figure class="highlight"><pre><code class="language-sql" data-lang="sql"><span class="k">SELECT</span> <span class="n">post</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">like_count</span>
    <span class="k">FROM</span> <span class="n">post</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">likes</span> <span class="k">ON</span> <span class="p">(</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">)</span>
    <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">post</span><span class="p">.</span><span class="n">id</span>
    <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">post</span><span class="p">.</span><span class="n">created_at</span> <span class="k">DESC</span>
    <span class="k">LIMIT</span> <span class="mi">50</span><span class="p">;</span></code></pre></figure>

<p>This will give us a structure like this:</p>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>user_id</th>
      <th>content</th>
      <th>created_at</th>
      <th>updated_at</th>
      <th>like_count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>4</td>
      <td>“hi”</td>
      <td>2025-09-13 19:43:16.718416Z</td>
      <td>2025-09-13 19:43:16.718416Z</td>
      <td>5</td>
    </tr>
  </tbody>
</table>

<p>This query works by using <a href="https://www.geeksforgeeks.org/dbms/joins-in-dbms/">joins</a>, we want to get each post record and its like count, so we must join <code class="language-plaintext highlighter-rouge">post</code> and <code class="language-plaintext highlighter-rouge">likes</code> on the intersection of <code class="language-plaintext highlighter-rouge">post.id</code> and <code class="language-plaintext highlighter-rouge">likes.post.id</code>. This will allow us to iterate over the combined results and use them in our templates later.</p>

<p>We also use the <code class="language-plaintext highlighter-rouge">GROUP BY</code> clause to ensure that there is only one result per post, and that each like for a given post is summed together, so we have one post with many likes, rather than many copies of the same post each with one like.</p>

<p>We use the <code class="language-plaintext highlighter-rouge">retrieve-by-sql</code> function from <code class="language-plaintext highlighter-rouge">mito</code> which allows us to run SQL explicitly, but as previously mentioned we will use SXQL to more easily generate the SQL we might want within Common Lisp.</p>

<p>We will also use the <code class="language-plaintext highlighter-rouge">yield</code> function (from <code class="language-plaintext highlighter-rouge">SXQL</code>) to actually convert the Common Lisp representation into a string SQL can use, within that we will begin with <code class="language-plaintext highlighter-rouge">select</code> (also from <code class="language-plaintext highlighter-rouge">SXQL</code>).</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">not-logged-in-posts</span> <span class="p">()</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
            <span class="p">(</span><span class="nv">sxql:select</span>
                <span class="p">(</span><span class="ss">:post.*</span> <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))))</span></code></pre></figure>

<p>You should be able to see that our original SQL is represented quite similarly in the SXQL, here’s a table to clearly show the minor differences.</p>

<table>
<tr>
<th>SQL</th>
<th>SXQL</th>
</tr>
<tr>
<td>
<pre>SELECT post.*, COUNT(likes.id) AS like_count
    FROM post
    LEFT JOIN likes ON (post.id = likes.post_id)
    GROUP BY post.id
    ORDER BY post.created_at DESC
    LIMIT 50;</pre>
</td>
<td>
<pre>(sxql:select (:post.* (:as (:count :likes.id) :like_count))
    (sxql:from :post)
    (sxql:left-join :likes :on (:= :post.id :likes.post_id))
    (sxql:group-by :post.id)
    (sxql:order-by (:desc :post.created_at))
    (sxql:limit 50))</pre>
</td>
</tr>
</table>

<p>The next query we need to construct is that of the logged in user, which includes a column denoting likes for any specific post, this will be our second function <code class="language-plaintext highlighter-rouge">logged-in-posts</code>. As before, let’s start with what the SQL will be:</p>

<figure class="highlight"><pre><code class="language-sql" data-lang="sql"><span class="k">SELECT</span> <span class="n">post</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">like_count</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">user_likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">liked_by_user</span>
<span class="k">FROM</span> <span class="n">post</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">likes</span> <span class="k">ON</span> <span class="p">(</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">)</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">likes</span> <span class="k">AS</span> <span class="n">user_likes</span> <span class="k">ON</span> <span class="p">((</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">user_likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">)</span> <span class="k">AND</span> <span class="p">(</span><span class="n">user_likes</span><span class="p">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="o">?</span><span class="p">))</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">post</span><span class="p">.</span><span class="n">id</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">post</span><span class="p">.</span><span class="n">created_at</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="mi">50</span><span class="p">;</span></code></pre></figure>

<p>Please note that we have a <code class="language-plaintext highlighter-rouge">?</code> where the user id would go, we do not wish to be subject to SQL injection attacks, so mito allows us to bind values, but we will keep the <code class="language-plaintext highlighter-rouge">?</code> as it’s what we will use in the SXQL too.</p>

<p>Which will generate the following table structure.</p>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>user_id</th>
      <th>content</th>
      <th>created_at</th>
      <th>updated_at</th>
      <th>like_count</th>
      <th>liked_by_user</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>4</td>
      <td>“hi”</td>
      <td>2025-09-13 19:43:16.718416Z</td>
      <td>2025-09-13 19:43:16.718416Z</td>
      <td>5</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>The extra column is only a small change on the first query, by adding a new call to <code class="language-plaintext highlighter-rouge">COUNT</code> in the <code class="language-plaintext highlighter-rouge">SELECT</code> line, we prepare the column, and we get the data from the second <code class="language-plaintext highlighter-rouge">LEFT JOIN</code> which will join (using a new alias; <code class="language-plaintext highlighter-rouge">user_likes</code>) where the post id is the same as the user likes post id and where the user likes user id is the same as the logged in user, this will either return a record or null. When we call count on the record returned, it becomes 1 or 0, effectively a boolean check.</p>

<p>We can see the differences between the SQL and the SXQL here.</p>

<p>SQL</p>

<figure class="highlight"><pre><code class="language-sql" data-lang="sql"><span class="k">SELECT</span> <span class="n">post</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">like_count</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">user_likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">liked_by_user</span>
<span class="k">FROM</span> <span class="n">post</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">likes</span> <span class="k">ON</span> <span class="p">(</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">)</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">likes</span> <span class="k">AS</span> <span class="n">user_likes</span> <span class="k">ON</span> <span class="p">((</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">user_likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">)</span> <span class="k">AND</span> <span class="p">(</span><span class="n">user_likes</span><span class="p">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="o">?</span><span class="p">))</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">post</span><span class="p">.</span><span class="n">id</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">post</span><span class="p">.</span><span class="n">created_at</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="mi">50</span><span class="p">;</span></code></pre></figure>

<p>SXQL</p>

<figure class="highlight"><pre><code class="language-sql" data-lang="sql"><span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">select</span> <span class="p">(:</span><span class="n">post</span><span class="p">.</span><span class="o">*</span> <span class="p">(:</span><span class="k">as</span> <span class="p">(:</span><span class="k">count</span> <span class="p">:</span><span class="n">likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="p">:</span><span class="n">like_count</span><span class="p">)</span> <span class="p">(:</span><span class="k">as</span> <span class="p">(:</span><span class="k">count</span> <span class="p">:</span><span class="n">user_likes</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="p">:</span><span class="n">liked_by_user</span><span class="p">))</span>
<span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">from</span> <span class="p">:</span><span class="n">post</span><span class="p">)</span>
<span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">left</span><span class="o">-</span><span class="k">join</span> <span class="p">:</span><span class="n">likes</span> <span class="p">:</span><span class="k">on</span> <span class="p">(:</span><span class="o">=</span> <span class="p">:</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="p">:</span><span class="n">likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">))</span>
<span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">left</span><span class="o">-</span><span class="k">join</span> <span class="p">(:</span><span class="k">as</span> <span class="p">:</span><span class="n">likes</span> <span class="p">:</span><span class="n">user_likes</span><span class="p">)</span> <span class="p">:</span><span class="k">on</span> <span class="p">(:</span><span class="k">and</span> <span class="p">(:</span><span class="o">=</span> <span class="p">:</span><span class="n">post</span><span class="p">.</span><span class="n">id</span> <span class="p">:</span><span class="n">user_likes</span><span class="p">.</span><span class="n">post_id</span><span class="p">)</span> <span class="p">(:</span><span class="o">=</span> <span class="p">:</span><span class="n">user_likes</span><span class="p">.</span><span class="n">user_id</span> <span class="p">:</span><span class="o">?</span><span class="p">)))</span>
<span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">group</span><span class="o">-</span><span class="k">by</span> <span class="p">:</span><span class="n">post</span><span class="p">.</span><span class="n">id</span><span class="p">)</span>
<span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">order</span><span class="o">-</span><span class="k">by</span> <span class="p">(:</span><span class="k">desc</span> <span class="p">:</span><span class="n">post</span><span class="p">.</span><span class="n">created_at</span><span class="p">))</span>
<span class="p">(</span><span class="n">sxql</span><span class="p">:</span><span class="k">limit</span> <span class="mi">50</span><span class="p">)))</span></code></pre></figure>

<p>So this SXQL will be used in our function like so:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defmethod</span> <span class="nv">logged-in-posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">uid</span> <span class="p">(</span><span class="nb">slot-value</span> <span class="nv">user</span> <span class="ss">'mito.dao.mixin::id</span><span class="p">)))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
            <span class="p">(</span><span class="nv">sxql:select</span>
                <span class="p">(</span><span class="ss">:post.*</span> <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span> <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="ss">:?</span><span class="p">)))</span>
                <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
            <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="nv">uid</span><span class="p">))))</span></code></pre></figure>

<p>As mentioned before, you can see the <code class="language-plaintext highlighter-rouge">:binds</code> which will insert the user id into the SXQL query for safety.</p>

<p>So with these two complex functions in place now, we have everything we need, for clarity the complete listing of the models.lisp file is as follows:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:ningle-auth/models</span> <span class="ss">#:user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:post</span>
           <span class="ss">#:id</span>
           <span class="ss">#:content</span>
           <span class="ss">#:likes</span>
           <span class="ss">#:user</span>
           <span class="ss">#:liked-post-p</span>
           <span class="ss">#:logged-in-posts</span>
           <span class="ss">#:not-logged-in-posts</span>
           <span class="ss">#:toggle-like</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/models</span><span class="p">)</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">post</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span>    <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span>    <span class="ss">:accessor</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">content</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">140</span><span class="p">)</span>          <span class="ss">:initarg</span> <span class="ss">:content</span> <span class="ss">:accessor</span> <span class="nv">content</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">likes</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span> <span class="ss">:col-type</span> <span class="nv">ningle-auth/models:user</span> <span class="ss">:initarg</span> <span class="ss">:user</span> <span class="ss">:reader</span> <span class="nv">user</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">post</span> <span class="ss">:col-type</span> <span class="nv">post</span>                    <span class="ss">:initarg</span> <span class="ss">:post</span> <span class="ss">:reader</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">likes</span> <span class="p">(</span><span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns the number of likes a post has"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">likes</span> <span class="p">((</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:count-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">toggle-like</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Toggles the like of a user to a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">toggle-like</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">liked-post</span> <span class="p">(</span><span class="nv">liked-post-p</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="nv">liked-post</span>
        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">liked-post</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'likes</span> <span class="ss">:post</span> <span class="nv">post</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">not</span> <span class="nv">liked-post</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">liked-post-p</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">post</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Returns true if a user likes a given post"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">liked-post-p</span> <span class="p">((</span><span class="nv">ningle-auth/models:user</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">post</span> <span class="nv">post</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'likes</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">logged-in-posts</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Gets the posts for a logged in user"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">logged-in-posts</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">uid</span> <span class="p">(</span><span class="nb">slot-value</span> <span class="nv">user</span> <span class="ss">'mito.dao.mixin::id</span><span class="p">)))</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
            <span class="p">(</span><span class="nv">sxql:select</span>
                <span class="p">(</span><span class="ss">:post.*</span>
                <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">)</span>
                <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:user_likes.id</span><span class="p">)</span> <span class="ss">:liked_by_user</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="p">(</span><span class="ss">:as</span> <span class="ss">:likes</span> <span class="ss">:user_likes</span><span class="p">)</span>
                                <span class="ss">:on</span> <span class="p">(</span><span class="ss">:and</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:user_likes.post_id</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_likes.user_id</span> <span class="ss">:?</span><span class="p">)))</span>
                <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))</span>
            <span class="ss">:binds</span> <span class="p">(</span><span class="nb">list</span> <span class="nv">uid</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">not-logged-in-posts</span> <span class="p">()</span>
    <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span>
        <span class="p">(</span><span class="nv">sxql:yield</span>
        <span class="p">(</span><span class="nv">sxql:select</span>
            <span class="p">(</span><span class="ss">:post.*</span> <span class="p">(</span><span class="ss">:as</span> <span class="p">(</span><span class="ss">:count</span> <span class="ss">:likes.id</span><span class="p">)</span> <span class="ss">:like_count</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:from</span> <span class="ss">:post</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:left-join</span> <span class="ss">:likes</span> <span class="ss">:on</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:post.id</span> <span class="ss">:likes.post_id</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:group-by</span> <span class="ss">:post.id</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">sxql:order-by</span> <span class="p">(</span><span class="ss">:desc</span> <span class="ss">:post.created_at</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">sxql:limit</span> <span class="mi">50</span><span class="p">)))))</span></code></pre></figure>

<h4 id="srcformslisp">src/forms.lisp</h4>

<p>Our forms are much simpler, we only have one form, the post. While we do have the likes model, our users will not be directly using that, and thus we don’t need to render a form for this.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/forms</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:cl-forms</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:post</span>
           <span class="ss">#:content</span>
           <span class="ss">#:submit</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/forms</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*post-validator*</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:not-blank</span><span class="p">)</span>
                                     <span class="p">(</span><span class="nv">clavier:is-a-string</span><span class="p">)</span>
                                     <span class="p">(</span><span class="nv">clavier:len</span> <span class="ss">:max</span> <span class="mi">140</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">defform</span> <span class="nv">post</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"post"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span> <span class="ss">:action</span> <span class="s">"/post"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">content</span>  <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*post-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span>   <span class="ss">:submit</span>   <span class="ss">:label</span> <span class="s">"Post"</span><span class="p">)))</span></code></pre></figure>

<p>Like we used in a previous tutorial, we use the <code class="language-plaintext highlighter-rouge">clavier</code> validation library to ensure that our users post things that fit within the constraints of our system, we also want to make sure we are using CSRF tokens for security.</p>

<p>We will style this form using CSS later.</p>

<h4 id="srcmigrationslisp">src/migrations.lisp</h4>

<p>Now, our main project now contains its own migrations, we perhaps should have written the code to perform migrations in another file and reserved this for specific migrations, but we can work with things the way they are.</p>

<p>We are going to start by adding a function to the top of our <code class="language-plaintext highlighter-rouge">migrations.lisp</code> file.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:post</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:likes</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:post</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:likes</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span></code></pre></figure>

<p>These will be the project specific migrations, however we still need a way to trigger them, and since we wrote a way to apply specific apps only, we need a way to exclude these if we do not wish to run these migrations.</p>

<p>The next thing we need to do is to extend the <code class="language-plaintext highlighter-rouge">migrate-apps</code> function we previously wrote. We will add a parameter to the function:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(defun migrate-apps (&amp;optional (apps nil) &amp;key skip-root)
</code></pre></div></div>

<p>And within the <code class="language-plaintext highlighter-rouge">macro</code> call:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(with-db-connection
    ...)
</code></pre></div></div>

<p>We add:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(unless skip-root
    (format t "Running root project migrations...~%")
    (migrate))
</code></pre></div></div>

<p>There is also a small correction we need to make, this line.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(error "Migrate function not found in package ~A." migrations-pkg-name)
</code></pre></div></div>

<p>Needs to be corrected to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(error (format nil "Migrate function not found in package ~A." migrations-pkg-name))
</code></pre></div></div>

<p>Full listing:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:ningle-tutorial-project/contrib</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate-apps</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:post</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:likes</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:post</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:likes</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate-apps</span> <span class="p">(</span><span class="k">&amp;optional</span> <span class="p">(</span><span class="nv">apps</span> <span class="no">nil</span><span class="p">)</span> <span class="k">&amp;key</span> <span class="nv">skip-root</span><span class="p">)</span>
  <span class="s">"Run migrate function for each app in APPS list. If APPS is nil, migrate all apps listed in *config* :installed-apps."</span>

  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">apps</span> <span class="p">(</span><span class="nb">or</span> <span class="nv">apps</span> <span class="p">(</span><span class="nb">getf</span> <span class="p">(</span><span class="nv">envy:config</span> <span class="ss">:ningle-tutorial-project/config</span><span class="p">)</span> <span class="ss">:installed-apps</span><span class="p">))))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">apps</span>
      <span class="p">(</span><span class="nb">error</span> <span class="s">"No apps specified and no :installed-apps found in config."</span><span class="p">))</span>

    <span class="p">(</span><span class="nv">with-db-connection</span>
        <span class="p">(</span><span class="nb">unless</span> <span class="nv">skip-root</span>
          <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Running root project migrations...~%"</span><span class="p">)</span>
          <span class="p">(</span><span class="nv">migrate</span><span class="p">))</span>

        <span class="p">(</span><span class="nb">dolist</span> <span class="p">(</span><span class="nv">app</span> <span class="nv">apps</span><span class="p">)</span>
            <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">migrations-pkg-name</span> <span class="p">(</span><span class="nb">string-upcase</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A/MIGRATIONS"</span> <span class="p">(</span><span class="nb">string-upcase</span> <span class="p">(</span><span class="nb">symbol-name</span> <span class="nv">app</span><span class="p">)))))</span>
                   <span class="p">(</span><span class="nv">migrations-pkg</span> <span class="p">(</span><span class="nb">find-package</span> <span class="nv">migrations-pkg-name</span><span class="p">)))</span>
                <span class="p">(</span><span class="nb">unless</span> <span class="nv">migrations-pkg</span>
                    <span class="p">(</span><span class="nb">error</span> <span class="s">"Migrations package ~A not found."</span> <span class="nv">migrations-pkg-name</span><span class="p">))</span>

                <span class="c1">;; Set app-specific config before calling migrate</span>
                <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">migrate-fn</span> <span class="p">(</span><span class="nb">find-symbol</span> <span class="s">"MIGRATE"</span> <span class="nv">migrations-pkg</span><span class="p">)))</span> <span class="c1">;; Name known to project</span>
                    <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">and</span> <span class="nv">migrate-fn</span> <span class="p">(</span><span class="nb">fboundp</span> <span class="nv">migrate-fn</span><span class="p">))</span>
                        <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"Migrate function not found in package ~A."</span> <span class="nv">migrations-pkg-name</span><span class="p">)))</span>
                    <span class="p">(</span><span class="nb">funcall</span> <span class="nv">migrate-fn</span><span class="p">)))))))</span></code></pre></figure>

<h4 id="ningle-tutorial-projectasd">ningle-tutorial-project.asd</h4>

<p>With these files added, we need to remember to add them to our project.asd file.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:components</span> <span class="p">((</span><span class="ss">:module</span> <span class="s">"src"</span>
                <span class="ss">:components</span>
                <span class="p">((</span><span class="ss">:file</span> <span class="s">"contrib"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"middleware"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"config"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"models"</span><span class="p">)</span>  <span class="c1">; add this line</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"forms"</span><span class="p">)</span>   <span class="c1">; add this line</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"migrations"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"main"</span><span class="p">))))</span></code></pre></figure>

<h3 id="controller-logic">Controller Logic</h3>

<h4 id="srcmainlisp">src/main.lisp</h4>

<p>We will now look at the controller logic to handle posting, well, posts. We will introduce a feature of <code class="language-plaintext highlighter-rouge">Ningle</code> we have not yet looked into that can help us create smaller, more specialised, logical units of work, <code class="language-plaintext highlighter-rouge">requirements</code>. Ningle has the ability to define conditions that can be passed as keyword arguments to a controller, if the condition is true, the controller is triggered. In our controllers previously we have had <code class="language-plaintext highlighter-rouge">if</code> checks for if a user is logged in, or if a request is a <code class="language-plaintext highlighter-rouge">GET</code> or a <code class="language-plaintext highlighter-rouge">POST</code>, these requirements allow us to write smaller functions to help us focus on one specific type of request (even if on the same route). I find this helps me, personally, if I can reduce the number of things I have to be remembering when I am working on a function.</p>

<p>Before we do, however, we will allow our main code to use the forms we defined in the previous section.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span> <span class="ss">:ningle-tutorial-project/forms</span><span class="p">)</span> <span class="c1">; Add the :ningle-tutorial-project/forms bit!</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project</span><span class="p">)</span></code></pre></figure>

<p>Now with that in place we can begin in earnest! We already use these requirements already with our <code class="language-plaintext highlighter-rouge">:method '(:GET :POST)</code> that we used previously, but we can define our own! We will define a requirement that there is a logged in user. In our <code class="language-plaintext highlighter-rouge">src/main.lisp</code> file, before the routes we previously defined, we will add this:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:requirement</span> <span class="vg">*app*</span> <span class="ss">:logged-in-p</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">value</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span> <span class="nv">value</span><span class="p">)))</span></code></pre></figure>

<p>Since this will be used as a keyword argument, the <code class="language-plaintext highlighter-rouge">lambda</code> function will always define a parameter, this will be the value found to the key word argument later when this is used in a route definition. We will use this <code class="language-plaintext highlighter-rouge">requirement</code> in a few places here, starting with our “/” route.</p>

<p>Previously we just had a dummy response that returned what we thought the posts might look like, but now we have the capability to store and retrieve posts from a database we can change this now.</p>

<p>We have different database queries too, a query to run when a user is not logged in, and a query to run when they are, this this helps split our controllers into a logged in view, and a not logged in view.</p>

<p>A quick word on controller definitions, if you have multiple controllers, you must define the most specific ones first! So we will start by defining a view that matches on “/” and when <code class="language-plaintext highlighter-rouge">logged-in-p</code> is <code class="language-plaintext highlighter-rouge">t</code>, because if we try to match on “/” first, then it matches every controller for that route, ignoring any other specific requirements of it, so we must define our logged in view first!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:logged-in-posts</span> <span class="nv">user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))))</span></code></pre></figure>

<p>In this controller we ensure that there is a user that is logged in using <code class="language-plaintext highlighter-rouge">:logged-in-p t</code>, and another change this controller handles if a user is logged in, is permitting them to post! So this controller grabs the logged in user, the form for posting content and the first 50 posts (which is what <code class="language-plaintext highlighter-rouge">logged-in-posts</code> does) and renders them in the template.</p>

<p>Then we can define a more general “/” controller after it.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:not-logged-in-posts</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">))))</span></code></pre></figure>

<p>This is simpler, by not needing a user or post form, we can forgo these and simply get a list of posts with <code class="language-plaintext highlighter-rouge">not-logged-in-posts</code>. Although, now I think about it, I could have written a helper method that takes a user object and runs these functions depending on if the user is <code class="language-plaintext highlighter-rouge">nil</code> or not, you live and learn!</p>

<p>Please note that these two controllers will replace the previous “/” controller we had.</p>

<p>With these in place we need a controller to toggle the <code class="language-plaintext highlighter-rouge">liked</code> status of a post.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id/likes"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
     <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
       <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
              <span class="p">(</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
              <span class="p">(</span><span class="nv">res</span> <span class="p">(</span><span class="nb">make-hash-table</span> <span class="ss">:test</span> <span class="ss">'equal</span><span class="p">)))</span>
           <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:post</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))</span>
           <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:likes</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">))</span>
           <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:liked</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:toggle-like</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">com.inuoe.jzon:stringify</span> <span class="nv">res</span><span class="p">))))</span></code></pre></figure>

<p>Here, this controller is permitted to <code class="language-plaintext highlighter-rouge">POST</code> only and requires that a user is logged in, we obviously don’t want users that aren’t logged in to be able to like posts. So we grab the user, the post that is to be liked and we create a <code class="language-plaintext highlighter-rouge">hash-table</code> for creating our response because here, we actually use the <code class="language-plaintext highlighter-rouge">jzon</code> package to return a valid json response. This controller sets the <code class="language-plaintext highlighter-rouge">:post</code>, <code class="language-plaintext highlighter-rouge">:likes</code>, and <code class="language-plaintext highlighter-rouge">:liked</code> fields and stringifies the <code class="language-plaintext highlighter-rouge">hash-table</code> so it can be read as json. We need to grab the post id from the url, but we have seen this before.</p>

<p>Our next controller simply directs the user to a specific post.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">handler-case</span>
          <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">)))))</span>
              <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/post.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Post"</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

          <span class="p">(</span><span class="kt">parse-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span></code></pre></figure>

<p>We set up a <code class="language-plaintext highlighter-rouge">handler-case</code> to attempt to load a specific post and render the template, if that fails, we set a 404 response code and render the error page.</p>

<p>Moving on now to actually posting some content! Once again this controller should only be permitted to serve <code class="language-plaintext highlighter-rouge">POST</code> requests and require that a user is logged in. As we have seen previously in this series we need to grab the user object and the form that was submitted. From there we do the error handling <code class="language-plaintext highlighter-rouge">handler-case</code> by handling the loading of the form, we handle the values of <code class="language-plaintext highlighter-rouge">valid</code>, or <code class="language-plaintext highlighter-rouge">errors</code> and enter the content of a post into the database if there’s no errors, if there are, a 403 is set and the error is rendered.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
              <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)))</span>
          <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
              <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

              <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                  <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span><span class="p">)</span> <span class="nv">form</span>
                    <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
              <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
              <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))))))</span></code></pre></figure>

<p>Finally we now look to replace the “/profile” controllers, we have already explored the new concepts but this serves as a simple, clear example, and it helps we need to work on this further anyway!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
          <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">)))</span></code></pre></figure>

<p>Full listing:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span> <span class="ss">:ningle-tutorial-project/forms</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="c1">;; requirements</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:requirement</span> <span class="vg">*app*</span> <span class="ss">:logged-in-p</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">value</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span> <span class="nv">value</span><span class="p">)))</span>

<span class="c1">;; routes</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:logged-in-posts</span> <span class="nv">user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">posts</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:not-logged-in-posts</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id/likes"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
     <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
       <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
              <span class="p">(</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))))</span>
              <span class="p">(</span><span class="nv">res</span> <span class="p">(</span><span class="nb">make-hash-table</span> <span class="ss">:test</span> <span class="ss">'equal</span><span class="p">)))</span>
           <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:post</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">))</span>
           <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:likes</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:likes</span> <span class="nv">post</span><span class="p">))</span>
           <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:liked</span> <span class="nv">res</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-tutorial-project/models:toggle-like</span> <span class="nv">user</span> <span class="nv">post</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">com.inuoe.jzon:stringify</span> <span class="nv">res</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post/:id"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">handler-case</span>
          <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">post</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:id</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:id</span> <span class="nv">params</span><span class="p">)))))</span>
              <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/post.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Post"</span> <span class="ss">:post</span> <span class="nv">post</span><span class="p">))</span>

          <span class="p">(</span><span class="kt">parse-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">)))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/post"</span> <span class="ss">:method</span> <span class="ss">:POST</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
              <span class="p">(</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'post</span><span class="p">)))</span>
          <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">progn</span>
              <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

              <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                  <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                  <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">content</span><span class="p">)</span> <span class="nv">form</span>
                    <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:post</span> <span class="ss">:content</span> <span class="nv">content</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span>

            <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
              <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
              <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span> <span class="ss">:logged-in-p</span> <span class="no">t</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
          <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">username-or-email</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">person</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                              <span class="ss">'ningle-auth/models:user</span>
                              <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username-or-email</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">username-or-email</span><span class="p">)))))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:person</span> <span class="nv">person</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span></code></pre></figure>

<h4 id="ningle-tutorial-projectasd-1">ningle-tutorial-project.asd</h4>

<p>There’s one final thing to add before we look at the aesthetic changes we will be applying, we need to ensure we add the <code class="language-plaintext highlighter-rouge">jzon</code> package to our project dependencies.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:cl-dotenv</span>
               <span class="ss">:clack</span>
               <span class="ss">:djula</span>
               <span class="ss">:cl-forms</span>
               <span class="ss">:cl-forms.djula</span>
               <span class="ss">:cl-forms.ningle</span>
               <span class="ss">:envy</span>
               <span class="ss">:envy-ningle</span>
               <span class="ss">:ingle</span>
               <span class="ss">:com.inuoe.jzon</span> <span class="c1">; &lt;- Add this line</span>
               <span class="ss">:mito</span>
               <span class="ss">:mito-auth</span>
               <span class="ss">:ningle</span>
               <span class="ss">:ningle-auth</span><span class="p">)</span></code></pre></figure>

<h3 id="html-changes">HTML Changes</h3>

<p>We make some changes to our html, sadly the biggest part of it is JavaScript, but nevermind!</p>

<h4 id="srctemplatebasehtml">src/template/base.html</h4>

<p>In our base template we only make a couple of changes, in our <code class="language-plaintext highlighter-rouge">&lt;head&gt;&lt;/head&gt;</code> section, prior to loading our own css, we must include the bootstrap icons package.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css"</span><span class="nt">&gt;</span> <span class="nt">&lt;</span><span class="err">!</span> <span class="na">--</span> <span class="na">add</span> <span class="na">this</span> <span class="na">line</span><span class="err">!</span> <span class="na">--</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static "</span><span class="na">css</span><span class="err">/</span><span class="na">main.css</span><span class="err">"</span> <span class="err">%}"</span><span class="nt">/&gt;</span></code></pre></figure>

<p>Next, right at the bottom, we include a way to add JS to templates, if we need to.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script&gt;</span>
    <span class="p">{</span><span class="o">%</span> <span class="nx">block</span> <span class="nx">js</span> <span class="o">%</span><span class="p">}</span>
    <span class="p">{</span><span class="o">%</span> <span class="nx">endblock</span> <span class="o">%</span><span class="p">}</span>
<span class="nt">&lt;/script&gt;</span></code></pre></figure>

<p>Full listing:</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="cp">&lt;!doctype html&gt;</span>
<span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        {% if title %}
            <span class="nt">&lt;title&gt;</span>{{ title }} - Y<span class="nt">&lt;/title&gt;</span>
        {% else %}
            <span class="nt">&lt;title&gt;</span>Welcome to Y<span class="nt">&lt;/title&gt;</span>
        {% endif %}
        <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static "</span><span class="na">css</span><span class="err">/</span><span class="na">main.css</span><span class="err">"</span> <span class="err">%}"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;nav</span> <span class="na">class=</span><span class="s">"navbar navbar-expand-lg navbar-dark bg-dark"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container-fluid"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"navbar-brand"</span> <span class="na">href=</span><span class="s">"/"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"{% static "</span><span class="na">images</span><span class="err">/</span><span class="na">logo.jpg</span><span class="err">"</span> <span class="err">%}"</span> <span class="na">alt=</span><span class="s">"Logo"</span> <span class="na">class=</span><span class="s">"d-inline-block align-text-top logo"</span><span class="nt">&gt;</span>
                    Y
                <span class="nt">&lt;/a&gt;</span>

                <span class="nt">&lt;button</span> <span class="na">class=</span><span class="s">"navbar-toggler"</span> <span class="na">type=</span><span class="s">"button"</span> <span class="na">data-bs-toggle=</span><span class="s">"collapse"</span> <span class="na">data-bs-target=</span><span class="s">"#navbarSupportedContent"</span> <span class="na">aria-controls=</span><span class="s">"navbarSupportedContent"</span> <span class="na">aria-expanded=</span><span class="s">"false"</span> <span class="na">aria-label=</span><span class="s">"Toggle navigation"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"navbar-toggler-icon"</span><span class="nt">&gt;&lt;/span&gt;</span>
                <span class="nt">&lt;/button&gt;</span>

                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"collapse navbar-collapse"</span> <span class="na">id=</span><span class="s">"navbarSupportedContent"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"navbar-nav me-auto"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"nav-item {% ifequal title "</span><span class="na">Home</span><span class="err">"</span> <span class="err">%}</span><span class="na">disabled</span><span class="err">{%</span> <span class="na">endifequal</span> <span class="err">%}"</span><span class="nt">&gt;</span>
                            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"nav-link"</span> <span class="na">href=</span><span class="s">"/"</span><span class="nt">&gt;</span>Home<span class="nt">&lt;/a&gt;</span>
                        <span class="nt">&lt;/li&gt;</span>
                        <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"nav-item {% ifequal title "</span><span class="na">People</span><span class="err">"</span> <span class="err">%}</span><span class="na">disabled</span><span class="err">{%</span> <span class="na">endifequal</span> <span class="err">%}"</span><span class="nt">&gt;</span>
                            <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"nav-link"</span> <span class="na">href=</span><span class="s">"/people"</span><span class="nt">&gt;</span>People<span class="nt">&lt;/a&gt;</span>
                        <span class="nt">&lt;/li&gt;</span>
                    <span class="nt">&lt;/ul&gt;</span>

                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"d-flex ms-auto"</span><span class="nt">&gt;</span>
                        {% if user %}
                            <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/profile"</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span><span class="nt">&gt;</span>{{ user.username }}<span class="nt">&lt;/a&gt;</span>
                            <span class="ni">&amp;nbsp;</span>|<span class="ni">&amp;nbsp;</span>
                            <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/auth/logout"</span> <span class="na">class=</span><span class="s">"btn btn-secondary"</span><span class="nt">&gt;</span>Logout<span class="nt">&lt;/a&gt;</span>
                        {% else %}
                            <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/auth/register"</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span><span class="nt">&gt;</span>Register<span class="nt">&lt;/a&gt;</span>
                            <span class="ni">&amp;nbsp;</span>|<span class="ni">&amp;nbsp;</span>
                            <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/auth/login"</span> <span class="na">class=</span><span class="s">"btn btn-success"</span><span class="nt">&gt;</span>Login<span class="nt">&lt;/a&gt;</span>
                        {% endif %}
                    <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/nav&gt;</span>

        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container mt-4"</span><span class="nt">&gt;</span>
            {% block content %}
            {% endblock %}
        <span class="nt">&lt;/div&gt;</span>

        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
        <span class="nt">&lt;script&gt;</span>
            <span class="p">{</span><span class="o">%</span> <span class="nx">block</span> <span class="nx">js</span> <span class="o">%</span><span class="p">}</span>
            <span class="p">{</span><span class="o">%</span> <span class="nx">endblock</span> <span class="o">%</span><span class="p">}</span>
        <span class="nt">&lt;/script&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span></code></pre></figure>

<h4 id="srctemplatemainindexhtml">src/template/main/index.html</h4>

<p>Our index page will need to include some JavaScript, this will be with the intention of sending a request to the controller to increment/decrement the <code class="language-plaintext highlighter-rouge">like</code> count of a post. Again since this tutorial is about Common Lisp, I won’t really be explaining the JS.</p>

<p>In the first part of the container div, we will add our form to post content:</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="c">&lt;!-- Post form --&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
            {% if form %}
                {% form form %}
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    ...</code></pre></figure>

<p>This displays the full form, including labels we don’t necessarily need, so we hide this using the css that was written, but this will only show when a user is logged in and will post content for the logged in user.</p>

<p>Next we will be changing the structure of the contents of our posts <code class="language-plaintext highlighter-rouge">for</code> loop, nothing major, but since we have better CSS we might want to ensure our HTML matches it.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% for post in posts %}
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card post mb-3"</span> <span class="na">data-href=</span><span class="s">"/post/{{ post.id }}"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title mb-2"</span><span class="nt">&gt;</span>{{ post.content }}<span class="nt">&lt;/h5&gt;</span>
      <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-subtitle text-muted mb-0"</span><span class="nt">&gt;</span>@{{ post.user.username }}<span class="nt">&lt;/p&gt;</span>
      <span class="nt">&lt;/div&gt;</span>

      <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-footer d-flex justify-content-between align-items-center"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"button"</span>
                <span class="na">class=</span><span class="s">"btn btn-sm btn-outline-primary like-button"</span>
                <span class="na">data-post-id=</span><span class="s">"{{ post.id }}"</span>
                <span class="na">data-logged-in=</span><span class="s">"{% if user.username != "</span><span class="err">"</span> <span class="err">%}</span><span class="na">true</span><span class="err">{%</span> <span class="na">else</span> <span class="err">%}</span><span class="na">false</span><span class="err">{%</span> <span class="na">endif</span> <span class="err">%}"</span>
                <span class="na">data-liked=</span><span class="s">"{% if post.liked-by-user == 1 %}true{% else %}false{% endif %}"</span>
                <span class="na">aria-label=</span><span class="s">"Like post {{ post.id }}"</span><span class="nt">&gt;</span>
            {% if post.liked-by-user == 1 %}
              <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up-fill text-primary"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
            {% else %}
              <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up text-muted"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
            {% endif %}
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"ms-1 like-count"</span><span class="nt">&gt;</span>{{ post.like-count }}<span class="nt">&lt;/span&gt;</span>
        <span class="nt">&lt;/button&gt;</span>

        <span class="nt">&lt;small</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>Posted on: {{ post.created-at }}<span class="nt">&lt;/small&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
{% endfor %}</code></pre></figure>

<p>Then in the case where we do not have any posts!</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% if not posts %}
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-center"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>No posts to display.<span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
{% endif %}</code></pre></figure>

<p>Finally the dreaded JS!</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="p">{</span><span class="o">%</span> <span class="nx">block</span> <span class="nx">js</span> <span class="o">%</span><span class="p">}</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">"</span><span class="s2">.like-button</span><span class="dl">"</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">btn</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">btn</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">e</span><span class="p">.</span><span class="nx">stopPropagation</span><span class="p">();</span>
    <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>

    <span class="c1">// Check login</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">loggedIn</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">You must be logged in to like posts.</span><span class="dl">"</span><span class="p">);</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">postId</span> <span class="o">=</span> <span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">postId</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">countSpan</span> <span class="o">=</span> <span class="nx">btn</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">.like-count</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">icon</span> <span class="o">=</span> <span class="nx">btn</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">i</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">liked</span> <span class="o">=</span> <span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">liked</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">previous</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">countSpan</span><span class="p">.</span><span class="nx">textContent</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`/post/</span><span class="p">${</span><span class="nx">postId</span><span class="p">}</span><span class="s2">/likes`</span><span class="p">;</span>

    <span class="c1">// Optimistic UI toggle</span>
    <span class="nx">countSpan</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">liked</span> <span class="p">?</span> <span class="nx">previous</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">:</span> <span class="nx">previous</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
    <span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">liked</span> <span class="o">=</span> <span class="nx">liked</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">false</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span><span class="p">;</span>

    <span class="c1">// Toggle icon classes optimistically</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">liked</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Currently liked, so unlike it</span>
      <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up text-muted</span><span class="dl">"</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="c1">// Currently not liked, so like it</span>
      <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up-fill text-primary</span><span class="dl">"</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">csrfTokenMeta</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">meta[name="csrf-token"]</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">};</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">csrfTokenMeta</span><span class="p">)</span> <span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">X-CSRF-Token</span><span class="dl">"</span><span class="p">]</span> <span class="o">=</span> <span class="nx">csrfTokenMeta</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">content</span><span class="dl">"</span><span class="p">);</span>

    <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="nx">headers</span><span class="p">,</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="na">toggle</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">resp</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">resp</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Revert optimistic changes on error</span>
        <span class="nx">countSpan</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">previous</span><span class="p">;</span>
        <span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">liked</span> <span class="o">=</span> <span class="nx">liked</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">false</span><span class="dl">"</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">liked</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up-fill text-primary</span><span class="dl">"</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up text-muted</span><span class="dl">"</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Network response was not ok</span><span class="dl">"</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">data</span> <span class="o">&amp;&amp;</span> <span class="k">typeof</span> <span class="nx">data</span><span class="p">.</span><span class="nx">likes</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">countSpan</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">likes</span><span class="p">;</span>
        <span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">liked</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">liked</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">false</span><span class="dl">"</span><span class="p">;</span>

        <span class="c1">// Update icon based on server response</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">liked</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up-fill text-primary</span><span class="dl">"</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up text-muted</span><span class="dl">"</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Like failed:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
      <span class="c1">// Revert optimistic changes on error</span>
      <span class="nx">countSpan</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">previous</span><span class="p">;</span>
      <span class="nx">btn</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">liked</span> <span class="o">=</span> <span class="nx">liked</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">false</span><span class="dl">"</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">liked</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up-fill text-primary</span><span class="dl">"</span><span class="p">;</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">icon</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">bi bi-hand-thumbs-up text-muted</span><span class="dl">"</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">"</span><span class="s2">.card.post</span><span class="dl">"</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">card</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">card</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">href</span> <span class="o">=</span> <span class="nx">card</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">href</span><span class="p">)</span> <span class="p">{</span>
      <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">href</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">});</span>
<span class="p">});</span>
<span class="p">{</span><span class="o">%</span> <span class="nx">endblock</span> <span class="o">%</span><span class="p">}</span></code></pre></figure>

<p>Full listing:</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="c">&lt;!-- Post form --&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
            {% if form %}
                {% form form %}
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="c">&lt;!-- Posts Section --&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            {% for post in posts %}
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card post mb-3"</span> <span class="na">data-href=</span><span class="s">"/post/{{ post.id }}"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                  <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title mb-2"</span><span class="nt">&gt;</span>{{ post.content }}<span class="nt">&lt;/h5&gt;</span>
                  <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-subtitle text-muted mb-0"</span><span class="nt">&gt;</span>@{{ post.user.username }}<span class="nt">&lt;/p&gt;</span>
                <span class="nt">&lt;/div&gt;</span>

                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-footer d-flex justify-content-between align-items-center"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"button"</span>
                        <span class="na">class=</span><span class="s">"btn btn-sm btn-outline-primary like-button"</span>
                        <span class="na">data-post-id=</span><span class="s">"{{ post.id }}"</span>
                        <span class="na">data-logged-in=</span><span class="s">"{% if user.username != "</span><span class="err">"</span> <span class="err">%}{%</span> <span class="na">endraw</span> <span class="err">%</span><span class="na">true</span><span class="err">{%</span> <span class="na">endraw</span> <span class="err">%</span><span class="na">true</span><span class="err">{%</span> <span class="na">raw</span> <span class="err">%}{%</span> <span class="na">else</span> <span class="err">%}</span><span class="na">false</span><span class="err">{%</span> <span class="na">endif</span> <span class="err">%}"</span>
                        <span class="na">data-liked=</span><span class="s">"{% if post.liked-by-user == 1 %}true{% else %}false{% endif %}"</span>
                        <span class="na">aria-label=</span><span class="s">"Like post {{ post.id }}"</span><span class="nt">&gt;</span>
                    {% if post.liked-by-user == 1 %}
                      <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up-fill text-primary"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                    {% else %}
                      <span class="nt">&lt;i</span> <span class="na">class=</span><span class="s">"bi bi-hand-thumbs-up text-muted"</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/i&gt;</span>
                    {% endif %}
                    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"ms-1 like-count"</span><span class="nt">&gt;</span>{{ post.like-count }}<span class="nt">&lt;/span&gt;</span>
                <span class="nt">&lt;/button&gt;</span>

                <span class="nt">&lt;small</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>Posted on: {{ post.created-at }}{% raw %}<span class="nt">&lt;/small&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
            {% raw %}{% endfor %}

            {% if not posts %}
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>No posts to display.<span class="nt">&lt;/p&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}

{% block js %}
document.querySelectorAll(".like-button").forEach(btn =&gt; {
  btn.addEventListener("click", function (e) {
    e.stopPropagation();
    e.preventDefault();

    // Check login
    if (btn.dataset.loggedIn !== "true") {
      alert("You must be logged in to like posts.");
      return;
    }

    const postId = btn.dataset.postId;
    const countSpan = btn.querySelector(".like-count");
    const icon = btn.querySelector("i");
    const liked = btn.dataset.liked === "true";
    const previous = parseInt(countSpan.textContent, 10) || 0;
    const url = `/post/${postId}/likes`;

    // Optimistic UI toggle
    countSpan.textContent = liked ? previous - 1 : previous + 1;
    btn.dataset.liked = liked ? "false" : "true";

    // Toggle icon classes optimistically
    if (liked) {
      // Currently liked, so unlike it
      icon.className = "bi bi-hand-thumbs-up text-muted";
    } else {
      // Currently not liked, so like it
      icon.className = "bi bi-hand-thumbs-up-fill text-primary";
    }

    const csrfTokenMeta = document.querySelector('meta[name="csrf-token"]');
    const headers = { "Content-Type": "application/json" };
    if (csrfTokenMeta) headers["X-CSRF-Token"] = csrfTokenMeta.getAttribute("content");

    fetch(url, {
      method: "POST",
      headers: headers,
      body: JSON.stringify({ toggle: true })
    })
    .then(resp =&gt; {
      if (!resp.ok) {
        // Revert optimistic changes on error
        countSpan.textContent = previous;
        btn.dataset.liked = liked ? "true" : "false";
        if (liked) {
          icon.className = "bi bi-hand-thumbs-up-fill text-primary";
        } else {
          icon.className = "bi bi-hand-thumbs-up text-muted";
        }
        throw new Error("Network response was not ok");
      }
      return resp.json();
    })
    .then(data =&gt; {
      if (data <span class="err">&amp;&amp;</span> typeof data.likes !== "undefined") {
        countSpan.textContent = data.likes;
        btn.dataset.liked = data.liked ? "true" : "false";

        // Update icon based on server response
        if (data.liked) {
          icon.className = "bi bi-hand-thumbs-up-fill text-primary";
        } else {
          icon.className = "bi bi-hand-thumbs-up text-muted";
        }
      }
    })
    .catch(err =&gt; {
      console.error("Like failed:", err);
      // Revert optimistic changes on error
      countSpan.textContent = previous;
      btn.dataset.liked = liked ? "true" : "false";
      if (liked) {
        icon.className = "bi bi-hand-thumbs-up-fill text-primary";
      } else {
        icon.className = "bi bi-hand-thumbs-up text-muted";
      }
    });
  });
});

document.querySelectorAll(".card.post").forEach(card =&gt; {
  card.addEventListener("click", function () {
    const href = card.dataset.href;
    if (href) {
      window.location.href = href;
    }
  });
});
{% endblock %}</code></pre></figure>

<h4 id="srctemplatemainposthtml">src/template/main/post.html</h4>

<p>We will add a new post template, this isn’t actually for creating a post, as we saw above we integrated that form into the index page, but rather this is the template for showing an individual post. In the future we might introduce comments etc and this would make it easier to see all of that content in one page.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h2&gt;</span>{{ post.user.username }}<span class="nt">&lt;/h2&gt;</span>
            <span class="nt">&lt;p&gt;</span>{{ post.content }}<span class="nt">&lt;/p&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}</code></pre></figure>

<h3 id="css-changes">CSS Changes</h3>

<p>I made a number of css changes (with the help of AI, cos I hate writing CSS!), and I wanted to include them here, but since the objective of this tutorial is Lisp not the nuances of selectors, I will just include the full listing without comments.</p>

<h4 id="srcstaticcssmaincss">src/static/css/main.css</h4>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nc">.logo</span> <span class="p">{</span>
    <span class="nl">height</span><span class="p">:</span> <span class="m">30px</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">30px</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.error-404</span> <span class="p">{</span>
    <span class="nl">height</span><span class="p">:</span> <span class="m">75vh</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">input</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>  <span class="c">/* Ensure inputs take up the full width */</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span> <span class="cp">!important</span><span class="p">;</span> <span class="c">/* Override any conflicting styles */</span>
    <span class="nl">max-width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="c">/* Ensure no unnecessary constraints */</span>
    <span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"email"</span><span class="o">],</span>
<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"text"</span><span class="o">],</span>
<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"password"</span><span class="o">]</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.form-control;</span> <span class="c">/* Apply Bootstrap's .form-control */</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="c">/* Ensure they are block-level elements */</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="c">/* Make the input full width */</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span> <span class="c">/* Spacing */</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">select</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.form-select;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.btn;</span>
    <span class="err">@extend</span> <span class="err">.btn-primary;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">div</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.mb-3;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#signup</span> <span class="nt">label</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.form-label;</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="nb">bold</span><span class="p">;</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>  <span class="c">/* Ensure inputs take up the full width */</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span> <span class="cp">!important</span><span class="p">;</span> <span class="c">/* Override any conflicting styles */</span>
    <span class="nl">max-width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="c">/* Ensure no unnecessary constraints */</span>
    <span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"text"</span><span class="o">],</span>
<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"password"</span><span class="o">]</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.form-control;</span> <span class="c">/* Apply Bootstrap's .form-control */</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="c">/* Ensure they are block-level elements */</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="c">/* Make the input full width */</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span> <span class="c">/* Spacing */</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.btn;</span>
    <span class="err">@extend</span> <span class="err">.btn-primary;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">div</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.mb-3;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#post</span> <span class="nt">div</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.mb-3;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#post</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="py">gap</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span> <span class="cp">!important</span><span class="p">;</span>
<span class="p">}</span>

<span class="c">/* Make the input wrapper expand */</span>
<span class="nt">form</span><span class="nf">#post</span> <span class="o">&gt;</span> <span class="nt">div</span><span class="nd">:first-of-type</span> <span class="p">{</span>
    <span class="nl">flex</span><span class="p">:</span> <span class="m">1</span> <span class="m">1</span> <span class="nb">auto</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="nl">min-width</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>  <span class="c">/* allow shrinking */</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#post</span> <span class="nt">label</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span> <span class="cp">!important</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#post</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"text"</span><span class="o">]</span> <span class="p">{</span>
    <span class="nl">flex</span><span class="p">:</span> <span class="m">1</span> <span class="m">1</span> <span class="m">0%</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="nl">min-width</span><span class="p">:</span> <span class="m">0</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="c">/* Bootstrap .form-control styles */</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">0.375rem</span> <span class="m">0.75rem</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="m">400</span><span class="p">;</span>
    <span class="nl">line-height</span><span class="p">:</span> <span class="m">1.5</span><span class="p">;</span>
    <span class="nl">color</span><span class="p">:</span> <span class="m">#212529</span><span class="p">;</span>
    <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span>
    <span class="nl">background-clip</span><span class="p">:</span> <span class="n">padding-box</span><span class="p">;</span>
    <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#ced4da</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">0.375rem</span><span class="p">;</span>
    <span class="nl">transition</span><span class="p">:</span> <span class="n">border-color</span> <span class="m">.15s</span> <span class="n">ease-in-out</span><span class="p">,</span> <span class="n">box-shadow</span> <span class="m">.15s</span> <span class="n">ease-in-out</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#post</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span> <span class="p">{</span>
    <span class="nl">flex</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="nb">auto</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="c">/* Bootstrap .btn + .btn-primary styles */</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="m">400</span><span class="p">;</span>
    <span class="nl">color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span>
    <span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
    <span class="nl">vertical-align</span><span class="p">:</span> <span class="nb">middle</span><span class="p">;</span>
    <span class="py">user-select</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="nl">background-color</span><span class="p">:</span> <span class="m">#0d6efd</span><span class="p">;</span>
    <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#0d6efd</span><span class="p">;</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">0.375rem</span> <span class="m">0.75rem</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
    <span class="nl">line-height</span><span class="p">:</span> <span class="m">1.5</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">0.375rem</span><span class="p">;</span>
    <span class="nl">transition</span><span class="p">:</span> <span class="n">color</span> <span class="m">.15s</span> <span class="n">ease-in-out</span><span class="p">,</span> <span class="n">background-color</span> <span class="m">.15s</span> <span class="n">ease-in-out</span><span class="p">,</span>
                <span class="n">border-color</span> <span class="m">.15s</span> <span class="n">ease-in-out</span><span class="p">,</span> <span class="n">box-shadow</span> <span class="m">.15s</span> <span class="n">ease-in-out</span><span class="p">;</span>
    <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">form</span><span class="nf">#post</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span><span class="nd">:hover</span> <span class="p">{</span>
    <span class="nl">background-color</span><span class="p">:</span> <span class="m">#0b5ed7</span><span class="p">;</span>
    <span class="nl">border-color</span><span class="p">:</span> <span class="m">#0a58ca</span><span class="p">;</span>
<span class="p">}</span>

<span class="c">/* Post container styling */</span>
<span class="nc">.post</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>                <span class="c">/* Makes the whole card clickable */</span>
    <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>         <span class="c">/* Remove underline from link */</span>
    <span class="nl">color</span><span class="p">:</span> <span class="nb">inherit</span><span class="p">;</span>                <span class="c">/* Use normal text color */</span>
    <span class="nl">background</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span>              <span class="c">/* White card background */</span>
    <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#dee2e6</span><span class="p">;</span>     <span class="c">/* Subtle border */</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>         <span class="c">/* Rounded corners */</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>                 <span class="c">/* Inner spacing */</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>           <span class="c">/* Space between posts */</span>
    <span class="nl">transition</span><span class="p">:</span> <span class="n">box-shadow</span> <span class="m">0.2s</span> <span class="n">ease</span><span class="p">,</span> <span class="n">transform</span> <span class="m">0.1s</span> <span class="n">ease</span><span class="p">;</span>
    <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
<span class="p">}</span>

<span class="c">/* Hover/active effect */</span>
<span class="nc">.post</span><span class="nd">:hover</span> <span class="p">{</span>
    <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">4px</span> <span class="m">12px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.08</span><span class="p">);</span>
    <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-2px</span><span class="p">);</span>
    <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>   <span class="c">/* still no underline on hover */</span>
<span class="p">}</span>

<span class="c">/* Post title/content */</span>
<span class="nc">.post-title</span> <span class="p">{</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="m">600</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">1.1rem</span><span class="p">;</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">0.25rem</span><span class="p">;</span>
    <span class="nl">color</span><span class="p">:</span> <span class="m">#0d6efd</span><span class="p">;</span>  <span class="c">/* bootstrap primary link color */</span>
<span class="p">}</span>

<span class="c">/* Post meta info */</span>
<span class="nc">.post-meta</span> <span class="p">{</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">0.875rem</span><span class="p">;</span>
    <span class="nl">color</span><span class="p">:</span> <span class="m">#6c757d</span><span class="p">;</span>  <span class="c">/* muted gray */</span>
    <span class="nl">margin-top</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<h3 id="conclusion">Conclusion</h3>

<p>Phew! That was another big one, but the good news is that most of the key pieces of building an application with <code class="language-plaintext highlighter-rouge">Ningle</code> and <code class="language-plaintext highlighter-rouge">Mito</code> are in place, next month we will look at tidying up our project. We are far from done with this tutorial series though, as we will still need to look at hosting our applications, testing, and developing good practices.</p>

<p>Thank you for following this tutorial series, I hope you are finding it as interesting/helpful to read as I am finding it interesting/helpful to write.</p>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Learning Outcome</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Remember</strong></td>
      <td>Define the purpose of post and likes models in Ningle. Recall the role of SXQL in generating SQL queries.</td>
    </tr>
    <tr>
      <td><strong>Understand</strong></td>
      <td>Explain how toggle-like manages user interactions with posts. Describe how requirements (e.g., :logged-in-p) simplify route definitions. Interpret SQL queries that use JOIN and GROUP BY to aggregate like counts. Summarize how SXQL represents SQL constructs such as LEFT JOIN, COUNT, and AS. Explain why COUNT(user_likes.id) can be used to represent a boolean “liked by user” column.</td>
    </tr>
    <tr>
      <td><strong>Apply</strong></td>
      <td>Use cl-forms to create a validated post submission form with CSRF protection. Implement not-logged-in-posts and logged-in-posts to retrieve posts with like counts.</td>
    </tr>
    <tr>
      <td><strong>Analyse</strong></td>
      <td>Compare the differences between raw SQL and SXQL representations for joins and counts. Distinguish between logged-in and non-logged-in query results.</td>
    </tr>
  </tbody>
</table>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-11">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<h3 id="common-lisp-hyperspec">Common Lisp HyperSpec</h3>

<ul>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defpkg.htm">defpackage</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_in_pkg.htm">in-package</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defvar.htm">defvar</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defpar.htm">defparameter</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defun.htm">defun</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defgen.htm">defgeneric</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defmet.htm">defmethod</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_fn.htm">lambda</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_setf.htm">setf</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">let</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm">let*</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_if.htm">if</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_when.htm">when</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_unless.htm">unless</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_not.htm">not</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/a_and.htm">and</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/a_or.htm">or</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_eq.htm">eq</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_equal.htm">equal</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_format.htm">format</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_multip.htm">multiple-value-bind</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_slotva.htm">slot-value</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_symb_2.htm">find-symbol</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_stg_up.htm">string-upcase</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_symb_1.htm">symbol-name</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_firstc.htm">first</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_list.htm">list</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_error.htm">error</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_handle.htm">handler-case</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_parse_.htm">parse-integer</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_mk_has.htm">make-hash-table</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_gethas.htm">gethash</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_dolist.htm">dolist</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/d_ignore.htm">ignore</a></li>
  <li><a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_declar.htm">declare</a></li>
</ul>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 10: Email</title><link href="nmunro.github.io/2025/08/28/ningle-10.html" rel="alternate" type="text/html" title="Ningle Tutorial 10: Email" /><published>2025-08-28T11:30:00+00:00</published><updated>2025-08-28T11:30:00+00:00</updated><id>nmunro.github.io/2025/08/28/ningle-10</id><content type="html" xml:base="nmunro.github.io/2025/08/28/ningle-10.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li>Part 10 (Email)</li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back to this tutorial series, in this chapter we are going to write a small app for sending email and connect it up to the authentication system we wrote last time, as part of that we will need to expand the settings we have in our main project. We will also look at different ways in which you can send email, from outputting the console (as a dummy test), <a href="https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol">smtp</a> (simple mail transfer protocol), and the <a href="https://sendgrid.com/en-us">sendgrid</a> service.</p>

<h3 id="main-package-part-1">Main Package (Part 1)</h3>

<p>There isn’t too much to change in this package, the most we will be doing is creating a series of settings objects to test the different email options. Of course, we will be introducing new settings and relying on the <a href="https://github.com/nmunro/envy-ningle">envy-ningle</a> package to load them for us. We will also create some templates in our auth application, but we will override them in our application using the templates override mechanism we developed previously.</p>

<p>There will be a number of required settings and some settings that will only be used in certain circumstances, we have seen before in <a href="https://nmunro.github.io/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a> how to use these settings objects.</p>

<p>Let’s start with the common, shared settings, we have our <code class="language-plaintext highlighter-rouge">:common</code> settings object:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="ss">:common</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:application-root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:component-pathname</span> <span class="p">(</span><span class="nv">asdf:find-system</span> <span class="ss">:ningle-tutorial-project</span><span class="p">))</span>
    <span class="ss">:installed-apps</span> <span class="p">(</span><span class="ss">:ningle-auth</span><span class="p">)</span>
    <span class="ss">:auth-mount-path</span> <span class="o">,</span><span class="vg">*auth-mount-path*</span>
    <span class="ss">:login-redirect</span> <span class="s">"/"</span>
    <span class="ss">:project-name</span> <span class="s">"NTP"</span>                  <span class="c1">; 1: add this</span>
    <span class="ss">:token-expiration</span> <span class="mi">3600</span>               <span class="c1">; 2: add this</span>
    <span class="ss">:email-admins</span> <span class="p">(</span><span class="s">"nmunro@duck.com"</span><span class="p">)))</span>  <span class="c1">; 3: add this</span></code></pre></figure>

<p>Our first setting is simply creating a name for our project that we can use in email titles etc.</p>

<p>The second setting is to add is related to the tokens we created, we might want to lower this during testing, and restore it when we go into production, it makes sense to centralise it! I should have considered this last time, but the chapter was so big, I had to make cuts somewhere!</p>

<p>The third setting is related to mailing project admins, in the event there’s an error we can mail someone (or in this case a list of people), it’s something we will explore, but not necessarily use, because this is, after all, a tutorial project and not a full blown commercial application.</p>

<p>We have four settings objects we need to create to test everything we need, we will continue using <code class="language-plaintext highlighter-rouge">sqlite</code> for our config, but we will explore the following email setups:</p>

<ul>
  <li>dummy (console)</li>
  <li>smtp with ethereal (a demo smtp service)</li>
  <li>smtp with gmail</li>
  <li>sendgrid</li>
</ul>

<p>Since we are going to write lots of new settings, each of which is going to duplicate the middleware, we are going to explore how to modularize settings (at least a little)!</p>

<p>We will start by defining a new settings block, but it will only contain our middleware settings, we will, for good measure also look into extracting out the database settings, if, for whatever reason, we need to change them in future.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|database-settings|</span>
  <span class="o">`</span><span class="p">((</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:sqlite3</span> <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">)))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|middleware|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="nv">ningle-tutorial-project/middleware:refresh-roles</span>
                 <span class="o">,@</span><span class="nv">|database-settings|</span>
                 <span class="p">(</span><span class="ss">:mount</span> <span class="o">,</span><span class="vg">*auth-mount-path*</span> <span class="o">,</span><span class="nv">ningle-auth:*app*</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span></code></pre></figure>

<p>We can see here that a small, database specific settings block exists that defines our database connection settings, when then include it inside a middleware settings block which we will now use in our dummy email settings block:</p>

<p>Prior to writing our settings, we will follow good security practices and NOT store details in our repository, so we will need to edit our <code class="language-plaintext highlighter-rouge">.env</code> file.</p>

<h4 id="env">.env</h4>

<p>I obviously didn’t include the actual values here, but simply wanted to include the settings names, for clarity.</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">EMAIL_DEFAULT_FROM</span><span class="o">=</span>xxx

<span class="nv">SMTP_GMAIL_HOST</span><span class="o">=</span>xxx
<span class="nv">SMTP_GMAIL_ACCOUNT_NAME</span><span class="o">=</span>xxx
<span class="nv">SMTP_GMAIL_PASSWORD</span><span class="o">=</span>xxx

<span class="nv">SMTP_ETHEREAL_HOST</span><span class="o">=</span>xxx
<span class="nv">SMTP_ETHEREAL_ACCOUNT_NAME</span><span class="o">=</span>xxx
<span class="nv">SMTP_ETHEREAL_PASSWORD</span><span class="o">=</span>xxx

<span class="nv">SENDGRID_API_KEY</span><span class="o">=</span>xxx</code></pre></figure>

<p>Some settings only apply to certain configurations, and some settings require some setup, for example if you want to use ethereal, you will need to set up an account and grab the user, account, and password, if you want to use sendgrid, you will need to get an api key etc.</p>

<p>These tasks I leave up to you, but I will mention them as each settings require them, just remember to come back and add in the settings you need.</p>

<h4 id="dummy-email-backend-settings">Dummy Email Backend Settings</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|dummy-email|</span>
<span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="o">,@</span><span class="nv">|sqlite-middleware|</span>
    <span class="ss">:email-backend</span> <span class="ss">:dummy</span>                                      <span class="c1">; 1</span>
    <span class="ss">:email-default-from</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"EMAIL_DEFAULT_FROM"</span><span class="p">)))</span>  <span class="c1">; 2</span></code></pre></figure>

<p>This helps us really focus on what we are adding in, it’s worth noting that these settings don’t configure anything yet, but they will when we write the email package, but for now we are:</p>

<ol>
  <li>Defining an email dummy back end (this will be used to print email to the terminal)</li>
  <li>Setup a default “from” address</li>
</ol>

<p>Since this requires the <code class="language-plaintext highlighter-rouge">EMAIL_DEFAULT_FROM</code> setting, please ensure you have an actual value stored.</p>

<p>Our next three configs follow a similar pattern.</p>

<h4 id="ethereal-smtp-email-backend-settings">Ethereal SMTP Email Backend Settings</h4>

<p><a href="https://ethereal.email/">Ethereal</a> is a free fake smtp service, it’s a great way to check smtp settings are correct prior to potentially spamming an email account with testing emails. We will use this as a test, while I have an example for smtp settings for gmail, this is not a comprehensive guide to every email provider, so etheral should help you test things, if I have not covered your specific email provider, or… Like me, your account was too locked down to use as an email.</p>

<p>Ethereal has a <a href="https://ethereal.email/help">help</a> page where you can find the <code class="language-plaintext highlighter-rouge">host</code> settings etc. The <code class="language-plaintext highlighter-rouge">SMTP_ETHERAL_ACCOUNT_NAME</code> gets used for the <code class="language-plaintext highlighter-rouge">:email-default-from</code> and <code class="language-plaintext highlighter-rouge">:email-reply-to</code> as well as part of the <code class="language-plaintext highlighter-rouge">:email-auth</code> settings, there will also be an account password when you set an account up, which will be stored as <code class="language-plaintext highlighter-rouge">SMTP_ETHEREAL_PASSWORD</code> and used in the <code class="language-plaintext highlighter-rouge">:email-auth</code> too.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|ethereal-smtp|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="o">,@</span><span class="nv">|middleware|</span>
    <span class="ss">:email-backend</span> <span class="ss">:smtp</span>
    <span class="ss">:email-smtp-host</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_ETHEREAL_HOST"</span><span class="p">)</span>
    <span class="ss">:email-default-from</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_ETHEREAL_ACCOUNT_NAME"</span><span class="p">)</span>
    <span class="ss">:email-reply-to</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_ETHEREAL_ACCOUNT_NAME"</span><span class="p">)</span>
    <span class="ss">:email-port</span> <span class="mi">587</span>
    <span class="ss">:email-auth</span> <span class="p">(</span><span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_ETHEREAL_ACCOUNT_NAME"</span><span class="p">)</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_ETHEREAL_PASSWORD"</span><span class="p">))</span>
    <span class="ss">:email-ssl</span> <span class="ss">:starttls</span><span class="p">))</span></code></pre></figure>

<p>Remember: Add the following to your <code class="language-plaintext highlighter-rouge">.env</code> file!</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SMTP_ETHEREAL_ACCOUNT_NAME</code></li>
  <li><code class="language-plaintext highlighter-rouge">SMTP_ETHEREAL_PASSWORD</code></li>
</ul>

<p>When we come to test this, we can use their web interface to check if email <em>would</em> have been sent.</p>

<h4 id="gmail-smtp-email-backend-settings">GMail SMTP Email Backend Settings</h4>

<p>Setting up GMail for smtp can be a little tricky, certain security settings have to be enabled (and certain ones NOT), at a minimum you must have mfa set up on the account, and Google no longer allows username and passwords as authentication, you must set up an “app password” for your application and use that for the authentication.</p>

<p>No big deal really, but it’s some gotchas that you’ll want to be aware of if you are using GMail as your email provider, again this isn’t a tutorial on how to configure GMail for SMTP, this is how to make Common Lisp use it once it is configured.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|gmail-smtp|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="o">,@</span><span class="nv">|middleware|</span>
    <span class="ss">:email-backend</span> <span class="ss">:smtp</span>
    <span class="ss">:email-smtp-host</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_GMAIL_HOST"</span><span class="p">)</span>
    <span class="ss">:email-default-from</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_GMAIL_ACCOUNT_NAME"</span><span class="p">)</span>
    <span class="ss">:email-reply-to</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_GMAIL_ACCOUNT_NAME"</span><span class="p">)</span>
    <span class="ss">:email-port</span> <span class="mi">587</span>
    <span class="ss">:email-auth</span> <span class="p">(</span><span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_GMAIL_ACCOUNT_NAME"</span><span class="p">)</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SMTP_GMAIL_PASSWORD"</span><span class="p">))</span>
    <span class="ss">:email-ssl</span> <span class="ss">:starttls</span><span class="p">))</span></code></pre></figure>

<p>Remember: Add in the following values in your <code class="language-plaintext highlighter-rouge">.env</code> file!</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SMTP_GMAIL_HOST</code></li>
  <li><code class="language-plaintext highlighter-rouge">SMTP_GMAIL_ACCOUNT_NAME</code></li>
  <li><code class="language-plaintext highlighter-rouge">SMTP_GMAIL_PASSWORD</code></li>
</ul>

<h4 id="sendgrid-email-backend-settings">SendGrid Email Backend Settings</h4>

<p><a href="https://sendgrid.com/en-us">Sendgrid</a> is a popular way to send mass emails, to get set up you will need an account with an api-key. Once you have those, the settings are as follows.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|sendgrid|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="o">,@</span><span class="nv">|middleware|</span>
    <span class="ss">:email-backend</span> <span class="ss">:sendgrid</span>
    <span class="ss">:email-reply-to</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"EMAIL_DEFAULT_FROM"</span><span class="p">)</span>
    <span class="ss">:sendgrid-api-key</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SENDGRID_API_KEY"</span><span class="p">)))</span></code></pre></figure>

<p>Remember: Add the following to your <code class="language-plaintext highlighter-rouge">.env</code> file!</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">EMAIL_DEFAULT_FROM</code></li>
  <li><code class="language-plaintext highlighter-rouge">SENDGRID_API_KEY</code></li>
</ul>

<h3 id="email-package">Email Package</h3>

<p>Now that we have your config in place, we can look at building an email package, don’t worry though it’s less than 50 lines, so nothing too crazy, we just create a package because <a href="https://github.com/fukamachi/ningle">Ningle</a> is a micro framework and so we create small packages to work with it. Perhaps in a later version of this series we build a tighter coupled framework, but not right now.</p>

<p>Using my <a href="https://github.com/nmunro/nmunro-project">project builder</a> create a new project like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(nmunro-project:make-project #p"~/quicklisp/local-projects/ningle-email")
</code></pre></div></div>

<p>In the project <code class="language-plaintext highlighter-rouge">asd</code> file we need to depend on three packages:</p>

<ol>
  <li>envy-ningle</li>
  <li>cl-smtp</li>
  <li>cl-sendgrid</li>
</ol>

<p>And with that, we can edit <code class="language-plaintext highlighter-rouge">ningle-email/src/main.lisp</code> and write two simple mail functions <code class="language-plaintext highlighter-rouge">send-mail</code> and <code class="language-plaintext highlighter-rouge">mail-admins</code>.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-email</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:mail-admins</span>
           <span class="ss">#:send-mail</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-email</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">mail-admins</span> <span class="p">(</span><span class="nv">subject</span> <span class="nv">message</span><span class="p">)</span>
  <span class="s">"Sends an email to the admins"</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">project-name</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:project-name</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">admins</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-admins</span><span class="p">)))</span>
    <span class="p">(</span><span class="nv">send-mail</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"[~A]: ~A"</span> <span class="nv">project-name</span> <span class="nv">subject</span><span class="p">)</span> <span class="nv">message</span> <span class="nv">admins</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">send-mail</span> <span class="p">(</span><span class="nv">subject</span> <span class="nv">content</span> <span class="nv">to</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">from</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-default-from</span><span class="p">)))</span>
  <span class="s">"Sends arbitrary email"</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">email-backend</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-backend</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">case</span> <span class="nv">email-backend</span>
        <span class="p">(</span><span class="ss">:dummy</span>
         <span class="p">(</span><span class="k">progn</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"from: ~A~%"</span> <span class="nv">from</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"to: ~A~%"</span> <span class="nv">to</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"subject: ~A~%"</span> <span class="nv">subject</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"content: ~A~%"</span> <span class="nv">content</span><span class="p">)))</span>

        <span class="p">(</span><span class="ss">:smtp</span>
            <span class="p">(</span><span class="nv">cl-smtp:send-email</span>
                <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-smtp-host</span><span class="p">)</span>
                <span class="nv">from</span>
                <span class="nv">to</span>
                <span class="nv">subject</span>
                <span class="nv">message</span>
                <span class="ss">:port</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-port</span><span class="p">)</span> <span class="mi">587</span><span class="p">)</span>
                <span class="ss">:ssl</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-ssl</span><span class="p">)</span> <span class="ss">:starttls</span><span class="p">)</span>
                <span class="ss">:authentication</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-auth</span><span class="p">)))</span>

        <span class="p">(</span><span class="ss">:sendgrid</span>
            <span class="p">(</span><span class="nv">sendgrid:send-email</span>
                <span class="ss">:to</span> <span class="nv">to</span>
                <span class="ss">:from</span> <span class="nv">from</span>
                <span class="ss">:subject</span> <span class="nv">subject</span>
                <span class="ss">:content</span> <span class="nv">message</span>
                <span class="ss">:api-key</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:sendgrid-api-key</span><span class="p">)))</span>

        <span class="p">(</span><span class="nv">otherwise</span>
            <span class="p">(</span><span class="nb">error</span> <span class="s">"Unknown email backend: ~A"</span> <span class="nv">email-backend</span><span class="p">)))))</span></code></pre></figure>

<p>It may seem a little unusual to define the <code class="language-plaintext highlighter-rouge">mail-admins</code> before we have defined our <code class="language-plaintext highlighter-rouge">send-mail</code> and while in common lisp it’s possible to compile a function that calls a function that doesn’t yet exist, because it will be compiled immediately after.</p>

<p>Our new <code class="language-plaintext highlighter-rouge">mail-admins</code> function will be a simple wrapper around the <code class="language-plaintext highlighter-rouge">send-mail</code> function, so we will look at that first.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">mail-admins</span> <span class="p">(</span><span class="nv">subject</span> <span class="nv">message</span><span class="p">)</span>
  <span class="s">"Sends an email to the admins"</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">project-name</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:project-name</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">admins</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-admins</span><span class="p">)))</span>
    <span class="p">(</span><span class="nv">send-mail</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"[~A]: ~A"</span> <span class="nv">project-name</span> <span class="nv">subject</span><span class="p">)</span> <span class="nv">message</span> <span class="nv">admins</span><span class="p">)))</span></code></pre></figure>

<p>We don’t yet know the shape of our <code class="language-plaintext highlighter-rouge">send-mail</code> function, we only know that we will use it, and in fact, thinking about how we will get and pass information into it, will help us see how its interface might be. When we mail our admins, we already know who we are emailing (our admins) and we also know who the email will be from (our application) so in reality we need a subject and message as parameters.</p>

<p>Although we know who is being mailed by who, we might want to make clear <em>what</em> they are being emailed by, our admins probably get a lot of mail, so I have made a choice that the email title will be <code class="language-plaintext highlighter-rouge">[NTP]: &lt;project name&gt;</code> in this way it’s clear that the service has mailed them.</p>

<p>We create a <code class="language-plaintext highlighter-rouge">let</code> block that grabs the project name from the settings. We also get the list of project admins from the settings in this block too and we simply call <code class="language-plaintext highlighter-rouge">send-mail</code> with a subject (our <code class="language-plaintext highlighter-rouge">format</code> expression), a message and a list of recipients (our admins), and with that done, we now know our <code class="language-plaintext highlighter-rouge">send-mail</code> function has parameters for a subject, a message, and a list of recipients, we <em>might</em> want to change the default sender, so we can add a <code class="language-plaintext highlighter-rouge">&amp;key</code> parameter for this, but we will default it to putting the email from the settings.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">send-mail</span> <span class="p">(</span><span class="nv">subject</span> <span class="nv">content</span> <span class="nv">to</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">from</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-default-from</span><span class="p">)))</span>
  <span class="s">"Sends arbitrary email"</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">email-backend</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-backend</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">case</span> <span class="nv">email-backend</span>
        <span class="p">(</span><span class="ss">:dummy</span>
            <span class="p">(</span><span class="k">progn</span>
                <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"from: ~A~%"</span> <span class="nv">from</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"to: ~A~%"</span> <span class="nv">to</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"subject: ~A~%"</span> <span class="nv">subject</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"content: ~A~%"</span> <span class="nv">content</span><span class="p">)))</span>

        <span class="p">(</span><span class="ss">:smtp</span>
            <span class="p">(</span><span class="nv">cl-smtp:send-email</span>
                <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-smtp-host</span><span class="p">)</span>
                <span class="nv">from</span>
                <span class="nv">to</span>
                <span class="nv">subject</span>
                <span class="nv">message</span>
                <span class="ss">:port</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-port</span><span class="p">)</span> <span class="mi">587</span><span class="p">)</span>
                <span class="ss">:ssl</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-ssl</span><span class="p">)</span> <span class="ss">:starttls</span><span class="p">)</span>
                <span class="ss">:authentication</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:email-auth</span><span class="p">)))</span>

        <span class="p">(</span><span class="ss">:sendgrid</span>
            <span class="p">(</span><span class="nv">sendgrid:send-email</span>
                <span class="ss">:to</span> <span class="nv">to</span>
                <span class="ss">:from</span> <span class="nv">from</span>
                <span class="ss">:subject</span> <span class="nv">subject</span>
                <span class="ss">:content</span> <span class="nv">message</span>
                <span class="ss">:api-key</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:sendgrid-api-key</span><span class="p">)))</span>

        <span class="p">(</span><span class="nv">otherwise</span>
            <span class="p">(</span><span class="nb">error</span> <span class="s">"Unknown email backend: ~A"</span> <span class="nv">email-backend</span><span class="p">)))))</span></code></pre></figure>

<p>As you can see, our parameters are quite simply what our <code class="language-plaintext highlighter-rouge">mail-admins</code> specified, the only tricky thing is the <code class="language-plaintext highlighter-rouge">from</code> parameter, which simply pulls a default value of <code class="language-plaintext highlighter-rouge">:email-default-from</code> from our settings, so in most cases the <code class="language-plaintext highlighter-rouge">send-mail</code> function will do exactly the right thing, but it’s possible to override the <code class="language-plaintext highlighter-rouge">from</code>, if needed.</p>

<p>The rest of this function is really quite simple, it’s just a <code class="language-plaintext highlighter-rouge">case</code> that checks the <code class="language-plaintext highlighter-rouge">:email-backend</code> setting we defined in our settings and dispatches to another package for the actual logic. The <code class="language-plaintext highlighter-rouge">:dummy</code> backend simply prints the email information to the terminal, the <code class="language-plaintext highlighter-rouge">:smtp</code> backend delegates to the <code class="language-plaintext highlighter-rouge">cl-smtp</code> package, the <code class="language-plaintext highlighter-rouge">:sendgrid</code> backend delegates to the <code class="language-plaintext highlighter-rouge">:cl-sendgrid</code> package and, finally, if the email backend wasn’t recognised and error is signalled.</p>

<p>That <em>really</em> is all we need to write for our email package, with it complete we can look at integrating it into our project as a whole and into the auth package we built last time.</p>

<h3 id="auth-package">Auth Package</h3>

<p>Since we now created a package we will be relying on, the first thing we need to do is to ensure we include it in the dependencies of this project.</p>

<h4 id="projectasd">project.asd</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:cl-dotenv</span>
             <span class="ss">:clack</span>
             <span class="ss">:djula</span>
             <span class="ss">:cl-forms</span>
             <span class="ss">:cl-forms.djula</span>
             <span class="ss">:cl-forms.ningle</span>
             <span class="ss">:envy-ningle</span>
             <span class="ss">:mito</span>
             <span class="ss">:ningle</span>
             <span class="ss">:local-time</span>
             <span class="ss">:cu-sith</span>
             <span class="ss">:ningle-email</span><span class="p">)</span> <span class="c1">; add this</span></code></pre></figure>

<h4 id="modelslisp">models.lisp</h4>

<p>We will make a <em>slight</em> change to the models, but this is only to support the expiration time that we defined in our settings. In our <code class="language-plaintext highlighter-rouge">ningle-auth/src/models.lisp</code> file we will make two changes.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defmethod</span> <span class="nb">initialize-instance</span> <span class="ss">:after</span> <span class="p">((</span><span class="nv">token</span> <span class="nv">token</span><span class="p">)</span> <span class="k">&amp;rest</span> <span class="nv">initargs</span> <span class="k">&amp;key</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">slot-boundp</span> <span class="nv">token</span> <span class="ss">'salt</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">token-salt</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nv">ironclad:make-random-salt</span> <span class="mi">16</span><span class="p">)))</span>

  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">slot-boundp</span> <span class="nv">token</span> <span class="ss">'expires-at</span><span class="p">)</span>
    <span class="c1">; change the below line from 3600 to the :token-expiration setting</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">token-expires-at</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:token-expiration</span><span class="p">)))))</span> </code></pre></figure>

<p>And here.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="c1">; Again change the token from 3600 to the value stored in the setting</span>
<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">generate-token</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">purpose</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">expires-in</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:token-expiration</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">member</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">error</span> <span class="s">"Invalid token purpose: ~A. Allowed: ~A"</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span><span class="p">))</span>

    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">salt</span> <span class="p">(</span><span class="nv">ironclad:make-random-salt</span> <span class="mi">16</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">expires-at</span> <span class="p">(</span><span class="nb">truncate</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="nv">expires-in</span><span class="p">)))</span>
           <span class="p">(</span><span class="kt">base-string</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A~A~A"</span> <span class="p">(</span><span class="nv">username</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">expires-at</span> <span class="nv">salt</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">hash</span> <span class="p">(</span><span class="nv">ironclad:byte-array-to-hex-string</span> <span class="p">(</span><span class="nv">ironclad:digest-sequence</span> <span class="ss">:sha256</span> <span class="p">(</span><span class="nv">babel:string-to-octets</span> <span class="kt">base-string</span><span class="p">)))))</span>
        <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">purpose</span> <span class="ss">:token</span> <span class="nv">hash</span> <span class="ss">:salt</span> <span class="nv">salt</span> <span class="ss">:expires-at</span> <span class="nv">expires-at</span><span class="p">)))</span></code></pre></figure>

<h4 id="mainlisp">main.lisp</h4>

<p>Now, since we deal a lot with token generations that are actually urls in our application, I decided we should simplify this a little by creating some utlity functions that generate these, as we do call them over and over again under different circumstances.</p>

<p>So, the first thing to add into our <code class="language-plaintext highlighter-rouge">main.lisp</code> are these utility functions:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">build-url-root</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">path</span> <span class="s">""</span><span class="p">))</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A://~A:~A~A"</span>
    <span class="p">(</span><span class="nv">lack/request:request-uri-scheme</span> <span class="nv">ningle:*request*</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">)</span>
    <span class="nv">path</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">build-activation-link</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">token</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">host</span> <span class="p">(</span><span class="nv">build-url-root</span> <span class="ss">:path</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">))))</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A/verify?user=~A&amp;token=~A~%"</span> <span class="nv">host</span> <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">build-reset-link</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">token</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">host</span> <span class="p">(</span><span class="nv">build-url-root</span> <span class="ss">:path</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">))))</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A/reset/process?user=~A&amp;token=~A~%"</span> <span class="nv">host</span> <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))))</span></code></pre></figure>

<p>In other frameworks there would ideally be a way to build an absolute url from the request object, but <code class="language-plaintext highlighter-rouge">ningle</code> is pretty lightweight, so we will make do with these.</p>

<p>We start with the <code class="language-plaintext highlighter-rouge">build-url-root</code>, which will build a url from the request object, using the scheme, server name, port, and any path parts. At the moment I don’t do any checking for the port to be 80 or 443, maybe that’s something for later! The intention is this will build up the basic part of our url, and the two functions <code class="language-plaintext highlighter-rouge">build-activation-link</code> and <code class="language-plaintext highlighter-rouge">build-reset-link</code> will use it to, well, build the links.</p>

<p>Each function will return a string that represents the link it is concerned with building, it doesn’t do anything we weren’t doing before, but instead of building the link in each place it is used, we have one place where the links are built, so that if we need to change it, we easily can. Each function only needs to take a user, and a token, it it then looks up the username and token-value of the objects and we’re pretty much done!</p>

<p>We don’t have too much we need to change here, only three areas or so, let’s start in our <code class="language-plaintext highlighter-rouge">/register</code> controller.</p>

<p>We previously had just a username and token and we used <code class="language-plaintext highlighter-rouge">format</code> to display this in the terminal, however if we want to do things right and send emails, we need to make some adjustments.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">link</span> <span class="p">(</span><span class="nv">build-activation-link</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">subject</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"Ningle Tutorial Project registration for ~A"</span> <span class="nv">user</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">template</span> <span class="s">"ningle-auth/email/register.txt"</span><span class="p">)</span>
       <span class="p">(</span><span class="nv">content</span> <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="nv">template</span> <span class="no">nil</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:link</span> <span class="nv">link</span><span class="p">)))</span>
<span class="p">(</span><span class="nv">ningle-email:send-mail</span> <span class="nv">subject</span> <span class="nv">content</span> <span class="nv">email</span><span class="p">)</span></code></pre></figure>

<p>In addition to the user and token, we need to generate the link we will send using the <code class="language-plaintext highlighter-rouge">build-activation-link</code> function we just wrote above, also, since we know our email needs a subject, we create that now in our <code class="language-plaintext highlighter-rouge">let*</code> block. Next we will have our template, although we haven’t yet created these, we will next, and our email content will use <code class="language-plaintext highlighter-rouge">djula</code> and this template location to render the content and store it ready for us to user in our <code class="language-plaintext highlighter-rouge">send-mail</code> invocation. Since this is happening in our <code class="language-plaintext highlighter-rouge">/register</code> controller, we already have an email address to send to, so we don’t need to create a new variable for that, it is already in scope.</p>

<p>The next place to make a change is in our <code class="language-plaintext highlighter-rouge">/reset</code> controller, there are two areas here where we would change things, thankfully the changes are exactly the same.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">link</span> <span class="p">(</span><span class="nv">build-reset-link</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">subject</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"Ningle Tutorial Project password reset for ~A"</span> <span class="nv">user</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">template</span> <span class="s">"ningle-auth/email/reset.txt"</span><span class="p">)</span>
           <span class="p">(</span><span class="nv">content</span> <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="nv">template</span> <span class="no">nil</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:link</span> <span class="nv">link</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">ningle-email:send-mail</span> <span class="nv">subject</span> <span class="nv">content</span> <span class="nv">email</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))</span></code></pre></figure>

<p>Here, in the case where we have a user and a token object, we perform basically the same set of steps we did before, getting the token, link, subject, template, and content and passing that on into the <code class="language-plaintext highlighter-rouge">send-mail</code> function. It’s worth noting that the template we are loading is different (although, again, we haven’t yet written the templates).</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">user</span>
    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">link</span> <span class="p">(</span><span class="nv">build-reset-link</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">subject</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"Ningle Tutorial Project password reset for ~A"</span> <span class="nv">user</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">template</span> <span class="s">"ningle-auth/email/reset.txt"</span><span class="p">)</span>
           <span class="p">(</span><span class="nv">content</span> <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="nv">template</span> <span class="no">nil</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:link</span> <span class="nv">link</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">ningle-email:send-mail</span> <span class="nv">subject</span> <span class="nv">content</span> <span class="nv">email</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))</span></code></pre></figure>

<p>This code is identical as the above, we can probably consolidate these down in a refactor later, but we will keep focused on getting our email working first.</p>

<p>The final place to change things is in the <code class="language-plaintext highlighter-rouge">/verify</code> controller.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">and</span> <span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
  <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">new-token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">link</span> <span class="p">(</span><span class="nv">build-activation-link</span> <span class="nv">user</span> <span class="nv">new-token</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">subject</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"Ningle Tutorial Project registration for ~A"</span> <span class="nv">user</span><span class="p">))</span>
          <span class="p">(</span><span class="nv">template</span> <span class="s">"ningle-auth/email/register.txt"</span><span class="p">)</span>
          <span class="p">(</span><span class="nv">content</span> <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="nv">template</span> <span class="no">nil</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:link</span> <span class="nv">link</span><span class="p">)))</span>
      <span class="p">(</span><span class="nv">ningle-email:send-mail</span> <span class="nv">subject</span> <span class="nv">content</span> <span class="p">(</span><span class="nv">ningle-auth/models:email</span> <span class="nv">user</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/verify.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Verify"</span> <span class="ss">:token-reissued</span> <span class="no">t</span><span class="p">)))</span></code></pre></figure>

<p>In this case however a <em>new</em> token is being issued, as it has expired at this point in the application lifecycle and needs to be reissued. There’s nothing really new here we haven’t seen before in our previous examples.</p>

<p>The only other thing I have changed is to remove the <code class="language-plaintext highlighter-rouge">format</code> line from inside the <code class="language-plaintext highlighter-rouge">(not token)</code> and <code class="language-plaintext highlighter-rouge">t</code> branches of the <code class="language-plaintext highlighter-rouge">cond</code> here, as they’re no longer needed.</p>

<p>And with those changes, we can move onto our templates!</p>

<h4 id="templates">Templates</h4>

<p>Since we will be sending email, and our controllers specify that we will be rendering templates we need to set these up, as discussed in <a href="https://nmunro.github.io/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a> we looked into how templates override each other, so we need to ensure our email templates are in the correct place to that our main application can override them, if needed.</p>

<p>Remember: These template <em>must</em> be placed in <code class="language-plaintext highlighter-rouge">ningle-auth/src/templates/ningle-auth/email</code> as it’s this directory structure that allows us to override in broader projects!</p>

<h5 id="basehtml">base.html</h5>

<p>Our base.html is going to be really very simple, it provides a content block that other templates can inject content into, but it also serves another purpose, a file we can override in another project and add headers/footers etc without having to override every template.</p>

<p>This is why its content is so small, we’d almost never directly use this, but because it’s a base template that others extend, we can use it!</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% block content %}{% endblock %}</code></pre></figure>

<h5 id="registerhtml">register.html</h5>

<p>Our register template will extend the base and provide the information a user will need to continue setting up their account. The template is simple enough (why complicate it?), but you <em>must</em> pay attention to the <code class="language-plaintext highlighter-rouge">safe</code> filter that is being used to correctly encode the url.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% extends "ningle-auth/email/base.txt" %}
{% block content %}
Hello, {{ user.username }}!

Thanks for registering, for security reasons you must verify your account by clicking on this link:
{{ link|safe }}
This link will expire in 1 hour.

If this was not you, you can ignore this email, as an account will not be activated without clicking on the above link.
{% endblock %}</code></pre></figure>

<h5 id="resethtml">reset.html</h5>

<p>The reset template is very similar to the register template, just with some slightly different wording, but just mind and use the <code class="language-plaintext highlighter-rouge">safe</code> template filter as before!</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% extends "ningle-auth/email/base.txt" %}
{% block content %}
Hello, {{ user.username }}!

We have received a password change request for your account, to do so, click this url:
{{ link|safe }}
This link will expire in 1 hour.

If this was not you, you can ignore this email, as your password will not be changed without clicking on the above link.
{% endblock %}</code></pre></figure>

<p>Now that we have our controllers wired up to send emails that are rendered from templates, we are ready to finally connect everything up!</p>

<h3 id="main-package-part-2">Main Package (Part 2)</h3>

<p>As we mentioned in the previous section, our ningle-auth email base template <em>can</em> be overridde, and in fact that’s <em>exactly</em> what we are going to do. We need to create the following file in our <code class="language-plaintext highlighter-rouge">ningle-tutorial-project</code> project: <code class="language-plaintext highlighter-rouge">src/templates/ningle-auth/email/base.txt</code> and we are going to add a footer!</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% block content %}{% endblock %}

Ningle Tutorial Project</code></pre></figure>

<p>It’s not a lot of code, and to be fair, that was the point, we can quickly and easily override the <code class="language-plaintext highlighter-rouge">ningle-auth</code> base template and add in a footer (or a header, or both, if you like), into the email base template and everything just works as we need it to.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Mercifully this tutorial is a lot shorter than the last time, and good news! This means we now have everything we need to begin working on a microblog! Authentication and email are very important, but they highlight a trade off in micro frameworks and macro frameworks, in micro frameworks we have to do a lot of the work either connecting up third party packages, or writing our own, but we are done now, and we can focus on what we set out to do.</p>

<p>We will begin next time by looking at users, and how to display information about their followers etc.</p>

<p>Thank you for following this tutorial series, I hope you are finding it as interesting/helpful to read as I am finding it interesting/helpful to write.</p>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Learning Outcome</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Remember</strong></td>
      <td>Identify the configuration options required for setting up different email backends (<code class="language-plaintext highlighter-rouge">dummy</code>, <code class="language-plaintext highlighter-rouge">smtp</code>, <code class="language-plaintext highlighter-rouge">sendgrid</code>) in a Ningle application. <br /> Recall the purpose of the <code class="language-plaintext highlighter-rouge">.env</code> file and its role in storing sensitive credentials.</td>
    </tr>
    <tr>
      <td><strong>Understand</strong></td>
      <td>Explain the difference between dummy, SMTP, and SendGrid email backends and when each might be used. <br /> Describe how template overrides in <code class="language-plaintext highlighter-rouge">ningle-auth</code> allow flexibility for customizing email content.</td>
    </tr>
    <tr>
      <td><strong>Apply</strong></td>
      <td>Configure a Ningle project to use different email backends by modifying <code class="language-plaintext highlighter-rouge">defconfig</code> settings. <br /> Use Djula templates to generate dynamic email content (e.g., activation and reset links).</td>
    </tr>
    <tr>
      <td><strong>Analyze</strong></td>
      <td>Compare the advantages and trade-offs of using a microframework (Ningle) versus a macro framework for handling email workflows. <br /> Examine how token expiration settings affect authentication workflows and security.</td>
    </tr>
    <tr>
      <td><strong>Evaluate</strong></td>
      <td>Assess the security implications of storing and handling email credentials in <code class="language-plaintext highlighter-rouge">.env</code> files. <br /> Justify the choice of email backend for different project stages (development, testing, production).</td>
    </tr>
    <tr>
      <td><strong>Create</strong></td>
      <td>Design and implement a custom email notification (e.g., welcome email, alert system) using the <code class="language-plaintext highlighter-rouge">ningle-email</code> package. <br /> Extend the project by building reusable utility functions to streamline email workflows beyond registration and password resets.</td>
    </tr>
  </tbody>
</table>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-10">here</a>.</li>
  <li>The link for the auth app code is available <a href="https://github.com/nmunro/ningle-auth">here</a>.</li>
  <li>The link for the email app code is available <a href="https://github.com/nmunro/ningle-email">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://gitlab.common-lisp.net/cl-smtp/cl-smtp">cl-smtp</a></li>
  <li><a href="https://github.com/vindarel/cl-sendgrid">cl-sendgrid</a></li>
</ul>

<h3 id="common-lisp-hyperspec">Common Lisp HyperSpec</h3>

<ul>
  <li><a href="http://clhs.lisp.se/Body/m_and.htm">and</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_case_.htm">case</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_cond.htm">cond</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defmet.htm">defmethod</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defpkg.htm">defpackage</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defun.htm">defun</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_format.htm">format</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_get_un.htm">get-universal-time</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_in_pkg.htm">in-package</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_init_i.htm">initialize-instance</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_let_l.htm">let / let*</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_member.htm">member</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_not.htm">not</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_or.htm">or</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_progn.htm">progn</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_setf_.htm">setf</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_slt_bo.htm">slot-boundp</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_stgeq.htm">string=</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_trunca.htm">truncate</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_unless.htm">unless</a></li>
</ul>

<h3 id="reader-macros">Reader Macros</h3>

<ul>
  <li><a href="http://clhs.lisp.se/Body/02_dhq.htm">backquote (`)</a></li>
  <li><a href="http://clhs.lisp.se/Body/02_dhq.htm">comma (,) and ,@ in backquote</a></li>
  <li><a href="http://clhs.lisp.se/Body/02_dhe.htm">pathname literal (#p)</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_quote.htm">quote (‘)</a></li>
</ul>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 9: Authentication System</title><link href="nmunro.github.io/2025/07/31/ningle-9.html" rel="alternate" type="text/html" title="Ningle Tutorial 9: Authentication System" /><published>2025-07-31T11:30:00+00:00</published><updated>2025-07-31T11:30:00+00:00</updated><id>nmunro.github.io/2025/07/31/ningle-9</id><content type="html" xml:base="nmunro.github.io/2025/07/31/ningle-9.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li>Part 9 (Authentication System)</li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back to this tutorial series, in this chapter we are going to build an authentication system and I ain’t gonna lie to you, it’s something of a monster of a chapter, we will be extending our settings, writting middleware code and injecting settings into apps at the point they are mounted, so buckle up, it’ll be a wild ride.</p>

<hr />
<blockquote>
  <p><strong>NOTE:</strong><br />
I have published an updated
<a href="https://github.com/nmunro/envy-ningle/releases/tag/v0.0.2">envy-ningle</a>.
Please ensure you have updated to this version before continuing with
this tutorial. The recommended version of <code class="language-plaintext highlighter-rouge">envy-ningle</code> for this lesson
is <code class="language-plaintext highlighter-rouge">v0.0.2</code>.</p>
</blockquote>

<hr />

<h3 id="practical-outcomes">Practical Outcomes</h3>

<p>We will be developing an authentication app that:</p>

<h4 id="allows-users-to-register">Allows users to register</h4>

<p>This will render a form that uses csrf protection, when a user fills in the form if the username or email address they have entered is already in use by another user, an error will be signalled, if they have entered two different passwords into the password and password-verify another error will be signalled. Assuming no errors are signalled, a user and token object will be created, a unique url based on the username and token will be displayed to the terminal (later to be send via email), and the browser is redirected to another route. It is important to note that tokens will only be valid for one hour.</p>

<h4 id="verify-new-accounts-prior-to-logging-in">Verify new accounts prior to logging in</h4>

<p>This is the second step in the user registration process, for the moment we will use the url printed in the terminal from the previous step (but remember this will be emailed to users later), when the url is requested, if there is a user that is already logged in, they will be redirected away from the url. If there is a matching token but it is expired, a new token will be issued (deleting the old one in the process), as before, a new url will be printed to the terminal. If there is no token, an error page will be displayed. Finally, if a token exists, it is valid, and there’s no logged in user, we can proceed with activating the user. This will delete the token, set up permissions for the user, activate and save the user and redirect the browser to the login route.</p>

<h4 id="allows-user-login-with-restricted-views">Allows user login with restricted views</h4>

<p>This will render a form to users to log in with as with our register form it will be protected with a csrf token, if a user is already logged in, it will redirect them away from this route, if there is a csrf token error this will be signalled, likewise errors will be signalled for users that don’t exist (or have not yet been activated via the verification process described above), or the password is invalid for the given user. If however there are no errors the user is logged in and redirected to a new url. As part of this, a route <code class="language-plaintext highlighter-rouge">/profile</code> will be set up that will only be accessible to users that are logged in.</p>

<h4 id="allow-users-to-request-a-secure-password-reset">Allow users to request a secure password reset</h4>

<p>Users forget their password, it happens, we need to facilitate a way to reset their password. This will be a two step process, as always we will have our form contain a csrf token, so it might be that this controller signals an error, but assuming this hasn’t happened. If there’s a user, and a token, but the token hasn’t expired, this suggests that a previous attempt was made, so an error should be sent back informing the user that they must either complete the reset, or wait for the token to expire.</p>

<p>If there is a user and a token that has expired, the old token will be deleted and a new one issued, the new url will then be displayed on the terminal (as always with these links they will be emailed in the future) and the browser will redirect.</p>

<p>If there is only a user and no token, this means that the reset process is being started for the first time and a token will be issued, the url printed to the terminal and the browser redirected.</p>

<p>Finally if there is no user found, an error will be displayed in the controller.</p>

<h4 id="allow-users-to-reset-password">Allow users to reset password</h4>

<p>Once the request to reset the password has been processed, the password should be reset, this controller will render the password reset form, if the user is logged in the browser should be redirected away from this url.</p>

<p>If there is no reset token, or it has expired an error should be rendered in the browser.</p>

<p>If there is a valid reset token, the form can be rendered to accept a new password, upon form submisison, as with all forms a csrf token protects the form and this can be signalled, likewise if two different passwords are entered, this will signal an error.</p>

<p>When the user, token, and passwords match the new user password is set and the user object is saved, the token is deleted and the browser is redirected, however if, for some reason, the user isn’t valid, an error will be displayed in the browser.</p>

<h4 id="allow-users-to-logout">Allow users to logout</h4>

<p>This will clear the active user from the session and redirect to the login page.</p>

<h2 id="building-the-authentication-app">Building the Authentication App</h2>

<h3 id="initial-clean-up">Initial Clean Up</h3>

<p>Before we begin in earnest we should remove a route setup in the last chapter that ultimately doesn’t belong in authentication, it more accurately belongs in user management, which we will explore in a futute chapter.</p>

<p>Find the controller for deleting users and delete it:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp">    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/delete"</span><span class="p">)</span>
        <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"auth/delete.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Delete"</span><span class="p">)))</span></code></pre></figure>

<p>Also find and remove the following templates:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/templates/ningle-auth/delete.html</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/templates/ningle-auth/logout.html</code></li>
</ul>

<p>It was anticipated that that these may have been needed, but in the process of developing the solution, they weren’t actually needed.</p>

<h3 id="forms">Forms</h3>

<p>The easiest place to start is with our forms, our forms control what data we want to send back and forth and how to validate it, so these offer a good high level view at what we will be doing. We previously wrote a form in the <code class="language-plaintext highlighter-rouge">ningle-tutorial-app</code> for registering users, we will move that form from the tutorial app and into the authentication app (ningle-auth) we created <a href="/2025/06/29/ningle-8.html">last time</a> and we will create a few other forms too. As before, we used the <code class="language-plaintext highlighter-rouge">cl-forms</code> package, and so these forms should be familiar from <a href="/2025/02/28/ningle-4.html">Part 4</a>, but specifically we have the following four forms:</p>

<h4 id="register">register</h4>

<p>Our register form concerns itself with allowing users to sign up to our application, it has the following fields:</p>

<ol>
  <li>Username used to log in (we could have used emails, but I wanted to demonstrate a few things)</li>
  <li>An email address (we will use this in a later tutorial to email information we produce during this tutorial)</li>
  <li>A password field</li>
  <li>A confirm password field (to help ensure the password typed was free of typos)</li>
  <li>A submit button</li>
</ol>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">register</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"register"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
    <span class="p">((</span><span class="nv">email</span>           <span class="ss">:email</span>    <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:valid-email</span><span class="p">)))</span>
     <span class="p">(</span><span class="nv">username</span>        <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*username-validator*</span><span class="p">)</span>
     <span class="p">(</span><span class="nv">password</span>        <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
     <span class="p">(</span><span class="nv">password-verify</span> <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
     <span class="p">(</span><span class="nv">submit</span>          <span class="ss">:submit</span>   <span class="ss">:label</span> <span class="s">"Register"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The fields have constraints on them as one might expect, as we do want to validate our forms! When this form is rendered a <code class="language-plaintext highlighter-rouge">GET</code> request will display this form and a <code class="language-plaintext highlighter-rouge">POST</code> request will process the data the form submitted.</p>

<h4 id="login">login</h4>

<p>Our login form concerns itself with allowing registered users to log into our application, this is as simple as a username and a password, we do not necessarily need to validate these they will only be comparing objects in the database not creating new objects.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">login</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"login"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
    <span class="p">((</span><span class="nv">username</span> <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">password</span> <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">submit</span>   <span class="ss">:submit</span>   <span class="ss">:value</span> <span class="s">"Login"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="reset-password">reset-password</h4>

<p>Our reset-password form concerns itself with allowing registered users to begin the process of securely changing their password if they cannot login. We do not want just anyone to be able to reset a users password, so we will need a form that will take an email address and send a link the user can follow to <em>actually</em> change the password.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">reset-password</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"password-reset"</span> <span class="ss">:csrf-protection</span> <span class="mi">5</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
    <span class="p">((</span><span class="nv">email</span>  <span class="ss">:string</span> <span class="ss">:value</span> <span class="s">""</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">submit</span> <span class="ss">:submit</span> <span class="ss">:value</span> <span class="s">"Reset"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="new-password">new-password</h4>

<p>Our new-password form concerns itself with completing the process of securely changing the password of registered users that have begun the process if they cannot login. It is assumed that this form is served by a url that the user has received via email and requires matching usernames and secure tokens that an attacker couldn’t guess, also these tokens expire within 1 hour and are deleted after a single use, so cannot be reused and its unlikely they could be cracked within the 1 hour window in which they are valid.</p>

<p>It is important to note that the <code class="language-plaintext highlighter-rouge">email</code>, and <code class="language-plaintext highlighter-rouge">token</code> fields will be of the type <code class="language-plaintext highlighter-rouge">hidden</code>, we don’t want the user to fill these in directly, but we certainly want to validate them along with all the other items in the form. When the form is initially rendered, these will need to be populated by us.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">new-password</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"new-password"</span> <span class="ss">:csrf-protection</span> <span class="mi">5</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
    <span class="p">((</span><span class="nv">email</span>           <span class="ss">:hidden</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:valid-email</span><span class="p">)))</span>
     <span class="p">(</span><span class="nv">token</span>           <span class="ss">:hidden</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*token-validator*</span><span class="p">)</span>
     <span class="p">(</span><span class="nv">password</span>        <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
     <span class="p">(</span><span class="nv">password-verify</span> <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
     <span class="p">(</span><span class="nv">submit</span>          <span class="ss">:submit</span>   <span class="ss">:value</span> <span class="s">"Reset"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="full-listing">Full Listing</h4>

<p>In the ningle-auth application create <code class="language-plaintext highlighter-rouge">src/forms.lisp</code>:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth/forms</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:register</span>
           <span class="ss">#:login</span>
           <span class="ss">#:reset-password</span>
           <span class="ss">#:new-password</span>
           <span class="ss">#:email</span>
           <span class="ss">#:username</span>
           <span class="ss">#:token</span>
           <span class="ss">#:password</span>
           <span class="ss">#:password-verify</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth/forms</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*username-validator*</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:not-blank</span><span class="p">)</span>
                                         <span class="p">(</span><span class="nv">clavier:is-a-string</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*password-validator*</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:not-blank</span><span class="p">)</span>
                                         <span class="p">(</span><span class="nv">clavier:is-a-string</span><span class="p">)</span>
                                         <span class="p">(</span><span class="nv">clavier:len</span> <span class="ss">:min</span> <span class="mi">8</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*token-validator*</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:not-blank</span><span class="p">)</span>
                                      <span class="p">(</span><span class="nv">clavier:is-a-string</span><span class="p">)</span>
                                      <span class="p">(</span><span class="nv">clavier:len</span> <span class="ss">:min</span> <span class="mi">64</span> <span class="ss">:max</span> <span class="mi">64</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">register</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"register"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">email</span>           <span class="ss">:email</span>    <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:valid-email</span><span class="p">)))</span>
   <span class="p">(</span><span class="nv">username</span>        <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*username-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">password</span>        <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">password-verify</span> <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span>          <span class="ss">:submit</span>   <span class="ss">:label</span> <span class="s">"Register"</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">login</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"login"</span> <span class="ss">:csrf-protection</span> <span class="no">t</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">username</span> <span class="ss">:string</span>   <span class="ss">:value</span> <span class="s">""</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">password</span> <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span>   <span class="ss">:submit</span>   <span class="ss">:value</span> <span class="s">"Login"</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">reset-password</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"password-reset"</span> <span class="ss">:csrf-protection</span> <span class="mi">5</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">email</span>  <span class="ss">:string</span> <span class="ss">:value</span> <span class="s">""</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span> <span class="ss">:submit</span> <span class="ss">:value</span> <span class="s">"Reset"</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">cl-forms:defform</span> <span class="nv">new-password</span> <span class="p">(</span><span class="ss">:id</span> <span class="s">"new-password"</span> <span class="ss">:csrf-protection</span> <span class="mi">5</span> <span class="ss">:csrf-field-name</span> <span class="s">"csrftoken"</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">email</span>           <span class="ss">:hidden</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nv">clavier:valid-email</span><span class="p">)))</span>
   <span class="p">(</span><span class="nv">token</span>           <span class="ss">:hidden</span>   <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*token-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">password</span>        <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">password-verify</span> <span class="ss">:password</span> <span class="ss">:value</span> <span class="s">""</span> <span class="ss">:constraints</span> <span class="vg">*password-validator*</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">submit</span>          <span class="ss">:submit</span>   <span class="ss">:value</span> <span class="s">"Reset"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="models">Models</h3>

<p>With our forms defined, we can go back and write our models, we will look at each model in isolation, any methods, and then see the complete listing, so we can then see what we need to export after having looked at the basic functionality.</p>

<h4 id="user-model">User Model</h4>

<p>Our user model will use the <code class="language-plaintext highlighter-rouge">mito-auth</code> mixin to provide an interface with which we can use <a href="https://www.okta.com/blog/2019/03/what-are-salted-passwords-and-password-hashing/">hashed and salted passwords</a>, we will have a text column <code class="language-plaintext highlighter-rouge">(:varchar 255)</code> for our email and username fields, and an integer field that will represent if the user is “active” or not (if they have completed the registration steps). Since we are using the <code class="language-plaintext highlighter-rouge">mito-auth</code> mixin we have a number of fields hidden here and the details aren’t too important except to know that there’s a <code class="language-plaintext highlighter-rouge">password-hash</code> that will contain the salted and hashed password, <code class="language-plaintext highlighter-rouge">mito-auth</code> does the heavy lifting for us here.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">deftable</span> <span class="nv">user</span> <span class="p">(</span><span class="nv">mito-auth:has-secure-password</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">email</span>    <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span> <span class="ss">:initarg</span>  <span class="ss">:email</span>    <span class="ss">:accessor</span> <span class="nv">email</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">username</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span> <span class="ss">:initarg</span>  <span class="ss">:username</span> <span class="ss">:accessor</span> <span class="nv">username</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">active</span>   <span class="ss">:col-type</span> <span class="ss">:integer</span>       <span class="ss">:initform</span> <span class="mi">0</span>         <span class="ss">:accessor</span> <span class="nv">active</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="nv">email</span> <span class="nv">username</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>From the last line, we can see that both email and username should be unique.</p>

<h4 id="role-model">Role Model</h4>

<p>The role model is quite simple and concerns itself with, as its name might suggest, roles, these are simply names and descriptions. When we come to writing our migrations, we will create <code class="language-plaintext highlighter-rouge">admin</code> and <code class="language-plaintext highlighter-rouge">user</code> roles and their permissions.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">deftable</span> <span class="nv">role</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">name</span>        <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span>  <span class="ss">:initarg</span> <span class="ss">:name</span>        <span class="ss">:accessor</span> <span class="nv">name</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">description</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">2048</span><span class="p">)</span> <span class="ss">:initarg</span> <span class="ss">:description</span> <span class="ss">:accessor</span> <span class="nv">description</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="nv">name</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We make the name unique here as we really don’t want two roles with the same name.</p>

<h4 id="permission-model">Permission Model</h4>

<p>In order to grant user roles, we need a permission model, this will link a user to a role. As we build the application having a permission table allows us to grant or revoke permissions easily.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">deftable</span> <span class="nv">permission</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span> <span class="ss">:col-type</span> <span class="nv">user</span> <span class="ss">:references</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">id</span><span class="p">))</span>
   <span class="p">(</span><span class="nv">role</span> <span class="ss">:col-type</span> <span class="nv">role</span> <span class="ss">:references</span> <span class="p">(</span><span class="nv">role</span> <span class="nv">id</span><span class="p">)))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">role</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Where we previously defined unique fields, here we define a unique <code class="language-plaintext highlighter-rouge">constraint</code> where the same value can repeat in this table multiple times, and the same role can appear in this table multiple times, but the same role with the same user cannot appear more than once. In effect a user can only ever be assigned a given role once.</p>

<h4 id="token-model">Token Model</h4>

<p>Our token model will concern itself with various tokens, in our authentication system there is only two an <code class="language-plaintext highlighter-rouge">email-verification</code> token and a <code class="language-plaintext highlighter-rouge">password-reset</code> token.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">deftable</span> <span class="nv">token</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span>       <span class="ss">:col-type</span> <span class="nv">user</span>          <span class="ss">:references</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">id</span><span class="p">))</span>
   <span class="p">(</span><span class="nv">purpose</span>    <span class="ss">:col-type</span> <span class="ss">:string</span>       <span class="ss">:initarg</span> <span class="ss">:purpose</span>    <span class="ss">:accessor</span> <span class="nv">token-purpose</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">token</span>      <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">64</span><span class="p">)</span> <span class="ss">:initarg</span> <span class="ss">:token</span>      <span class="ss">:accessor</span> <span class="nv">token-value</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">salt</span>       <span class="ss">:col-type</span> <span class="ss">:binary</span>       <span class="ss">:accessor</span> <span class="nv">token-salt</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">expires-at</span> <span class="ss">:col-type</span> <span class="ss">:timestamp</span>    <span class="ss">:accessor</span> <span class="nv">token-expires-at</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user-id</span> <span class="nv">purpose</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>As in our permission model, we have a <code class="language-plaintext highlighter-rouge">constraint</code> where a user can only ever have one type of token, there’s something to note, that while our field is called <code class="language-plaintext highlighter-rouge">user</code> and we can use that in code, the actual name in the database is <code class="language-plaintext highlighter-rouge">user_id</code>. Just like our user model, we will use salts and hashes to create unique and secure tokens.</p>

<h4 id="token-methods">Token Methods</h4>

<p>While not all of our models require methods, some do, staring with our token model we have to check if a token has expired, so we will write a method that simply returns <code class="language-plaintext highlighter-rouge">t</code> or <code class="language-plaintext highlighter-rouge">nil</code> depending on if the token has indeed expired, or not.</p>

<p>The type of the expiration date may change depending on when it is serialized, so we use a <code class="language-plaintext highlighter-rouge">typecase</code> here to handle the different types it may be.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">is-expired-p</span> <span class="p">(</span><span class="nv">token</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Determines if a token has expired"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">is-expired-p</span> <span class="p">((</span><span class="nv">token</span> <span class="nv">token</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">expiry</span> <span class="p">(</span><span class="nv">token-expires-at</span> <span class="nv">token</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">typecase</span> <span class="nv">expiry</span>
      <span class="p">(</span><span class="nv">local-time:timestamp</span>
       <span class="p">(</span><span class="nb">&gt;</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="p">(</span><span class="nv">local-time:timestamp-to-universal</span> <span class="nv">expiry</span><span class="p">)))</span>

      <span class="p">(</span><span class="nc">integer</span>
       <span class="p">(</span><span class="nb">&gt;</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="nv">expiry</span><span class="p">))</span>

      <span class="p">(</span><span class="no">t</span>
       <span class="p">(</span><span class="nb">error</span> <span class="s">"Unknown type for token-expires-at: ~S"</span> <span class="p">(</span><span class="nb">type-of</span> <span class="nv">expiry</span><span class="p">))))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Since we have specific token types, we want to ensure that invalid values cannot be passed into the objects, so here we write our own implementations of the <code class="language-plaintext highlighter-rouge">initialize-instance</code> <code class="language-plaintext highlighter-rouge">method</code> using <code class="language-plaintext highlighter-rouge">:before</code> and <code class="language-plaintext highlighter-rouge">:after</code> to ensure that if an invalid token type is passed in we signal an error, but also, if no salt or expires-at value was provided, a default is created, for security.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defmethod</span> <span class="nb">initialize-instance</span> <span class="ss">:before</span> <span class="p">((</span><span class="nv">token</span> <span class="nv">token</span><span class="p">)</span> <span class="k">&amp;rest</span> <span class="nv">initargs</span> <span class="k">&amp;key</span> <span class="nv">purpose</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">member</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">error</span> <span class="s">"Invalid token purpose: ~A. Allowed: ~A"</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nb">initialize-instance</span> <span class="ss">:after</span> <span class="p">((</span><span class="nv">token</span> <span class="nv">token</span><span class="p">)</span> <span class="k">&amp;rest</span> <span class="nv">initargs</span> <span class="k">&amp;key</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">slot-boundp</span> <span class="nv">token</span> <span class="ss">'salt</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">token-salt</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nv">ironclad:make-random-salt</span> <span class="mi">16</span><span class="p">)))</span>

  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">slot-boundp</span> <span class="nv">token</span> <span class="ss">'expires-at</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">token-expires-at</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="mi">3600</span><span class="p">))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="user-methods">User Methods</h4>

<p>Finally the methods for our user object, we will start by defining a method to activate our user object (which will be used when a user completes the account verification step), all this does is set the active slot on the user object to <code class="language-plaintext highlighter-rouge">1</code>, please note that due to <a href="https://en.wikipedia.org/wiki/Separation_of_concerns">separation of concerns</a> and the <a href="https://en.wikipedia.org/wiki/Principle_of_least_astonishment">principle of the least surprise</a> setting the active flat does <em>not</em> save the user object.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">activate</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Set the active slot of a user to 1"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">activate</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
  <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">active</span> <span class="nv">user</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>As we have mentioned, we must create tokens, and tokens are linked to a user, so it makes sense to have a method that dispatches on a user model for creating a token, calling <code class="language-plaintext highlighter-rouge">generate-token</code> with a user and a valid token type will create and return the token.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">generate-token</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">purpose</span> <span class="k">&amp;key</span> <span class="nv">expires-in</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Generates a token for a user"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">generate-token</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">purpose</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">expires-in</span> <span class="mi">3600</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">member</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">error</span> <span class="s">"Invalid token purpose: ~A. Allowed: ~A"</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span><span class="p">))</span>

    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">salt</span> <span class="p">(</span><span class="nv">ironclad:make-random-salt</span> <span class="mi">16</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">expires-at</span> <span class="p">(</span><span class="nb">truncate</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="nv">expires-in</span><span class="p">)))</span>
           <span class="p">(</span><span class="kt">base-string</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A~A~A"</span> <span class="p">(</span><span class="nv">username</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">expires-at</span> <span class="nv">salt</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">hash</span> <span class="p">(</span><span class="nv">ironclad:byte-array-to-hex-string</span> <span class="p">(</span><span class="nv">ironclad:digest-sequence</span> <span class="ss">:sha256</span> <span class="p">(</span><span class="nv">babel:string-to-octets</span> <span class="kt">base-string</span><span class="p">)))))</span>
        <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">purpose</span> <span class="ss">:token</span> <span class="nv">hash</span> <span class="ss">:salt</span> <span class="nv">salt</span> <span class="ss">:expires-at</span> <span class="nv">expires-at</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="token-types">Token Types</h4>

<p>We have discussed the two token types, they’re simple strings, but we define them in our package and include them in a list so that if we add more it’s easy to check membership of <code class="language-plaintext highlighter-rouge">+token-purposes+</code>.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defparameter</span> <span class="nv">+email-verification+</span> <span class="s">"email-verification"</span><span class="p">)</span>
<span class="p">(</span><span class="nb">defparameter</span> <span class="nv">+password-reset+</span> <span class="s">"password-reset"</span><span class="p">)</span>
<span class="p">(</span><span class="nb">defparameter</span> <span class="nv">+token-purposes+</span> <span class="p">(</span><span class="nb">list</span> <span class="nv">+email-verification+</span> <span class="nv">+password-reset+</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="package-structure">Package Structure</h4>

<p>Unusually, we are looking at the package structure and exports now at the end, but we didn’t know what would be exported until we wrote it!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:mito-auth</span>
                <span class="ss">:password-hash</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:user</span>
           <span class="ss">#:id</span>
           <span class="ss">#:created-at</span>
           <span class="ss">#:updated-at</span>
           <span class="ss">#:email</span>
           <span class="ss">#:username</span>
           <span class="ss">#:password-hash</span>
           <span class="ss">#:role</span>
           <span class="ss">#:permission</span>
           <span class="ss">#:token</span>
           <span class="ss">#:token-value</span>
           <span class="ss">#:generate-token</span>
           <span class="ss">#:is-expired-p</span>
           <span class="ss">#:activate</span>
           <span class="ss">#:+email-verification+</span>
           <span class="ss">#:+password-reset+</span>
           <span class="ss">#:+token-purposes+</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth/models</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="full-listing-1">Full Listing</h4>

<p>In the ningle-auth application create <code class="language-plaintext highlighter-rouge">src/models.lisp</code>:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span> <span class="ss">:mito-auth</span>
                <span class="ss">:password-hash</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:user</span>
           <span class="ss">#:id</span>
           <span class="ss">#:created-at</span>
           <span class="ss">#:updated-at</span>
           <span class="ss">#:email</span>
           <span class="ss">#:username</span>
           <span class="ss">#:password-hash</span>
           <span class="ss">#:role</span>
           <span class="ss">#:permission</span>
           <span class="ss">#:token</span>
           <span class="ss">#:token-value</span>
           <span class="ss">#:generate-token</span>
           <span class="ss">#:is-expired-p</span>
           <span class="ss">#:activate</span>
           <span class="ss">#:+email-verification+</span>
           <span class="ss">#:+password-reset+</span>
           <span class="ss">#:+token-purposes+</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth/models</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="nv">+email-verification+</span> <span class="s">"email-verification"</span><span class="p">)</span>
<span class="p">(</span><span class="nb">defparameter</span> <span class="nv">+password-reset+</span> <span class="s">"password-reset"</span><span class="p">)</span>
<span class="p">(</span><span class="nb">defparameter</span> <span class="nv">+token-purposes+</span> <span class="p">(</span><span class="nb">list</span> <span class="nv">+email-verification+</span> <span class="nv">+password-reset+</span><span class="p">))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">user</span> <span class="p">(</span><span class="nv">mito-auth:has-secure-password</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">email</span>    <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span> <span class="ss">:initarg</span>  <span class="ss">:email</span>    <span class="ss">:accessor</span> <span class="nv">email</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">username</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span> <span class="ss">:initarg</span>  <span class="ss">:username</span> <span class="ss">:accessor</span> <span class="nv">username</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">active</span>   <span class="ss">:col-type</span> <span class="ss">:integer</span>       <span class="ss">:initform</span> <span class="mi">0</span>         <span class="ss">:accessor</span> <span class="nv">active</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="nv">email</span> <span class="nv">username</span><span class="p">))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">role</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">name</span>        <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span>  <span class="ss">:initarg</span> <span class="ss">:name</span>        <span class="ss">:accessor</span> <span class="nv">name</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">description</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">2048</span><span class="p">)</span> <span class="ss">:initarg</span> <span class="ss">:description</span> <span class="ss">:accessor</span> <span class="nv">description</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="nv">name</span><span class="p">))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">permission</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span> <span class="ss">:col-type</span> <span class="nv">user</span> <span class="ss">:references</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">id</span><span class="p">))</span>
   <span class="p">(</span><span class="nv">role</span> <span class="ss">:col-type</span> <span class="nv">role</span> <span class="ss">:references</span> <span class="p">(</span><span class="nv">role</span> <span class="nv">id</span><span class="p">)))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">role</span><span class="p">)))</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">token</span> <span class="p">()</span>
  <span class="p">((</span><span class="nv">user</span>       <span class="ss">:col-type</span> <span class="nv">user</span>          <span class="ss">:references</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">id</span><span class="p">))</span>
   <span class="p">(</span><span class="nv">purpose</span>    <span class="ss">:col-type</span> <span class="ss">:string</span>       <span class="ss">:initarg</span> <span class="ss">:purpose</span>    <span class="ss">:accessor</span> <span class="nv">token-purpose</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">token</span>      <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">64</span><span class="p">)</span> <span class="ss">:initarg</span> <span class="ss">:token</span>      <span class="ss">:accessor</span> <span class="nv">token-value</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">salt</span>       <span class="ss">:col-type</span> <span class="ss">:binary</span>       <span class="ss">:accessor</span> <span class="nv">token-salt</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">expires-at</span> <span class="ss">:col-type</span> <span class="ss">:timestamp</span>    <span class="ss">:accessor</span> <span class="nv">token-expires-at</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="p">(</span><span class="nv">user-id</span> <span class="nv">purpose</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">activate</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Set the active slot of a user to 1"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">activate</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">))</span>
  <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">active</span> <span class="nv">user</span><span class="p">)</span> <span class="mi">1</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">is-expired-p</span> <span class="p">(</span><span class="nv">token</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Determines if a token has expired"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">is-expired-p</span> <span class="p">((</span><span class="nv">token</span> <span class="nv">token</span><span class="p">))</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">expiry</span> <span class="p">(</span><span class="nv">token-expires-at</span> <span class="nv">token</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">typecase</span> <span class="nv">expiry</span>
      <span class="p">(</span><span class="nv">local-time:timestamp</span>
       <span class="p">(</span><span class="nb">&gt;</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="p">(</span><span class="nv">local-time:timestamp-to-universal</span> <span class="nv">expiry</span><span class="p">)))</span>

      <span class="p">(</span><span class="nc">integer</span>
       <span class="p">(</span><span class="nb">&gt;</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="nv">expiry</span><span class="p">))</span>

      <span class="p">(</span><span class="no">t</span>
       <span class="p">(</span><span class="nb">error</span> <span class="s">"Unknown type for token-expires-at: ~S"</span> <span class="p">(</span><span class="nb">type-of</span> <span class="nv">expiry</span><span class="p">))))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nb">initialize-instance</span> <span class="ss">:before</span> <span class="p">((</span><span class="nv">tok</span> <span class="nv">token</span><span class="p">)</span> <span class="k">&amp;rest</span> <span class="nv">initargs</span> <span class="k">&amp;key</span> <span class="nv">purpose</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">member</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">error</span> <span class="s">"Invalid token purpose: ~A. Allowed: ~A"</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nb">initialize-instance</span> <span class="ss">:after</span> <span class="p">((</span><span class="nv">token</span> <span class="nv">token</span><span class="p">)</span> <span class="k">&amp;rest</span> <span class="nv">initargs</span> <span class="k">&amp;key</span> <span class="k">&amp;allow-other-keys</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">slot-boundp</span> <span class="nv">token</span> <span class="ss">'salt</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">token-salt</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nv">ironclad:make-random-salt</span> <span class="mi">16</span><span class="p">)))</span>

  <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">slot-boundp</span> <span class="nv">token</span> <span class="ss">'expires-at</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">token-expires-at</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="mi">3600</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">defgeneric</span> <span class="nv">generate-token</span> <span class="p">(</span><span class="nv">user</span> <span class="nv">purpose</span> <span class="k">&amp;key</span> <span class="nv">expires-in</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:documentation</span> <span class="s">"Generates a token for a user"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">generate-token</span> <span class="p">((</span><span class="nv">user</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">purpose</span> <span class="k">&amp;key</span> <span class="p">(</span><span class="nv">expires-in</span> <span class="mi">3600</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">member</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)</span>
      <span class="p">(</span><span class="nb">error</span> <span class="s">"Invalid token purpose: ~A. Allowed: ~A"</span> <span class="nv">purpose</span> <span class="nv">+token-purposes+</span><span class="p">))</span>

    <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">salt</span> <span class="p">(</span><span class="nv">ironclad:make-random-salt</span> <span class="mi">16</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">expires-at</span> <span class="p">(</span><span class="nb">truncate</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">get-universal-time</span><span class="p">)</span> <span class="nv">expires-in</span><span class="p">)))</span>
           <span class="p">(</span><span class="kt">base-string</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A~A~A"</span> <span class="p">(</span><span class="nv">username</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">expires-at</span> <span class="nv">salt</span><span class="p">))</span>
           <span class="p">(</span><span class="nv">hash</span> <span class="p">(</span><span class="nv">ironclad:byte-array-to-hex-string</span> <span class="p">(</span><span class="nv">ironclad:digest-sequence</span> <span class="ss">:sha256</span> <span class="p">(</span><span class="nv">babel:string-to-octets</span> <span class="kt">base-string</span><span class="p">)))))</span>
        <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">purpose</span> <span class="ss">:token</span> <span class="nv">hash</span> <span class="ss">:salt</span> <span class="nv">salt</span> <span class="ss">:expires-at</span> <span class="nv">expires-at</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="migrations">Migrations</h3>

<p>We know from a previous tutorial that when we are setting up and application of have changed the structures of the models we need to migrate them, we have seen that <code class="language-plaintext highlighter-rouge">mito</code> has the <code class="language-plaintext highlighter-rouge">ensure-table-exists</code> and <code class="language-plaintext highlighter-rouge">migrate-table</code> functions, so we must write a migration file.</p>

<h4 id="creating-tables">Creating tables</h4>

<p>As a reminder on how to create the tables for our four models.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
<span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:role</span><span class="p">)</span>
<span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:permission</span><span class="p">)</span>
<span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:token</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="migrating-tables">Migrating tables</h4>

<p>Migrating an existing table is similarly easy.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
<span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:role</span><span class="p">)</span>
<span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:permission</span><span class="p">)</span>
<span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:token</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="initial-object-creation">Initial object creation</h4>

<p>If we have some objects we want to create as part of our migration, in our case creating “user” and “admin” roles, we might want to write something like the following:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-auth/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:role</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:permission</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:token</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:role</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:permission</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:token</span><span class="p">)</span>

  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">admin-role</span> <span class="p">(</span><span class="nv">find-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"admin"</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">admin-role</span>
      <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"admin"</span> <span class="ss">:description</span> <span class="s">"Admin"</span><span class="p">)))</span>

  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user-role</span> <span class="p">(</span><span class="nv">find-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"user"</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">user-role</span>
      <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"user"</span> <span class="ss">:description</span> <span class="s">"User"</span><span class="p">)))</span>

  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>You might notice at no point we establish a database connection to run this migration, don’t worry, we will come to that a little bit later, this migration function is assumed to be run inside a context where a database has already been established. This will come in handy if we had many applications that needed to be migrated, each migration wont be connecting and disconnecting, there’s one connection established, and all migrations run inside that connection.</p>

<h4 id="full-listing-2">Full Listing</h4>

<p>Create <code class="language-plaintext highlighter-rouge">src/migrations.lisp</code>:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-auth/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:role</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:permission</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-auth/models:token</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:role</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:permission</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-auth/models:token</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"admin"</span> <span class="ss">:description</span> <span class="s">"Admin"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"user"</span> <span class="ss">:description</span> <span class="s">"User"</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="main">Main</h3>

<p>The “main” event, so to speak! Most of our logic will go in here, remember however that our main project will set up the configuration and we will need a way to pass this down into applications it uses. There is a package I created for managing user objects in the http session called <a href="https://github.com/nmunro/cu-sith">cu-sith</a>, we will use that in our application here. We also use <a href="https://github.com/nmunro/envy-ningle">envy-ningle</a> which adds some functions around <code class="language-plaintext highlighter-rouge">envy</code> to help build middleware etc.</p>

<p>So, before we work on the controllers, ensure you have downloaded <code class="language-plaintext highlighter-rouge">cu-sith</code> to your local package registry and once you have, add it to the dependencies in the application <code class="language-plaintext highlighter-rouge">asd</code> file, the full dependencies are shown here:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:cl-dotenv</span>
             <span class="ss">:clack</span>
             <span class="ss">:djula</span>
             <span class="ss">:cl-forms</span>
             <span class="ss">:cl-forms.djula</span>
             <span class="ss">:cl-forms.ningle</span>
             <span class="ss">:envy-ningle</span>
             <span class="ss">:mito</span>
             <span class="ss">:ningle</span>
             <span class="ss">:local-time</span>
             <span class="ss">:cu-sith</span><span class="p">)</span></code></pre></figure>

<p>Once you have your dependencies in place, we can look at what we will initially change from last time. We have already spoken about removing the <code class="language-plaintext highlighter-rouge">delete</code> controller, which leaves us with six controllers to write.</p>

<h4 id="initial-setup">Initial Setup</h4>

<p>We began our authentication application last time with this beginning:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:*app*</span>
           <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-auth</span> <span class="s">"src/templates/"</span><span class="p">))</span></code></pre></figure>

<p>We will now begin adding some config, the application <code class="language-plaintext highlighter-rouge">cu-sith</code> that we added as a dependency is used to help manage the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Session">session</a>, we need to provide it with a way to look up a user object and how to get a list of the permissions assigned to the user.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">cu-sith:setup</span>
    <span class="ss">:user-p</span> <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">username</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:active</span> <span class="mi">1</span><span class="p">))</span>
    <span class="ss">:user-roles</span> <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_id</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))))</span></code></pre></figure>

<p>We set up two <a href="http://clhs.lisp.se/Body/m_lambda.htm">lambda</a> functions:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">username</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:active</span> <span class="mi">1</span><span class="p">))</span></code></pre></figure>

<p>This one will, given a <code class="language-plaintext highlighter-rouge">username</code> (a string) will use the <code class="language-plaintext highlighter-rouge">mito</code> <code class="language-plaintext highlighter-rouge">orm</code> to look up our user object, finding the object that matches the username and is also active (remember that the active column is used to determine if a user account is valid to use). Any time the application needs to find out if a user is logged in, this lambda function will be called.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_id</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">)))))</span></code></pre></figure>

<p>This lambda function is used to get the permissions a logged in user has. We will want to check this regularly as a users permissions may change, and it would be poor security to continue to allow a user to perform an action they no longer had the permission for. It takes a user object, and then returns a list of permission object where the user id matches the user passed in. Cu-sith tries to be un-opinionated and doesn’t assume any structure about the way a user object or permissions are loaded, and in fact, because we define our own models here, cu-sith couldn’t possibly have known what our models are or how to use them, which is why we have to provide these functions.</p>

<p><code class="language-plaintext highlighter-rouge">cu-sith</code> stores these lambda functions and runs them at key points in the application run time. Our authentication system can set these up and our project (ningle-tutorial-project) can make calls to <code class="language-plaintext highlighter-rouge">cu-sith</code> and everything will work together.</p>

<p>With this initial setup done, we can look at the individual controllers now!</p>

<h4 id="register-1">Register</h4>

<p>While we looked at a version of the register controller previously, it has changed to a degree so we shall go through the process of writing this again.</p>

<p>As with any controller, we must bind it to our application, we know from our previous work that we bind a <code class="language-plaintext highlighter-rouge">lambda</code>, because we must also render a register form and submit data, the <code class="language-plaintext highlighter-rouge">:methods</code> that we ought to support are <code class="language-plaintext highlighter-rouge">:GET</code> and <code class="language-plaintext highlighter-rouge">:POST</code>:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="o">...</span><span class="p">))</span></code></pre></figure>

<p>Since we know we need to render both a <code class="language-plaintext highlighter-rouge">:GET</code> response and a <code class="language-plaintext highlighter-rouge">:POST</code> response, we can write a simple <code class="language-plaintext highlighter-rouge">if</code> expression, however, both branches will need to access the <code class="language-plaintext highlighter-rouge">register</code> <code class="language-plaintext highlighter-rouge">form</code> object, our <code class="language-plaintext highlighter-rouge">:GET</code> branch will simply render it, our <code class="language-plaintext highlighter-rouge">:POST</code> branch will read and validate data, we will look at the <code class="language-plaintext highlighter-rouge">if</code> branch first before looking at the <code class="language-plaintext highlighter-rouge">else</code> branch:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'register</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/register.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Register"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">)</span>
        <span class="o">...</span><span class="p">))</span></code></pre></figure>

<p>We first load the form object, and if the http request type is <code class="language-plaintext highlighter-rouge">:GET</code> we use djula to render a register template passing in the blank form, however if the http request type is <code class="language-plaintext highlighter-rouge">:POST</code> we will want to do a lot more. We will start with a <code class="language-plaintext highlighter-rouge">handler-case</code>, run <code class="language-plaintext highlighter-rouge">progn</code> which could potentially throw some errors.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">handler-case</span>
    <span class="p">(</span><span class="k">progn</span>
        <span class="o">...</span><span class="p">)</span>
        
    <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))</span>
        
    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">)))</span></code></pre></figure>

<p>There could be a <code class="language-plaintext highlighter-rouge">csrf-error</code> in which case we want to set the http response code to <code class="language-plaintext highlighter-rouge">403</code> and render an error template, with some sort of error displayed, however there may be other types of error we don’t have specific error types for, such as the user entered two different passwords (thus they don’t match) or they tried to register an account with a username or email address that already exists. We will in fact those exact situations into the <code class="language-plaintext highlighter-rouge">progn</code>!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">progn</span>
    <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

    <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

        <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

        <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">username</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
                <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:user</span>
                                        <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                                                    <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
                    <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span>

                <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
                    <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>

                <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">))</span>
                       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
                    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/verify?user=~A&amp;token=~A~%"</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                        <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))</span>
                    <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">)))))</span></code></pre></figure>

<p>We start by handling the request of the form, which can throw a csrf error (handled in the handler-case as described above), but assuming the form is able to pass the security checks we must then validate the form (with the validators we wrote on them). <code class="language-plaintext highlighter-rouge">When</code> there are errors we shall simply display them by using <code class="language-plaintext highlighter-rouge">format</code> to display them in the running terminal.</p>

<p>If however the form is valid, we can continue to process the form as the data is both secure and valid (although that doesn’t mean we’re ready to accept it yet!) we then want to grab the field values with <code class="language-plaintext highlighter-rouge">(cl-forms:with-form-field-values ...)</code> we will grab the email, username, password, and password-verify values from the <code class="language-plaintext highlighter-rouge">form</code>.</p>

<p>Using:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:user</span>
        <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                    <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
    <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span><span class="o">```</span></code></pre></figure>

<p>We check the username and email values to ensure no user object can be found with either of them, if a user can be found we signal an error.</p>

<p>Likewise with the following:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span></code></pre></figure>

<p>If the password and password-verify do not match, we will signal an error again.</p>

<p>Finally, if none of our error conditions have triggered, we can begin to process the data. The following eight lines, do the heavy lifting for us.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/verify?user=~A&amp;token=~A~%"</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Using a <code class="language-plaintext highlighter-rouge">let*</code> binding we create a user object (notice that the active flag is NOT set, as we want users to complete a login flow), and a token object (of the type <code class="language-plaintext highlighter-rouge">+email-verification+</code>), once both of these objects are created we simply build up the url that a user needs to click to take them to form that will activate the user, while we are printing this out to the terminal right now, it is intended that these will be emailed out. Lines 3-7 build and print this url, and finally, once that is done, the controller redirects the browser to the “/” route.</p>

<h5 id="full-listing-3">Full Listing</h5>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'register</span><span class="p">)))</span>
          <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/register.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Register"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">handler-case</span>
                <span class="p">(</span><span class="k">progn</span>
                    <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>
                    <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                      <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                      <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">username</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
                          <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:user</span>
                                 <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                                             <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>

                          <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">))</span>
                                 <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/verify?user=~A&amp;token=~A~%"</span>
                                            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                                            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))))))</span>

                <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))</span>

                <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">)))))))</span></code></pre></figure>

<h4 id="verify">Verify</h4>

<p>To verify our user after initial user registration we must activate the user securely, we start with the usual setup:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="o">...</span><span class="p">))</span></code></pre></figure>

<p>Since we are passing a user and token as <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/search">Query parameters</a> we will immediately extract these in a <code class="language-plaintext highlighter-rouge">let*</code> and since we have multiple conditions to check we will use a <code class="language-plaintext highlighter-rouge">cond</code>.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"user"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">))))</span>
       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+email-verification+</span> <span class="ss">:token</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))))</span>
    <span class="p">(</span><span class="nb">cond</span>
        <span class="o">...</span><span class="p">)</span></code></pre></figure>

<p>There are four conditions to manager inside this <code class="language-plaintext highlighter-rouge">cond</code>, the first is to check if the user is logged in, then redirect if they are.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span></code></pre></figure>

<p>The second condition is when there is a token, but it has expired, we will delete the existing token and issue a new one, printing out the new url and rendering the verification template.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">and</span> <span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">new-token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Token ~A expired, issuing new token: ~A~A/verify?user=~A&amp;token=~A~%"</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">new-token</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/verify.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Verify"</span> <span class="ss">:token-reissued</span> <span class="no">t</span><span class="p">))</span></code></pre></figure>

<p>The third condition is when no token exists, an error message is rendered to the error template.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">not</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Token ~A does not exist~%"</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Token not valid"</span><span class="p">))</span></code></pre></figure>

<p>Finally, we can activate the user by first deleting the verification token, creating the permissions to be associated with the user account, set the user as active and save them. The browser will then redirect to the <code class="language-plaintext highlighter-rouge">"/login"</code> route.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="no">t</span>
    <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:role</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"user"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">ningle-auth/models:activate</span> <span class="nv">user</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:save-dao</span> <span class="nv">user</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"User ~A activated!~%"</span> <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/login"</span><span class="p">)))</span></code></pre></figure>

<h5 id="full-listing-4">Full Listing</h5>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/verify"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
      <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"user"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">))))</span>
             <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+email-verification+</span> <span class="ss">:token</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))))</span>
        <span class="p">(</span><span class="nb">cond</span>
          <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

          <span class="p">((</span><span class="nb">and</span> <span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">new-token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
                <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Token ~A expired, issuing new token: ~A~A/verify?user=~A&amp;token=~A~%"</span>
                    <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                    <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">new-token</span><span class="p">)))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/verify.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Verify"</span> <span class="ss">:token-reissued</span> <span class="no">t</span><span class="p">))</span>

          <span class="p">((</span><span class="nb">not</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Token ~A does not exist~%"</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Token not valid"</span><span class="p">))</span>

          <span class="p">(</span><span class="no">t</span>
            <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:role</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"user"</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:activate</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">mito:save-dao</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"User ~A activated!~%"</span> <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/login"</span><span class="p">)))))))</span></code></pre></figure>

<h4 id="login-1">Login</h4>

<p>As always, let’s prepare the controller!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/login"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
    <span class="o">...</span><span class="p">))</span></code></pre></figure>

<p>Immediately inside it, we will use <code class="language-plaintext highlighter-rouge">let</code> to grab the login form, we will then use a <code class="language-plaintext highlighter-rouge">cond</code> to handle the three conditions we described above, we have seen above how to handle the redirect case, so we will just include it now.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'login</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">cond</span>
        <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>
        <span class="o">...</span><span class="p">))</span></code></pre></figure>

<p>Now, to render the form for a user to fill in (the <code class="language-plaintext highlighter-rouge">GET</code> request), you will notice that we pass in a new parameter <code class="language-plaintext highlighter-rouge">url</code>, this is the url that will be used to allow a user to click a “forgotten password” link, but of course since this application can’t know anything about where it is mounted we both have to look up from the envy-ningle package what the mount path is (we will look at the settings towards the end of this chapter when we integrate the app into our project), and pass the the result of <code class="language-plaintext highlighter-rouge">concatenate</code> with the mount path and <code class="language-plaintext highlighter-rouge">/reset</code>, since we mount this on <code class="language-plaintext highlighter-rouge">/auth</code> the result <em>should</em> be <code class="language-plaintext highlighter-rouge">/auth/reset</code>.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/login.html"</span> <span class="no">nil</span> <span class="ss">:form</span> <span class="nv">form</span> <span class="ss">:url</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/reset"</span><span class="p">)))</span></code></pre></figure>

<p>Finally, when the form is submitted (the <code class="language-plaintext highlighter-rouge">POST</code> request). We will start by using a <code class="language-plaintext highlighter-rouge">handler-case</code> (as we have done before) and immediately open a <code class="language-plaintext highlighter-rouge">progn</code> and use the <code class="language-plaintext highlighter-rouge">cl-forms:handle-request</code> to handle our form. There’s three errors to handle, two come from the <code class="language-plaintext highlighter-rouge">cu-sith</code> package, the <code class="language-plaintext highlighter-rouge">invalid-user</code> and <code class="language-plaintext highlighter-rouge">invalid-password</code> errors, the third is a standard csrf error that we have used before.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="no">t</span>
    <span class="p">(</span><span class="nb">handler-case</span>
        <span class="p">(</span><span class="k">progn</span>
            <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

            <span class="o">...</span><span class="p">)</span>

        <span class="p">(</span><span class="nv">cu-sith:invalid-user</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A, have you verified the account?"</span> <span class="p">(</span><span class="nv">cu-sith:msg</span> <span class="nv">err</span><span class="p">))))</span>

        <span class="p">(</span><span class="nv">cu-sith:invalid-password</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="p">(</span><span class="nv">cu-sith:msg</span> <span class="nv">err</span><span class="p">)))</span>

        <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))</span></code></pre></figure>

<p>We can see that if the <code class="language-plaintext highlighter-rouge">invalid-user</code> error is signalled, it might be that there is no such user, or that the user is not yet active, either way, the user isn’t permitted to log in, and is invalid, in which case rendering the error template with a relevent message is the most helpful thing to do.</p>

<p>The <code class="language-plaintext highlighter-rouge">invalid-password</code> is pretty obvious, the user exists but the password is incorrect, we handle it by rendering the error template.</p>

<p>Finally, as before, if the csrf error is triggered, we use the same handling logic we wrote previously in other controllers.</p>

<p>The rest of the login logic is quite short, within the <code class="language-plaintext highlighter-rouge">handler-case</code> and under the call to <code class="language-plaintext highlighter-rouge">cl-forms:handle-request</code> we can add the following:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">username</span> <span class="nv">password</span><span class="p">)</span> <span class="nv">form</span>
            <span class="p">(</span><span class="nv">cu-sith:login</span> <span class="ss">:user</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:login-redirect</span><span class="p">)))))</span></code></pre></figure>

<p>We bind the valid and errors using the <code class="language-plaintext highlighter-rouge">multiple-value-bind</code> (as we have done before), if there are errors print them to the terminal, and if the form is valid we use <code class="language-plaintext highlighter-rouge">cl-forms:with-form-field-values</code> (again, similarly to before), capturing the username and password, we use the <code class="language-plaintext highlighter-rouge">cu-sith:login</code> function with the username and password, the login function can signal the <code class="language-plaintext highlighter-rouge">invalid-user</code> or <code class="language-plaintext highlighter-rouge">invalid-password</code> that we wrote handlers for above. So either a user will be logged in and saved to the session and the browser will be redirected to a url looked up from settings (we will look at that later), or an error will be signalled which we handle.</p>

<h5 id="full-listing-5">Full Listing</h5>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/login"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'login</span><span class="p">)))</span>
          <span class="p">(</span><span class="nb">cond</span>
            <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

            <span class="p">((</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/login.html"</span> <span class="no">nil</span> <span class="ss">:form</span> <span class="nv">form</span> <span class="ss">:url</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/reset"</span><span class="p">)))</span>

            <span class="p">(</span><span class="no">t</span>
                <span class="p">(</span><span class="nb">handler-case</span>
                    <span class="p">(</span><span class="k">progn</span>
                        <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                        <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">username</span> <span class="nv">password</span><span class="p">)</span> <span class="nv">form</span>
                                <span class="p">(</span><span class="nv">cu-sith:login</span> <span class="ss">:user</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">)</span>
                                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:login-redirect</span><span class="p">))))))</span>

                    <span class="p">(</span><span class="nv">cu-sith:invalid-user</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A, have you verified the account?"</span> <span class="p">(</span><span class="nv">cu-sith:msg</span> <span class="nv">err</span><span class="p">))))</span>

                    <span class="p">(</span><span class="nv">cu-sith:invalid-password</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="p">(</span><span class="nv">cu-sith:msg</span> <span class="nv">err</span><span class="p">)))</span>

                    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))))))</span></code></pre></figure>

<h4 id="logout">Logout</h4>

<p>You may be pleased to know that the logout controller is much, much simpler, all we need to is use <code class="language-plaintext highlighter-rouge">cu-sith</code> to log a user out.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/logout"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">cu-sith:logout</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:login-redirect</span><span class="p">))))</span></code></pre></figure>

<p><code class="language-plaintext highlighter-rouge">cu-sith:logout</code> doesn’t signal any errors, all it does is remove a user and their permissions from the active session. Our controller then just redirects the browser.</p>

<h4 id="reset">Reset</h4>

<p>The password reset process is a fair amount of code, however we have seen a decent amount of it already, certainly concerning the route, the <code class="language-plaintext highlighter-rouge">lambda</code>, grabbing a form and setting up a <code class="language-plaintext highlighter-rouge">cond</code> and handling redirecting the user if they are already logged in. So we will skip over aspects we have already seen before and setup the controller ready to add in the real logic. Lines 5-6 show the redirect, lines 8-9 show the rendering of the template with the form, and of course we have a <code class="language-plaintext highlighter-rouge">handler-case</code> in the <code class="language-plaintext highlighter-rouge">cond</code> where our logic goes.</p>

<p>Line 24 is where we will pick up the new material.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/reset"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'reset-password</span><span class="p">)))</span>
            <span class="p">(</span><span class="nb">cond</span>
              <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

              <span class="p">((</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/reset.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Reset GET"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))</span>

              <span class="p">(</span><span class="no">t</span>
                <span class="p">(</span><span class="nb">handler-case</span>
                    <span class="p">(</span><span class="k">progn</span>
                        <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                        <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span><span class="p">)</span> <span class="nv">form</span>
                                <span class="o">...</span><span class="p">))))</span>

                    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We will start with a <code class="language-plaintext highlighter-rouge">let*</code> binding a user and token object, there may not always be a token, but there may be, within the <code class="language-plaintext highlighter-rouge">let*</code> we set up a <code class="language-plaintext highlighter-rouge">cond</code> with the four conditions we need to be aware of.</p>

<p>Our first check will check if there’s a user, a token, and the token has not expired, and if this condition is met, a warning about an active password reset in progress message is rendered in the error template.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">cond</span>
        <span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span> <span class="p">(</span><span class="nb">not</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">)))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"There is already a password reset in progress, either continue or wait a while before retrying"</span><span class="p">))</span>

        <span class="o">...</span><span class="p">))</span></code></pre></figure>

<p>The next check is if there’s a user and a token (implied to have expired since the check above checked the token wasn’t expired), if so, the token will be deleted, a new one created and a new url printed to the terminal, then the browser will be redirected. This follows a similar pattern for validating our user, which is fortunate, as much of this will be familiar.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/reset/process?user=~A&amp;token=~A~%"</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span></code></pre></figure>

<p>If there is only a user object (that is to say, no active token), the logic is similar to the check above, with the exception that there’s no token to delete.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">user</span>
    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/reset/process?user=~A&amp;token=~A~%"</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span></code></pre></figure>

<p>Finally, if no user could be found, we should display an error:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="no">t</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"No user found"</span><span class="p">))</span></code></pre></figure>

<h5 id="full-listing-6">Full Listing</h5>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/reset"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'reset-password</span><span class="p">)))</span>
            <span class="p">(</span><span class="nb">cond</span>
              <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

              <span class="p">((</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/reset.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Reset GET"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))</span>

              <span class="p">(</span><span class="no">t</span>
                <span class="p">(</span><span class="nb">handler-case</span>
                    <span class="p">(</span><span class="k">progn</span>
                        <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                        <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span><span class="p">)</span> <span class="nv">form</span>
                                <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))</span>
                                       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                  <span class="p">(</span><span class="nb">cond</span>
                                    <span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span> <span class="p">(</span><span class="nb">not</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">)))</span>
                                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"There is already a password reset in progress, either continue or wait a while before retrying"</span><span class="p">))</span>

                                    <span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">)</span>
                                        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
                                        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                          <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/reset/process?user=~A&amp;token=~A~%"</span>
                                            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                                            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)))</span>
                                        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

                                    <span class="p">(</span><span class="nv">user</span>
                                        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                          <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/reset/process?user=~A&amp;token=~A~%"</span>
                                            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                                            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)))</span>
                                        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

                                    <span class="p">(</span><span class="no">t</span>
                                     <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"No user found"</span><span class="p">))))))))</span>

                    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))))))</span></code></pre></figure>

<h4 id="resetprocess">Reset/Process</h4>

<p>Now that a reset url is generated, we need a controller to handle the actual changing of the password, as before we set a route and a handler, but what we will immediately do is grab the form, the user, and the token, with that done we will use a <code class="language-plaintext highlighter-rouge">cond</code> to handle the different cases we need to handle. We have seen before that the first condition is to redirect away if there is a logged in user, so it’s included immediately below.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/reset/process"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'new-password</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"user"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">))))</span>
               <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span> <span class="ss">:token</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))))</span>
          <span class="p">(</span><span class="nb">cond</span>
            <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

              <span class="o">...</span><span class="p">))))</span></code></pre></figure>

<p>The next condition is if the token is invalid, where invalid is defined as not existing, or having expired. In this instance, the error template will be rendered by <code class="language-plaintext highlighter-rouge">djula</code> informing the user that the token is invalid.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">and</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">not</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">)))</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Invalid reset token, please try again"</span><span class="p">))</span></code></pre></figure>

<p>Now our third condition concerns itself with rendering the form ready for a user to fill in, as discussed in the forms section, the <code class="language-plaintext highlighter-rouge">email</code> and <code class="language-plaintext highlighter-rouge">token</code> fields need to be populated so that they’re included in the complete <code class="language-plaintext highlighter-rouge">POST</code> request body, we then render the form.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">((</span><span class="nb">and</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span> <span class="nv">token</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">cl-forms:set-field-value</span> <span class="nv">form</span> <span class="ss">'ningle-auth/forms:email</span> <span class="p">(</span><span class="nv">ningle-auth/models:email</span> <span class="nv">user</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">cl-forms:set-field-value</span> <span class="nv">form</span> <span class="ss">'ningle-auth/forms:token</span> <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/reset.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Create a new password"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))</span></code></pre></figure>

<p>The final condition is processing the form and is our fall through case or <code class="language-plaintext highlighter-rouge">t</code> (as we have seen many times before already). The pattern which has emerged is to have a <code class="language-plaintext highlighter-rouge">handler-case</code> with a <code class="language-plaintext highlighter-rouge">progn</code> inside it and handle, certainly the csrf token error (if it occurs) and any other errors, in this case we will only need to check that passwords do not match. Again, there’s some boiler plate code we are using, such as <code class="language-plaintext highlighter-rouge">cl-forms:handle-request</code> and binding <code class="language-plaintext highlighter-rouge">valid</code> and <code class="language-plaintext highlighter-rouge">errors</code> and checking for each. Inside our <code class="language-plaintext highlighter-rouge">(when valid ...)</code> is where the main logic goes.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="no">t</span>
    <span class="p">(</span><span class="nb">handler-case</span>
        <span class="p">(</span><span class="k">progn</span>
            <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>
            <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>
                    
                <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                    <span class="o">...</span><span class="p">)))</span>

        <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))</span>

        <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))</span></code></pre></figure>

<p>As with in previous form logic, we need to get the field values from <code class="language-plaintext highlighter-rouge">cl-forms</code>, and if the two passwords do not match, an error will be signalled, which we handle in the code above.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">token</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
    <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>
        
    <span class="o">...</span><span class="p">)</span></code></pre></figure>

<p>If no error is signalled then, we can assume that we are able to go ahead and update the user object. We start by opening a <code class="language-plaintext highlighter-rouge">let*</code> block to capture the user and token. If the user exists we will process the update, and if there is no user render a template to inform the browser that there is no such user.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))</span>
       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:token</span> <span class="nv">token</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
    <span class="p">(</span><span class="k">if</span> <span class="nv">user</span>
        <span class="p">(</span><span class="k">progn</span>
            <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">mito-auth:password</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">password</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">mito:save-dao</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/login"</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"No user found"</span><span class="p">)))</span></code></pre></figure>

<p>In the logic for updating the user, the password is set, the user is saved, the token is deleted and the browser is redirected to the login route.</p>

<h4 id="full-listing-7">Full Listing</h4>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span> <span class="ss">:ningle-auth/forms</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:*app*</span>
           <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-auth</span> <span class="s">"src/templates/"</span><span class="p">))</span>

<span class="p">(</span><span class="nv">cu-sith:setup</span>
    <span class="ss">:user-p</span> <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">username</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:active</span> <span class="mi">1</span><span class="p">))</span>
    <span class="ss">:user-roles</span> <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">user</span><span class="p">)</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user_id</span> <span class="p">(</span><span class="nv">mito:object-id</span> <span class="nv">user</span><span class="p">))))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'register</span><span class="p">)))</span>
          <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/register.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Register"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">handler-case</span>
                <span class="p">(</span><span class="k">progn</span>
                    <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>
                    <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                      <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                      <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">username</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
                          <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:user</span>
                                 <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                                             <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>

                          <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">))</span>
                                 <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/verify?user=~A&amp;token=~A~%"</span>
                                            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                                            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))</span>
                            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))))))</span>

                <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))</span>

                <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">)))))))</span>

<span class="c1">;; Must be logged out</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/login"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'login</span><span class="p">)))</span>
          <span class="p">(</span><span class="nb">cond</span>
            <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

            <span class="p">((</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/login.html"</span> <span class="no">nil</span> <span class="ss">:form</span> <span class="nv">form</span> <span class="ss">:url</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/reset"</span><span class="p">)))</span>

            <span class="p">(</span><span class="no">t</span>
                <span class="p">(</span><span class="nb">handler-case</span>
                    <span class="p">(</span><span class="k">progn</span>
                        <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                        <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">username</span> <span class="nv">password</span><span class="p">)</span> <span class="nv">form</span>
                                <span class="p">(</span><span class="nv">cu-sith:login</span> <span class="ss">:user</span> <span class="nv">username</span> <span class="ss">:password</span> <span class="nv">password</span><span class="p">)</span>
                                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:login-redirect</span><span class="p">))))))</span>

                    <span class="p">(</span><span class="nv">cu-sith:invalid-user</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A, have you verified the account?"</span> <span class="p">(</span><span class="nv">cu-sith:msg</span> <span class="nv">err</span><span class="p">))))</span>

                    <span class="p">(</span><span class="nv">cu-sith:invalid-password</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="p">(</span><span class="nv">cu-sith:msg</span> <span class="nv">err</span><span class="p">)))</span>

                    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))))))</span>

<span class="c1">;; Must be logged in</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/logout"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">cu-sith:logout</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:login-redirect</span><span class="p">))))</span>

<span class="c1">;; Must be logged out</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/reset"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'reset-password</span><span class="p">)))</span>
            <span class="p">(</span><span class="nb">cond</span>
              <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

              <span class="p">((</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/reset.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Reset GET"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))</span>

              <span class="p">(</span><span class="no">t</span>
                <span class="p">(</span><span class="nb">handler-case</span>
                    <span class="p">(</span><span class="k">progn</span>
                        <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>

                        <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span><span class="p">)</span> <span class="nv">form</span>
                                <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))</span>
                                       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                  <span class="p">(</span><span class="nb">cond</span>
                                    <span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span> <span class="p">(</span><span class="nb">not</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">)))</span>
                                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"There is already a password reset in progress, either continue or wait a while before retrying"</span><span class="p">))</span>

                                    <span class="p">((</span><span class="nb">and</span> <span class="nv">user</span> <span class="nv">token</span><span class="p">)</span>
                                        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
                                        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                          <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/reset/process?user=~A&amp;token=~A~%"</span>
                                            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                                            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)))</span>
                                        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

                                    <span class="p">(</span><span class="nv">user</span>
                                        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                          <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Reset url: ~A~A/reset/process?user=~A&amp;token=~A~%"</span>
                                            <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                                            <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                                            <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)))</span>
                                        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

                                    <span class="p">(</span><span class="no">t</span>
                                     <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"No user found"</span><span class="p">))))))))</span>

                    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/reset/process"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'new-password</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"user"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">))))</span>
               <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span> <span class="ss">:token</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))))</span>
          <span class="p">(</span><span class="nb">cond</span>
            <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

            <span class="p">((</span><span class="nb">and</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nb">not</span> <span class="nv">token</span><span class="p">)</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">)))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Invalid reset token, please try again"</span><span class="p">))</span>

            <span class="p">((</span><span class="nb">and</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span> <span class="nv">token</span><span class="p">)</span>
                <span class="p">(</span><span class="nv">cl-forms:set-field-value</span> <span class="nv">form</span> <span class="ss">'ningle-auth/forms:email</span> <span class="p">(</span><span class="nv">ningle-auth/models:email</span> <span class="nv">user</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">cl-forms:set-field-value</span> <span class="nv">form</span> <span class="ss">'ningle-auth/forms:token</span> <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">))</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/reset.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Create a new password"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">))</span>

            <span class="p">(</span><span class="no">t</span>
                <span class="p">(</span><span class="nb">handler-case</span>
                    <span class="p">(</span><span class="k">progn</span>
                        <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>
                        <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                            <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                            <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">token</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
                                <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
                                    <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>

                                <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))</span>
                                       <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:token</span> <span class="nv">token</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+password-reset+</span><span class="p">)))</span>
                                  <span class="p">(</span><span class="k">if</span> <span class="nv">user</span>
                                      <span class="p">(</span><span class="k">progn</span>
                                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">mito-auth:password</span> <span class="nv">user</span><span class="p">)</span> <span class="nv">password</span><span class="p">)</span>
                                        <span class="p">(</span><span class="nv">mito:save-dao</span> <span class="nv">user</span><span class="p">)</span>
                                        <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
                                        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/login"</span><span class="p">)))</span>
                                      <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"No user found"</span><span class="p">)))))))</span>

                    <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))</span>

                    <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                        <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">))))))))</span>

<span class="c1">;; Must not be fully set up</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/verify"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
      <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"user"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">))))</span>
             <span class="p">(</span><span class="nv">token</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:token</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:purpose</span> <span class="nv">ningle-auth/models:+email-verification+</span> <span class="ss">:token</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))))</span>
        <span class="p">(</span><span class="nb">cond</span>
          <span class="p">((</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/"</span><span class="p">))</span>

          <span class="p">((</span><span class="nb">and</span> <span class="nv">token</span> <span class="p">(</span><span class="nv">ningle-auth/models:is-expired-p</span> <span class="nv">token</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">new-token</span> <span class="p">(</span><span class="nv">ningle-auth/models:generate-token</span> <span class="nv">user</span> <span class="nv">ningle-auth/models:+email-verification+</span><span class="p">)))</span>
                <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Token ~A expired, issuing new token: ~A~A/verify?user=~A&amp;token=~A~%"</span>
                    <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"http://~A:~A"</span> <span class="p">(</span><span class="nv">lack/request:request-server-name</span> <span class="nv">ningle:*request*</span><span class="p">)</span> <span class="p">(</span><span class="nv">lack/request:request-server-port</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
                    <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">token</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">ningle-auth/models:token-value</span> <span class="nv">new-token</span><span class="p">)))</span>

            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/verify.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Verify"</span> <span class="ss">:token-reissued</span> <span class="no">t</span><span class="p">))</span>

          <span class="p">((</span><span class="nb">not</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Token ~A does not exist~%"</span> <span class="p">(</span><span class="nb">cdr</span> <span class="p">(</span><span class="nb">assoc</span> <span class="s">"token"</span> <span class="nv">params</span> <span class="ss">:test</span> <span class="nf">#'</span><span class="nb">string=</span><span class="p">)))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Token not valid"</span><span class="p">))</span>

          <span class="p">(</span><span class="no">t</span>
            <span class="p">(</span><span class="nv">mito:delete-dao</span> <span class="nv">token</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:role</span> <span class="p">(</span><span class="nv">mito:find-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"user"</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">ningle-auth/models:activate</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nv">mito:save-dao</span> <span class="nv">user</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"User ~A activated!~%"</span> <span class="p">(</span><span class="nv">ningle-auth/models:username</span> <span class="nv">user</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="p">(</span><span class="nb">concatenate</span> <span class="ss">'string</span> <span class="p">(</span><span class="nv">envy-ningle:get-config</span> <span class="ss">:auth-mount-path</span><span class="p">)</span> <span class="s">"/login"</span><span class="p">)))))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-auth</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-auth/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="templates">Templates</h3>

<p>Our templates haven’t changed dramatically since last time, but there’s some small changes.</p>

<h4 id="registerhtml">register.html</h4>

<p>All we do here is render the form that is passed in from our controller.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Register for an account<span class="nt">&lt;/h1&gt;</span>
            {% form form %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="verifyhtml">verify.html</h4>

<p>In our verify template we pass in (from our controller) if the token had expired, we use the <code class="language-plaintext highlighter-rouge">token-reissued</code> variable that <em>may</em> be passed in to inform the user to expect a new email.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Your account is almost ready!<span class="nt">&lt;/h1&gt;</span>
            {% if token-reissued %}
                <span class="nt">&lt;p&gt;</span>This token has expired and a new one has been issued and sent to the email address used when registering.<span class="nt">&lt;/p&gt;</span>
            {% else %}
                <span class="nt">&lt;p&gt;</span>Please follow the instructions send to the email used when registering to verify your account!<span class="nt">&lt;/p&gt;</span>
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="loginhtml">login.html</h4>

<p>In our login template we render our login form, but we also display the <code class="language-plaintext highlighter-rouge">url</code> passed in, that allows a user to click to the “forgot password” link.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Login<span class="nt">&lt;/h1&gt;</span>
            {% form form %}
            <span class="nt">&lt;h4&gt;&lt;a</span> <span class="na">href=</span><span class="s">"{{ url }}"</span><span class="nt">&gt;</span>Forgotten Password?<span class="nt">&lt;/a&gt;&lt;/h4&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="resethtml">reset.html</h4>

<p>Our reset template simply renders the form passed into it.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Reset Password<span class="nt">&lt;/h1&gt;</span>
            {% form form %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h2 id="integrating-the-authentication-app">Integrating the Authentication App</h2>

<h3 id="initial-clean-up-1">Initial Clean Up</h3>

<p>The following files can be deleted as they have been moved into the authentication app:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/forms.lisp</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/models.lisp</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/templates/main/login.html</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/templates/main/logout.html</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/templates/main/register.html</code></li>
</ul>

<h3 id="updating-projectasd-file">Updating project.asd file</h3>

<p>Due to removing some old files we will need to update the project asd file, it should be stressed that we will also be adding new files too, so you will see some files we haven’t written (yet) in this updated <code class="language-plaintext highlighter-rouge">:components</code> section.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="ss">:components</span>
    <span class="p">((</span><span class="ss">:file</span> <span class="s">"contrib"</span><span class="p">)</span>
     <span class="p">(</span><span class="ss">:file</span> <span class="s">"middleware"</span><span class="p">)</span>
     <span class="p">(</span><span class="ss">:file</span> <span class="s">"config"</span><span class="p">)</span>
     <span class="p">(</span><span class="ss">:file</span> <span class="s">"migrations"</span><span class="p">)</span>
     <span class="p">(</span><span class="ss">:file</span> <span class="s">"main"</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="contriblisp">contrib.lisp</h3>

<p>While we are still building up our ideal project structure, we have some code that depends on <code class="language-plaintext highlighter-rouge">ningle-auth</code> (which we have just written) and <em>may</em> end up somewhere else in the project, ningle-auth may become baked into our project structure going forward, at the moment it’s hard to know how best to manage the following code, so I have <code class="language-plaintext highlighter-rouge">contrib</code>-uted some helper code. If a better place for it is found, or we decide to formally bundle things together, we can move it, but for now we will just keep the code here.</p>

<p>In this package we will define a <code class="language-plaintext highlighter-rouge">create-super-user</code> function (which depends on the <code class="language-plaintext highlighter-rouge">ningle-auth</code> models) and a <code class="language-plaintext highlighter-rouge">macro</code> (<code class="language-plaintext highlighter-rouge">with-db-connection</code>) to enable code to run that needs to be run in the context of a database connection. We will use the <code class="language-plaintext highlighter-rouge">with-db-connection</code> macro in other parts of this project.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/contrib</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:create-super-user</span>
           <span class="ss">#:with-db-connection</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/contrib</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defmacro</span> <span class="nv">with-db-connection</span> <span class="p">(</span><span class="k">&amp;body</span> <span class="nv">body</span><span class="p">)</span>
    <span class="o">`</span><span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">backend</span> <span class="nv">args</span><span class="p">)</span> <span class="p">(</span><span class="nv">envy-ningle:extract-middleware-config</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="ss">:mito</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">unless</span> <span class="nv">backend</span>
            <span class="p">(</span><span class="nb">error</span> <span class="s">"No MITO backend found for config ~A"</span> <span class="nv">cfg</span><span class="p">))</span>

        <span class="p">(</span><span class="k">unwind-protect</span>
             <span class="p">(</span><span class="k">progn</span>
               <span class="p">(</span><span class="nb">apply</span> <span class="nf">#'</span><span class="nv">mito:connect-toplevel</span> <span class="nv">backend</span> <span class="nv">args</span><span class="p">)</span>
               <span class="o">,@</span><span class="nv">body</span>
               <span class="p">(</span><span class="nv">mito:disconnect-toplevel</span><span class="p">)))))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">create-super-user</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="nv">username</span> <span class="nv">email</span> <span class="nv">password</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">with-db-connection</span>
      <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'ningle-auth/models:user</span> <span class="ss">:username</span> <span class="nv">username</span> <span class="ss">:email</span> <span class="nv">email</span> <span class="ss">:password</span> <span class="nv">password</span> <span class="ss">:active</span> <span class="mi">1</span><span class="p">)))</span>
        <span class="p">(</span><span class="nv">create-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:role</span> <span class="p">(</span><span class="nv">find-dao</span> <span class="ss">'ningle-auth/models:role</span> <span class="ss">:name</span> <span class="s">"admin"</span><span class="p">))</span>
        <span class="nv">user</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="middlewarelisp">middleware.lisp</h3>

<p>Now, this middleware isn’t, strictly speaking, required, but it will demonstrate another piece of managing security. It’s a little bit more complicated than is ideal, but oh well! We have learned, from previous chapters that middleware runs on each request, <code class="language-plaintext highlighter-rouge">cu-sith</code> stores the user and roles in the active session, however if the permissions change, we need to update the session. This piece of middleware does this.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="ss">:ningle-tutorial-project/middleware</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span> <span class="ss">:ningle-tutorial-project/contrib</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:refresh-roles</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/middleware</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">refresh-roles</span> <span class="p">(</span><span class="nv">app</span><span class="p">)</span>
  <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">env</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">with-db-connection</span>
        <span class="p">(</span><span class="nb">handler-case</span>
            <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">session</span> <span class="p">(</span><span class="nb">getf</span> <span class="nv">env</span> <span class="ss">:lack.session</span><span class="p">)))</span>
              <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">and</span> <span class="nv">session</span> <span class="p">(</span><span class="nb">hash-table-p</span> <span class="nv">session</span><span class="p">)</span> <span class="p">(</span><span class="nb">&gt;</span> <span class="p">(</span><span class="nb">hash-table-count</span> <span class="nv">session</span><span class="p">)</span> <span class="mi">0</span><span class="p">))</span>
                <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">session</span><span class="p">)))</span>
                  <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">typep</span> <span class="nv">user</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)</span>
                    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"[refreshing-roles]~%"</span><span class="p">)</span>
                    <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">roles</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-auth/models:permission</span> <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)))))</span>
                      <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:roles</span> <span class="nv">session</span><span class="p">)</span> <span class="nv">roles</span><span class="p">)</span>
                      <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"[refreshed-roles for ~A] result: ~A~%"</span> <span class="nv">user</span> <span class="nv">roles</span><span class="p">))))))</span>
          <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">e</span><span class="p">)</span>
              <span class="p">(</span><span class="nb">format</span> <span class="vg">*error-output*</span> <span class="s">"Error refreshing roles: ~A~%"</span> <span class="nv">e</span><span class="p">))))</span>
    <span class="p">(</span><span class="nb">funcall</span> <span class="nv">app</span> <span class="nv">env</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We learned from <a href="https://nmunro.github.io/2025/01/30/ningle-3.html">part 3</a> that middleware is a function that accepts an application object (which is a function itself!) and returns a function that accepts an environment. There’s a nuance, however, middleware has to run in a specific order, for example, this middleware depends on using the <code class="language-plaintext highlighter-rouge">session</code> object, so the <code class="language-plaintext highlighter-rouge">:session</code> middleware must run first, else this will fail because there’s no session set up for us to use!</p>

<p>We use the <code class="language-plaintext highlighter-rouge">with-db-connection</code> macro to ensure we have a database connection, and set up a <code class="language-plaintext highlighter-rouge">handler-case</code>, we handle this by capturing any error and displaying to the <em>error-output</em> stream a message, however inside the code to be handled we use a <code class="language-plaintext highlighter-rouge">let</code> to get the session object, but, just because we have a session object (a <code class="language-plaintext highlighter-rouge">hash-table</code>) it doesn’t mean it has any data in it, so we check that the session object <em>is</em> a <code class="language-plaintext highlighter-rouge">hash-table</code> and it has at least one key/value pair in it. If there is, we grab the user object from the session (of course there may not be a user!) and check it is of the type of our model, assuming we have a valid user object we then grab the roles the user can perform and set them into the <code class="language-plaintext highlighter-rouge">:roles</code> section of the session object.</p>

<p>As mentioned above this will run on each request, so if the user permissions changed, the session will be updated as the user navigates the web application. Of course it would be more performant to use a cache, or redis or something, but for this demonstration, this is a decent enough example of how to get this working.</p>

<h3 id="configlisp">config.lisp</h3>

<p>We have a small amount of tinkering to do to our settings, including setting up the middleware order as described above. Most of our changes are concerned with mounting our authentication app, however, because it has migrations, we have added some settings for use in the next section (migrations).</p>

<p>The tricky thing is, we want to mount our authentication application on a route, but we <em>also</em> want the authentication application to know where it is mounted (so that its internal links and routing are correct), as a result we want to set a parameter that defines the mount point and is both set explicitly as a named setting and used in the <code class="language-plaintext highlighter-rouge">:mount</code> middleware section.</p>

<p>Thus the <code class="language-plaintext highlighter-rouge">*auth-mount-path*</code> is used to define this mount path, and in the <code class="language-plaintext highlighter-rouge">:common</code> settings block it is set as the named <code class="language-plaintext highlighter-rouge">:auth-mount-path</code> and later in the <code class="language-plaintext highlighter-rouge">|sqlite|</code> section in the <code class="language-plaintext highlighter-rouge">:mount</code> line.</p>

<p>Additionally, you can see on line #19, we add in the <code class="language-plaintext highlighter-rouge">refresh-roles</code> middleware we defined in the previous section, do remember that order matters and it must be between the <code class="language-plaintext highlighter-rouge">:session</code> middleware and the <code class="language-plaintext highlighter-rouge">:mito</code> middleware else it wont work!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/config</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:envy</span><span class="p">))</span>
<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/config</span><span class="p">)</span>

<span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">config-env-var</span><span class="p">)</span> <span class="s">"APP_ENV"</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defparameter</span> <span class="vg">*auth-mount-path*</span> <span class="s">"/auth"</span><span class="p">)</span> <span class="c1">;; add this</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="ss">:common</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:application-root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:component-pathname</span> <span class="p">(</span><span class="nv">asdf:find-system</span> <span class="ss">:ningle-tutorial-project</span><span class="p">))</span>
    <span class="ss">:installed-apps</span> <span class="p">(</span><span class="ss">:ningle-auth</span><span class="p">)</span>      <span class="c1">;; add this</span>
    <span class="ss">:auth-mount-path</span> <span class="o">,</span><span class="vg">*auth-mount-path*</span> <span class="c1">;; add this</span>
    <span class="ss">:login-redirect</span> <span class="s">"/"</span><span class="p">))</span>               <span class="c1">;; add this</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|sqlite|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="nv">ningle-tutorial-project/middleware:refresh-roles</span> <span class="c1">;; add this</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:sqlite3</span> <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">)))</span>
                 <span class="p">(</span><span class="ss">:mount</span> <span class="o">,</span><span class="vg">*auth-mount-path*</span> <span class="o">,</span><span class="nv">ningle-auth:*app*</span><span class="p">)</span>   <span class="c1">;; add this</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|mysql|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:mysql</span>
                         <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_DB_NAME"</span><span class="p">)))</span>
                         <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_USER"</span><span class="p">)</span>
                         <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PASSWORD"</span><span class="p">)</span>
                         <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_ADDRESS"</span><span class="p">)</span>
                         <span class="ss">:port</span> <span class="o">,</span><span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PORT"</span><span class="p">))))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|postgresql|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:postgres</span>
                         <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_DB_NAME"</span><span class="p">)))</span>
                         <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_USER"</span><span class="p">)</span>
                         <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PASSWORD"</span><span class="p">)</span>
                         <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_ADDRESS"</span><span class="p">)</span>
                         <span class="ss">:port</span> <span class="o">,</span><span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PORT"</span><span class="p">))))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="migrationslisp">migrations.lisp</h3>

<p>Previously we wrote the migrations in such a way that they established their own database connection and ran their migrations, with two apps however, where one defines the settings, it becomes important to ensure that the other does not <em>need</em> to know. As a result we have redesigned the migrations, each application will define a <code class="language-plaintext highlighter-rouge">migrate</code> function, and our project will search through a list of known <code class="language-plaintext highlighter-rouge">installed apps</code> to find their <code class="language-plaintext highlighter-rouge">migrate</code> function, and it will then run these function inside the <code class="language-plaintext highlighter-rouge">with-db-connection</code>. We spoke about this briefly when we rewrote the ningle-auth migrations file, and here we are now!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:ningle-tutorial-project/contrib</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate-apps</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate-apps</span> <span class="p">(</span><span class="k">&amp;optional</span> <span class="p">(</span><span class="nv">apps</span> <span class="no">nil</span><span class="p">))</span>
  <span class="s">"Run migrate function for each app in APPS list. If APPS is nil, migrate all apps listed in *config* :installed-apps."</span>
  <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">apps</span> <span class="p">(</span><span class="nb">or</span> <span class="nv">apps</span> <span class="p">(</span><span class="nb">getf</span> <span class="p">(</span><span class="nv">envy:config</span> <span class="ss">:ningle-tutorial-project/config</span><span class="p">)</span> <span class="ss">:installed-apps</span><span class="p">))))</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">apps</span>
      <span class="p">(</span><span class="nb">error</span> <span class="s">"No apps specified and no :installed-apps found in config."</span><span class="p">))</span>

    <span class="p">(</span><span class="nv">with-db-connection</span>
        <span class="p">(</span><span class="nb">dolist</span> <span class="p">(</span><span class="nv">app</span> <span class="nv">apps</span><span class="p">)</span>
            <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">migrations-pkg-name</span> <span class="p">(</span><span class="nb">string-upcase</span> <span class="p">(</span><span class="nb">format</span> <span class="no">nil</span> <span class="s">"~A/MIGRATIONS"</span> <span class="p">(</span><span class="nb">string-upcase</span> <span class="p">(</span><span class="nb">symbol-name</span> <span class="nv">app</span><span class="p">)))))</span>
                   <span class="p">(</span><span class="nv">migrations-pkg</span> <span class="p">(</span><span class="nb">find-package</span> <span class="nv">migrations-pkg-name</span><span class="p">)))</span>
                <span class="p">(</span><span class="nb">unless</span> <span class="nv">migrations-pkg</span>
                    <span class="p">(</span><span class="nb">error</span> <span class="s">"Migrations package ~A not found."</span> <span class="nv">migrations-pkg-name</span><span class="p">))</span>

                <span class="c1">;; Set app-specific config before calling migrate</span>
                <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">migrate-fn</span> <span class="p">(</span><span class="nb">find-symbol</span> <span class="s">"MIGRATE"</span> <span class="nv">migrations-pkg</span><span class="p">)))</span> <span class="c1">;; Name known to project</span>
                    <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">and</span> <span class="nv">migrate-fn</span> <span class="p">(</span><span class="nb">fboundp</span> <span class="nv">migrate-fn</span><span class="p">))</span>
                        <span class="p">(</span><span class="nb">error</span> <span class="s">"Migrate function not found in package ~A."</span> <span class="nv">migrations-pkg-name</span><span class="p">))</span>
                    <span class="p">(</span><span class="nb">funcall</span> <span class="nv">migrate-fn</span><span class="p">)))))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We start by defining a <code class="language-plaintext highlighter-rouge">migrate-apps</code> function, it can either be passed a list of apps, or it will read the <code class="language-plaintext highlighter-rouge">:installed-apps</code> setting that we added in <code class="language-plaintext highlighter-rouge">config.lisp</code>, if there are no apps an error is signalled, however, if there are, we, once again, use the <code class="language-plaintext highlighter-rouge">with-db-connection</code> macro and loop over the list of apps, getting each package name with a <code class="language-plaintext highlighter-rouge">migrations</code> suffix, if there’s no such package an error is signalled.</p>

<p>Assuming the migrations package has been found, an attempt it made to find the <code class="language-plaintext highlighter-rouge">migrate</code> function within it (this does mean that each app <em>has</em> to have a migrations package with a <code class="language-plaintext highlighter-rouge">migrate</code> function), if this function couldn’t be found an error is signalled, however if it could be, the migration function for that application is called.</p>

<h3 id="mainlisp">main.lisp</h3>

<p>Since we removed much of the logic we previously had from here, we removed <code class="language-plaintext highlighter-rouge">forms.lisp</code> so we will immediately need to remove the import we had in the <code class="language-plaintext highlighter-rouge">defpackage</code>, it should now look like this.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span></code></pre></figure>

<p>Additionally there was a register route, this must be completely removed, which leaves us with only four controllers in this file, including the <code class="language-plaintext highlighter-rouge">/profile</code> controller we are yet to write! So let’s look at them one by one.</p>

<h4 id="route-">Route: “/”</h4>

<p>While this view has not changed much at all, where we previously hard coded the user, we can now pass a real user from the session into our templates.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span>  <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span> <span class="c1">;; Change this</span>
              <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:author</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"Bob"</span><span class="p">)</span>  <span class="ss">:content</span> <span class="s">"Experimenting with Dylan"</span> <span class="ss">:created-at</span> <span class="s">"2025-01-24 @ 13:34"</span><span class="p">)</span>
                           <span class="p">(</span><span class="nb">list</span> <span class="ss">:author</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"Jane"</span><span class="p">)</span> <span class="ss">:content</span> <span class="s">"Wrote in my diary today"</span>  <span class="ss">:created-at</span> <span class="s">"2025-01-24 @ 13:23"</span><span class="p">))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">))))</span></code></pre></figure>

<h4 id="route-profile">Route: “/profile”</h4>

<p>This is our new controller that will only be accessible if the user is logged in. We can see this works by grabbing the user from the session (using a <code class="language-plaintext highlighter-rouge">let</code>) and using a simple <code class="language-plaintext highlighter-rouge">if</code> to either render the template if there is a user, or set the http response code to 403 and render the “Unauthorized” error.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
            <span class="p">(</span><span class="k">if</span> <span class="nv">user</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)</span>
                <span class="p">(</span><span class="k">progn</span>
                    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">))))))</span></code></pre></figure>

<h4 id="route-people">Route: “/people”</h4>

<p>Again, not much has changed here, the only thing we have done is update the code such that the model is now the ningle-auth, and in the final line, we use <code class="language-plaintext highlighter-rouge">cu-sith</code> to pass the logged in user into the template, along with a list of the users registered with the system.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)))))</span></code></pre></figure>

<h4 id="route-peopleperson">Route: “/people/:person”</h4>

<p>A slight change here is, again, to pass the user pulled from the session into the template, but also because we enabled a user to be looked up by username, or email, we have changed the variables, just for clarity.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">username-or-email</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">person</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                              <span class="ss">'ningle-auth/models:user</span>
                              <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username-or-email</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">username-or-email</span><span class="p">)))))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:person</span> <span class="nv">person</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)))))</span></code></pre></figure>

<h4 id="full-listing-8">Full Listing</h4>

<p>Putting it all together!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span>  <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">))</span>
              <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:author</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"Bob"</span><span class="p">)</span>  <span class="ss">:content</span> <span class="s">"Experimenting with Dylan"</span> <span class="ss">:created-at</span> <span class="s">"2025-01-24 @ 13:34"</span><span class="p">)</span>
                           <span class="p">(</span><span class="nb">list</span> <span class="ss">:author</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"Jane"</span><span class="p">)</span> <span class="ss">:content</span> <span class="s">"Wrote in my diary today"</span>  <span class="ss">:created-at</span> <span class="s">"2025-01-24 @ 13:23"</span><span class="p">))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/profile"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span> <span class="p">(</span><span class="nb">gethash</span> <span class="ss">:user</span> <span class="nv">ningle:*session*</span><span class="p">)))</span>
            <span class="p">(</span><span class="k">if</span> <span class="nv">user</span>
                <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/profile.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Profile"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)</span>
                <span class="p">(</span><span class="k">progn</span>
                    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Unauthorized"</span><span class="p">))))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-auth/models:user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">username-or-email</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">person</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                              <span class="ss">'ningle-auth/models:user</span>
                              <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username-or-email</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">username-or-email</span><span class="p">)))))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:person</span> <span class="nv">person</span> <span class="ss">:user</span> <span class="p">(</span><span class="nv">cu-sith:logged-in-p</span><span class="p">)))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span></code></pre></figure>

<h3 id="templates-1">Templates</h3>

<p>Now that our application logic is done, we turn now towards our templates, there’s only three, we need to update the base, our person template, and to write our new profile template.</p>

<h4 id="basehtml">base.html</h4>

<p>In our base.html we will be adapting the upper right of the screen, where we previously had a registration button, we will expand this somewhat to include “register” and “login” if a user is not logged in otherwise a profile link and “logout”.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"d-flex ms-auto"</span><span class="nt">&gt;</span>
    {% if user %}
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/profile"</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span><span class="nt">&gt;</span>{{ user.username }}<span class="nt">&lt;/a&gt;</span>
        <span class="ni">&amp;nbsp;</span>|<span class="ni">&amp;nbsp;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/auth/logout"</span> <span class="na">class=</span><span class="s">"btn btn-secondary"</span><span class="nt">&gt;</span>Logout<span class="nt">&lt;/a&gt;</span>
    {% else %}
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/auth/register"</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span><span class="nt">&gt;</span>Register<span class="nt">&lt;/a&gt;</span>
        <span class="ni">&amp;nbsp;</span>|<span class="ni">&amp;nbsp;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/auth/login"</span> <span class="na">class=</span><span class="s">"btn btn-success"</span><span class="nt">&gt;</span>Login<span class="nt">&lt;/a&gt;</span>
    {% endif %}
<span class="nt">&lt;/div&gt;</span></code></pre></figure>

<h4 id="personhtml">person.html</h4>

<p>Since we have adjusted the data that we pass into the person template, we need to likewise adapt the template to the new data. The reason we have both user and person is that the user is the active logged in user, and the person is the one that is being looked up to view this page, and these are very likely to be different values, unless, you know, you’re <a href="https://en.wikipedia.org/wiki/Ed_Balls#Ed_Balls_Day">Ed Balls</a>.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
    {% if not person %} ;; change 'user' to 'person'
        <span class="nt">&lt;h1&gt;</span>No users<span class="nt">&lt;/h1&gt;</span>
    {% else %}
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title"</span><span class="nt">&gt;</span>{{ person.username }}<span class="nt">&lt;/h5&gt;</span> ;; change 'user' to 'person'
                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-text"</span><span class="nt">&gt;</span>{{ person.email }}<span class="nt">&lt;/p&gt;</span> ;; change 'user' to 'person'
                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted small"</span><span class="nt">&gt;&lt;/p&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    {% endif %}
<span class="nt">&lt;/div&gt;</span></code></pre></figure>

<h4 id="profilehtml">profile.html</h4>

<p>Our new profile template will be real simple, since the check is if it is accessible at all, it really doesn’t have to contain much, at least, right now.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12 text-center"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
              <span class="nt">&lt;h1&gt;</span>Profile<span class="nt">&lt;/h1&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="css">CSS</h3>

<p>I am by no means a CSS expert, and things aren’t really looking the way I would like them do, I will include what css I have written, although it really is beyond my ability to teach good css!</p>

<h4 id="maincss">main.css</h4>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>  <span class="c">/* Ensure inputs take up the full width */</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span> <span class="cp">!important</span><span class="p">;</span> <span class="c">/* Override any conflicting styles */</span>
    <span class="nl">max-width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="c">/* Ensure no unnecessary constraints */</span>
    <span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"text"</span><span class="o">],</span>
<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"password"</span><span class="o">]</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.form-control;</span> <span class="c">/* Apply Bootstrap's .form-control */</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="c">/* Ensure they are block-level elements */</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="c">/* Make the input full width */</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span> <span class="c">/* Spacing */</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.btn;</span>
    <span class="err">@extend</span> <span class="err">.btn-primary;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">div</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.mb-3;</span>
<span class="p">}</span>

<span class="nt">form</span><span class="nf">#login</span> <span class="nt">label</span> <span class="p">{</span>
    <span class="err">@extend</span> <span class="err">.form-label;</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="nb">bold</span><span class="p">;</span>
    <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<h2 id="conclusion">Conclusion</h2>

<p>If you are still here, thank you, truly, this was quite a lot to both write the code for, and write up, so I really do appreciate you reading this far, I certainly hope you found it helpful and interesting. It certainly covered a lot, but security is something to take seriously, and understanding how to write a complete authentication system, even one this basic, requires a lot of learning!</p>

<p>Fortunately next time wont be anywhere near as large, we will be looking at how to email the urls with tokens to users to make this system practical to use in the real world!</p>

<h3 id="learning-outcomes">Learning Outcomes</h3>

<ul>
  <li>Describe the flow and logic of a secure user authentication system in a Common Lisp web application.</li>
  <li>Implement a registration system that validates inputs, detects existing users, and generates secure email verification tokens.</li>
  <li>Evaluate verification links and activate user accounts by processing and expiring one-time tokens securely.</li>
  <li>Develop a login system that restricts access to authenticated users and gracefully handles CSRF and invalid credentials.</li>
  <li>Construct protected routes that enforce login restrictions using session-based user identity via cu-sith.</li>
  <li>Design and implement a two-step password reset flow using secure, expiring tokens.</li>
  <li>Diagnose and respond to expired or missing tokens by regenerating tokens or returning user-friendly error messages.</li>
  <li>Securely update user passwords after verifying token authenticity and ensuring CSRF compliance.</li>
  <li>Demonstrate the use of mito, djula, cl-forms, and cu-sith to integrate session handling, form validation, and database access in a cohesive system.</li>
  <li>Critique and improve form validation logic and user feedback in an authentication flow.</li>
</ul>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-9">here</a>.</li>
  <li>The link for the auth app code is available <a href="https://github.com/nmunro/ningle-auth">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="http://clhs.lisp.se/Body/f_concat.htm">concatenate</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_cond.htm">cond</a></li>
  <li><a href="https://github.com/nmunro/cu-sith">cu-sith</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defmac.htm">defmacro</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defpar.htm">defparameter</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defun.htm">defun</a></li>
  <li><a href="https://github.com/mmontone/djula">djula</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_hand_1.htm">handler-case</a></li>
  <li><a href="https://www.okta.com/blog/2019/03/what-are-salted-passwords-and-password-hashing/">hashed and salted passwords</a></li>
  <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Session">http session</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_if.htm">if</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_init_i.htm#initialize-instance">initialize-instance</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_lambda.htm">lambda</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_let_l.htm">let</a></li>
  <li><a href="http://clhs.lisp.se/Body/s_let_l.htm">let*</a></li>
  <li><a href="http://clhs.lisp.se/Body/26_glo_m.htm#macro">macro</a></li>
  <li><a href="https://github.com/fukamachi/mito-auth">mito-auth</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_multip.htm">multiple-value-bind</a></li>
  <li><a href="https://github.com/fukamachi/ningle">Ningle</a></li>
  <li><a href="https://github.com/nmunro/ningle-auth">ningle-auth</a></li>
  <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/search">query parameters</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_tpcase.htm#typecase">typecase</a></li>
</ul>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 8: Mounting Middleware</title><link href="nmunro.github.io/2025/06/29/ningle-8.html" rel="alternate" type="text/html" title="Ningle Tutorial 8: Mounting Middleware" /><published>2025-06-29T11:30:00+00:00</published><updated>2025-06-29T11:30:00+00:00</updated><id>nmunro.github.io/2025/06/29/ningle-8</id><content type="html" xml:base="nmunro.github.io/2025/06/29/ningle-8.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li>Part 8 (Mounting Middleware)</li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back to this Ningle tutorial series, in this part we are gonna have another look at some middleware, now that we have settings and configuration done there’s another piece of middleware we might want to look at; application <code class="language-plaintext highlighter-rouge">mounting</code>, many web frameworks have the means to use apps within other apps, you might want to do this because you have some functionality you use over and over again in many projects, it makes sense to make it into an app and simply include it in other apps. You might also might want to make applications available for others to use in their applications.</p>

<p>Which is exactly what we are gonna do here, we spent some time building a registration view, but for users we might want to have a full registration system that will have:</p>

<ul>
  <li>Register</li>
  <li>Login</li>
  <li>Logout</li>
  <li>Account Verification</li>
  <li>Account Reset</li>
  <li>Account Deletion</li>
</ul>

<h3 id="creating-the-auth-app">Creating the auth app</h3>

<p>We will begin by building the basic views that return a simple template and mount them into our main application, we will then fill the actual logic out in another tutorial. So, we will create a new Ningle project that has 6 views that simply handle <code class="language-plaintext highlighter-rouge">get</code> requests, the important thing to bear in mind is that we will have to adjust the layout of our templates, we need our auth app to use its own templates, or use the templates of a parent app, this means we will have to namespace our templates, if you have use django before this will seem familiar.</p>

<p>Using my <a href="https://github.com/nmunro/nmunro-project">project builder</a> set up a new project for our authentication application.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp">    <span class="p">(</span><span class="nv">nmunro:make-project</span> <span class="ss">#p"~/quicklisp/local-projects/ningle-auth/"</span><span class="p">)</span></code></pre></figure>

<p>This will create a project skeleton, complete with an <code class="language-plaintext highlighter-rouge">asd</code> file, a <code class="language-plaintext highlighter-rouge">src</code>, and <code class="language-plaintext highlighter-rouge">tests</code> directory. In the <code class="language-plaintext highlighter-rouge">asd</code> file we need to add some packages (we will add more in a later tutorial).</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp">  <span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:cl-dotenv</span>
               <span class="ss">:clack</span>
               <span class="ss">:djula</span>
               <span class="ss">:envy-ningle</span>
               <span class="ss">:mito</span>
               <span class="ss">:ningle</span><span class="p">)</span></code></pre></figure>

<p>In the <code class="language-plaintext highlighter-rouge">src/main.lisp</code> file, we will add the following:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:*app*</span>
           <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-auth</span> <span class="s">"src/templates/"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Test: ~A~%"</span> <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="s">"SELECT 2 + 3 AS result"</span><span class="p">))</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/register.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Register"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/login"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/login.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Login"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/logout"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/logout.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Logout"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/reset"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/reset.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Reset"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/verify"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/verify.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Verify"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/delete"</span><span class="p">)</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"ningle-auth/delete.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Delete"</span><span class="p">)))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-auth</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-auth/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Just as we did with our main application, we will need to create a <code class="language-plaintext highlighter-rouge">src/config.lisp</code>:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="code"><pre><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-auth/config</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:envy</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-auth/config</span><span class="p">)</span>

<span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-auth</span> <span class="s">".env"</span><span class="p">))</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">config-env-var</span><span class="p">)</span> <span class="s">"APP_ENV"</span><span class="p">)</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="ss">:common</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:application-root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:component-pathname</span> <span class="p">(</span><span class="nv">asdf:find-system</span> <span class="ss">:ningle-auth</span><span class="p">))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|test|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:sqlite3</span> <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">))))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Now, I mentioned that the template files need to be organised in a certain way, we will start with the <em>new</em> template layout in our auth application, the directory structure should look like this:</p>

<figure class="highlight"><pre><code class="language-zsh" data-lang="zsh">➜  ningle-auth git:<span class="o">(</span>main<span class="o">)</span> tree <span class="nb">.</span>
<span class="nb">.</span>
├── ningle-auth.asd
├── README.md
├── src
│   ├── config.lisp
│   ├── main.lisp
│   └── templates
│       ├── ningle-auth
│       │   ├── delete.html
│       │   ├── login.html
│       │   ├── logout.html
│       │   ├── register.html
│       │   ├── reset.html
│       │   └── verify.html
│       ├── base.html
│       └── error.html
└── tests
    └── main.lisp</code></pre></figure>

<p>So in your <code class="language-plaintext highlighter-rouge">src/templates</code> directory there will be a directory called <code class="language-plaintext highlighter-rouge">ningle-auth</code> and two files <code class="language-plaintext highlighter-rouge">base.html</code> and <code class="language-plaintext highlighter-rouge">error.html</code>, it is important that this structure is followed, as when the app is used as part of a larger app, we want to be able to layer templates, and this is how we do it.</p>

<h4 id="basehtml">base.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre><span class="cp">&lt;!doctype html&gt;</span>
<span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;title&gt;</span>{{ title }}<span class="nt">&lt;/title&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container mt-4"</span><span class="nt">&gt;</span>
            {% block content %}
            {% endblock %}
        <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="errorhtml">error.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;h1&gt;</span>{{ error }}<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<p>Now the rest of the html files are similar, with only the title changing. Using the following html, create files for:</p>

<h4 id="deletehtml">delete.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Delete<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="loginhtml">login.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Login<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="logouthtml">logout.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Logout<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="registerhtml">register.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Register<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="resethtml">reset.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Reset<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="verifyhtml">verify.html</h4>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre>{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;h1&gt;</span>Verify<span class="nt">&lt;/h1&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}
</pre></td></tr></tbody></table></code></pre></figure>

<p>There is one final file to create, the <code class="language-plaintext highlighter-rouge">.env</code> file! Even though this application wont typically run on its own, we will use one to test it is all working, since we did write <code class="language-plaintext highlighter-rouge">src/config.lisp</code> afterall!</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="code"><pre><span class="nv">APP_ENV</span><span class="o">=</span><span class="nb">test
</span><span class="nv">SQLITE_DB_NAME</span><span class="o">=</span>ningle-auth.db
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="testing-the-auth-app">Testing the auth app</h3>

<p>Now that the auth application has been created we will test that it at least runs on its own, once we have confirmed this, we can integrate it into our main app. Like with our main application, we will load the system and run the start function that we defined.</p>

<figure class="highlight"><pre><code class="language-commonlisp" data-lang="commonlisp">(ql:quickload :ningle-auth)
To load "ningle-auth":
  Load 1 ASDF system:
    ningle-auth
; Loading "ningle-auth"
..................................................
[package ningle-auth/config].
(:NINGLE-AUTH)
(ningle-auth:start)
NOTICE: Running in debug mode. Debugger will be invoked on errors.
  Specify ':debug nil' to turn it off on remote environments.
Woo server is started.
Listening on 127.0.0.1:8000.
#S(CLACK.HANDLER::HANDLER
   :SERVER :WOO
   :SWANK-PORT NIL
   :ACCEPTOR #&lt;BT2:THREAD "clack-handler-woo" {1203E4E3E3}&gt;)
*</code></pre></figure>

<p>If this works correctly, you should be able to access the defined routes in your web browser, if not, and there is an error, check that another web server isn’t running on port 8000 first! When you are able to access the simple routes from your web browser, we are ready to integrate this into our main application!</p>

<h3 id="integrating-the-auth-app">Integrating the auth app</h3>

<p>Made it this far? Congratulations, we are almost at the end, I’m sure you’ll be glad to know, there isn’t all that much more to do, but we do have to ensure we follow the structure we set up in the auth app, which we will get to in just a moment, first, lets remember to add the <code class="language-plaintext highlighter-rouge">ningle-auth</code> app to our dependencies in our project <code class="language-plaintext highlighter-rouge">asd</code> file.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:cl-dotenv</span>
               <span class="ss">:clack</span>
               <span class="ss">:djula</span>
               <span class="ss">:cl-forms</span>
               <span class="ss">:cl-forms.djula</span>
               <span class="ss">:cl-forms.ningle</span>
               <span class="ss">:envy</span>
               <span class="ss">:envy-ningle</span>
               <span class="ss">:ingle</span>
               <span class="ss">:mito</span>
               <span class="ss">:mito-auth</span>
               <span class="ss">:ningle</span>
               <span class="ss">:ningle-auth</span><span class="p">)</span> <span class="c1">;; add this</span></code></pre></figure>

<p>Next, we need to move most of our template files into a directory called <code class="language-plaintext highlighter-rouge">main</code>, to make things easy, the only two templates we will <em>not</em> move are <code class="language-plaintext highlighter-rouge">base.html</code> and <code class="language-plaintext highlighter-rouge">error.html</code>; create a new directory <code class="language-plaintext highlighter-rouge">src/templates/main</code> and put everything else in there.</p>

<p>For reference this is what your directory structure should look like:</p>

<figure class="highlight"><pre><code class="language-zsh" data-lang="zsh">➜  ningle-tutorial-project git:<span class="o">(</span>main<span class="o">)</span> tree <span class="nb">.</span>
<span class="nb">.</span>
├── ningle-tutorial-project.asd
├── ntp.db
├── README.md
├── src
│   ├── config.lisp
│   ├── forms.lisp
│   ├── main.lisp
│   ├── migrations.lisp
│   ├── models.lisp
│   ├── static
│   │   ├── css
│   │   │   └── main.css
│   │   └── images
│   │       ├── logo.jpg
│   │       └── lua.jpg
│   └── templates
│       ├── base.html
│       ├── error.html
│       └── main
│           ├── index.html
│           ├── login.html
│           ├── logout.html
│           ├── people.html
│           ├── person.html
│           └── register.html
└── tests
    └── main.lisp</code></pre></figure>

<p>With the templates having been moved, we must find all areas in <code class="language-plaintext highlighter-rouge">src/main.lisp</code> where we reference one of these templates and point to the new location, thankfully there’s only 4 lines that need to be changed, the render-template* calls, below is what they should be changed to.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">)</span>
<span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span><span class="p">)</span>
<span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">)</span>
<span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/register.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Register"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">)</span></code></pre></figure>

<p>Here is a complete listing of the file in question.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:import-from</span>
   <span class="ss">:ningle-tutorial-project/forms</span>
   <span class="ss">#:email</span>
   <span class="ss">#:username</span>
   <span class="ss">#:password</span>
   <span class="ss">#:password-verify</span>
   <span class="ss">#:register</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:start</span>
           <span class="ss">#:stop</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">user</span>  <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"NMunro"</span><span class="p">))</span>
              <span class="p">(</span><span class="nv">posts</span> <span class="p">(</span><span class="nb">list</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:author</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"Bob"</span><span class="p">)</span>  <span class="ss">:content</span> <span class="s">"Experimenting with Dylan"</span> <span class="ss">:created-at</span> <span class="s">"2025-01-24 @ 13:34"</span><span class="p">)</span>
                           <span class="p">(</span><span class="nb">list</span> <span class="ss">:author</span> <span class="p">(</span><span class="nb">list</span> <span class="ss">:username</span> <span class="s">"Jane"</span><span class="p">)</span> <span class="ss">:content</span> <span class="s">"Wrote in my diary today"</span>  <span class="ss">:created-at</span> <span class="s">"2025-01-24 @ 13:23"</span><span class="p">))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/index.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Home"</span> <span class="ss">:user</span> <span class="nv">user</span> <span class="ss">:posts</span> <span class="nv">posts</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">person</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">user</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                              <span class="ss">'ningle-tutorial-project/models:user</span>
                              <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">person</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">person</span><span class="p">)))))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'register</span><span class="p">)))</span>
          <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">string=</span> <span class="s">"GET"</span> <span class="p">(</span><span class="nv">lack.request:request-method</span> <span class="nv">ningle:*request*</span><span class="p">))</span>
            <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"main/register.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Register"</span> <span class="ss">:form</span> <span class="nv">form</span><span class="p">)</span>
            <span class="p">(</span><span class="nb">handler-case</span>
                <span class="p">(</span><span class="k">progn</span>
                    <span class="p">(</span><span class="nv">cl-forms:handle-request</span> <span class="nv">form</span><span class="p">)</span> <span class="c1">; Can throw an error if CSRF fails</span>
                    <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
                        <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>

                      <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
                        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

                      <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
                        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">username</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
                          <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span>
                                 <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                                             <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span>

                          <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>

                          <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span>
                                           <span class="ss">:email</span> <span class="nv">email</span>
                                           <span class="ss">:username</span> <span class="nv">username</span>
                                           <span class="ss">:password</span> <span class="nv">password</span><span class="p">)</span>
                          <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/people"</span><span class="p">)))))</span>

                <span class="p">(</span><span class="nb">error</span> <span class="p">(</span><span class="nv">err</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">err</span><span class="p">))</span>

                <span class="p">(</span><span class="kt">simple-error</span> <span class="p">(</span><span class="nv">csrf-error</span><span class="p">)</span>
                    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">403</span><span class="p">)</span>
                    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="nv">csrf-error</span><span class="p">)))))))</span>

<span class="p">(</span><span class="nb">defmethod</span> <span class="nv">ningle:not-found</span> <span class="p">((</span><span class="nv">app</span> <span class="nv">ningle:&lt;app&gt;</span><span class="p">))</span>
    <span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">app</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">lack.response:response-status</span> <span class="nv">ningle:*response*</span><span class="p">)</span> <span class="mi">404</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"error.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Error"</span> <span class="ss">:error</span> <span class="s">"Not Found"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">stop</span> <span class="p">(</span><span class="nv">instance</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:stop</span> <span class="nv">instance</span><span class="p">))</span></code></pre></figure>

<p>The final step we must complete is actually mounting our <code class="language-plaintext highlighter-rouge">ningle-auth</code> application into our main app, which is thankfully quite easy. Mounting middleware exists for <code class="language-plaintext highlighter-rouge">ningle</code> and so we can configure this in <code class="language-plaintext highlighter-rouge">src/config.lisp</code>, to demonstrate this we will add it to our <code class="language-plaintext highlighter-rouge">sqlite</code> config:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|sqlite|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:sqlite3</span> <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">)))</span>
                 <span class="p">(</span><span class="ss">:mount</span> <span class="s">"/auth"</span> <span class="o">,</span><span class="nv">ningle-auth:*app*</span><span class="p">)</span> <span class="c1">;; This line!</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>You can see on line #5 that a new <code class="language-plaintext highlighter-rouge">mount</code> point is being defined, we are mounting all the routes that <code class="language-plaintext highlighter-rouge">ningle-auth</code> has, onto the <code class="language-plaintext highlighter-rouge">/auth</code> prefix. This means that, for example, the <code class="language-plaintext highlighter-rouge">/register</code> route in <code class="language-plaintext highlighter-rouge">ningle-auth</code> will actually be accessed <code class="language-plaintext highlighter-rouge">/auth/register</code>.</p>

<p>If you can check that you can access all the urls to confirm this works, then we have assurances that we are set up correctly, however we need to come back to the templates one last time.</p>

<p>The reason we changed the directory structure, because ningle-auth is now running in the context of our main app, we can actually override the templates, so if we wanted to, in our <code class="language-plaintext highlighter-rouge">src/templates</code> directory, we could create a <code class="language-plaintext highlighter-rouge">ningle-auth</code> directory and create our own <code class="language-plaintext highlighter-rouge">register.html</code>, <code class="language-plaintext highlighter-rouge">login.html</code>, etc, allowing us to style and develop our pages as we see fit, allowing complete control to override, if that is our wish. By NOT moving the <code class="language-plaintext highlighter-rouge">base.html</code> and <code class="language-plaintext highlighter-rouge">error.html</code> files, we ensure that templates from another app can inherit our styles and layouts in a simple and predictable manner.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Wow, what a ride… Thanks for sticking with it this month, although, next month isn’t going to be much easier as we begin to develop a real authentication application for use in our microblog app! As always, I hope you have found this helpful and you have learned something.</p>

<p>In this tutorial you should be able to:</p>

<ul>
  <li>Explain what mounting an application means</li>
  <li>Describe how routes play a part in mounting an application</li>
  <li>Justify why you might mount an application into another</li>
  <li>Develop and mount an application inside another</li>
</ul>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-8">here</a>.</li>
  <li>The link for the auth app code is available <a href="https://github.com/nmunro/ningle-auth">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://github.com/fukamachi/ningle">Ningle</a></li>
  <li><a href="https://github.com/nmunro/ningle-auth">ningle-auth</a></li>
  <li><a href="https://github.com/nmunro/nmunro-project">nmunro project builder</a></li>
  <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET">HTTP GET</a></li>
</ul>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 7: Envy Configuration Switching</title><link href="nmunro.github.io/2025/05/31/ningle-7.html" rel="alternate" type="text/html" title="Ningle Tutorial 7: Envy Configuration Switching" /><published>2025-05-31T21:30:00+00:00</published><updated>2025-05-31T21:30:00+00:00</updated><id>nmunro.github.io/2025/05/31/ningle-7</id><content type="html" xml:base="nmunro.github.io/2025/05/31/ningle-7.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li><a href="/2025/04/30/ningle-6.html">Part 6 (Database Connections)</a></li>
  <li>Part 7 (Envy Configuation Switching)</li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back, in this tutorial we will look at how to simplify the complexities introduced last time. We had three different versions of our application, depending on which SQL database we wanted to use, this is hardly an ideal situation, we might want to run SQLite on one environment and PostgreSQL on another, it does not make sense to have to edit code to change such things, we should have code that is generalised and some configuration (like environmental variables) can provide the system with the connection information.</p>

<p>We want to separate our application configuration from our application logic, in software development we might build an application and have different environments in which is can be deployed, and different cloud providers/environments might have different capabilities, for example some providers provide PostgreSQL and others MySQL. As application designers we do not want to concern ourselves with having to patch our application based whatever an environment has provided, it would be better if we had a means by which we could read in <em>how</em> we connect to our databases and defer to that.</p>

<p>This type of separation is very common, in fact it is this separation that <code class="language-plaintext highlighter-rouge">ningle</code> itself if for! Just as now we are creating a means to connect to a number of different databases based on config, <code class="language-plaintext highlighter-rouge">ningle</code> allows us to run on a number of different webservers, without <code class="language-plaintext highlighter-rouge">ningle</code> we would have to write code directly in the way a web server expects, <code class="language-plaintext highlighter-rouge">ningle</code> allows us to write more generalised code.</p>

<p>Enter <a href="https://github.com/fukamachi/envy">envy</a>, a package that allows us to define different application configurations. Envy will allow us to set up different configurations and switch them based on an environmental variable, just like we wanted. Using this allows us to remove all of our database specific connection code and read it from a configuration, the configuration of which can be changed, the application can be restarted and everything <em>should</em> just work.</p>

<p>We have a slight complication in that we have our migration code, so we will need a way to also extract the active settings, but I wrote a package to assist in this <a href="https://github.com/nmunro/envy-ningle">envy-ningle</a>, we will use both these packages to clean up our code.</p>

<h2 id="installing-packages">Installing Packages</h2>

<p>To begin with we will need to ensure we have installed and added the packages we need to our project asd file, there are two that we will be installing:</p>

<ul>
  <li><a href="https://github.com/fukamachi/envy">envy</a></li>
  <li><a href="https://github.com/nmunro/envy-ningle">envy-ningle</a></li>
</ul>

<p>Note: My package (envy-ningle) is NOT in <a href="https://www.quicklisp.org/beta/">quicklisp</a>, so you will need to clone it using <code class="language-plaintext highlighter-rouge">git</code> into your <code class="language-plaintext highlighter-rouge">local-packages</code> directory. Please ensure that you use the tag <a href="https://github.com/nmunro/envy-ningle/releases/tag/v0.0.1">v0.0.1</a> for this tutorial, it is a package I work on and use in my own projects, so the latest may not necessarily work for any given chapter. I will try to ensure the correct versions are specified, but if you find a mistake <a href="mailto:nmunro@duck.com">let me know</a>.</p>

<p>Once you have acquired the packages, as normal you will need to add them in the <code class="language-plaintext highlighter-rouge">:depends-on</code> section.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:clack</span>
             <span class="ss">:cl-dotenv</span>
             <span class="ss">:djula</span>
             <span class="ss">:cl-forms</span>
             <span class="ss">:cl-forms.djula</span>
             <span class="ss">:cl-forms.ningle</span>
             <span class="ss">:envy</span>         <span class="c1">;; Add this</span>
             <span class="ss">:envy-ningle</span>  <span class="c1">;; Add this</span>
             <span class="ss">:ingle</span>
             <span class="ss">:mito</span>
             <span class="ss">:mito-auth</span>
             <span class="ss">:ningle</span><span class="p">)</span></code></pre></figure>

<h2 id="writing-application-configs">Writing Application Configs</h2>

<h3 id="configlisp">config.lisp</h3>

<p>We must write our application configs somewhere, so we will do that in <code class="language-plaintext highlighter-rouge">src/config.lisp</code>, as always when adding a new file to our application we must ensure it is added to the <code class="language-plaintext highlighter-rouge">asd</code> file, in the <code class="language-plaintext highlighter-rouge">:components section</code>. This will ensure the file will be loaded and compiled when the system is loaded.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:components</span> <span class="p">((</span><span class="ss">:module</span> <span class="s">"src"</span>
              <span class="ss">:components</span>
              <span class="p">((</span><span class="ss">:file</span> <span class="s">"config"</span><span class="p">)</span>  <span class="c1">;; Add this</span>
               <span class="p">(</span><span class="ss">:file</span> <span class="s">"models"</span><span class="p">)</span>
               <span class="p">(</span><span class="ss">:file</span> <span class="s">"migrations"</span><span class="p">)</span>
               <span class="p">(</span><span class="ss">:file</span> <span class="s">"forms"</span><span class="p">)</span>
               <span class="p">(</span><span class="ss">:file</span> <span class="s">"main"</span><span class="p">))))</span></code></pre></figure>

<p>So we should write this file now!</p>

<p>As normal we set up a package, declare what packages we will use <code class="language-plaintext highlighter-rouge">(:use :cl :envy)</code> and set the active package to this one. There’s some conventions we must follow using this that may seem unimportant at first, but actually are, specifically the <code class="language-plaintext highlighter-rouge">|sqlite|</code>, <code class="language-plaintext highlighter-rouge">|mysql|</code>, and <code class="language-plaintext highlighter-rouge">|postgresql|</code> they <em>must</em> include the <code class="language-plaintext highlighter-rouge">|</code> surrounding the name, (although the name doesn’t have to be <code class="language-plaintext highlighter-rouge">sqlite</code>, <code class="language-plaintext highlighter-rouge">mysql</code>, or <code class="language-plaintext highlighter-rouge">postgresql</code>, those are just what I used based on the last tutorial).</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/config</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:envy</span><span class="p">))</span>
<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/config</span><span class="p">)</span></code></pre></figure>

<p>We will start by loading the <code class="language-plaintext highlighter-rouge">.env</code> file using the <code class="language-plaintext highlighter-rouge">dotenv</code> package, we will remove it from our <code class="language-plaintext highlighter-rouge">main.lisp</code> file a little later, but we need to include it here, next we will inform envy of what the name of the environmental variable is that will be used to switch config, in this case <code class="language-plaintext highlighter-rouge">APP_ENV</code>.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">config-env-var</span><span class="p">)</span> <span class="s">"APP_ENV"</span><span class="p">)</span></code></pre></figure>

<p>This means that in your <code class="language-plaintext highlighter-rouge">.env</code> file you should add the following:</p>

<p>Note: I am using the sqlite config here, but you can use any of the configs below.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">APP_ENV=sqlite</code></pre></figure>

<p>We can define a “common” set of configs using the <code class="language-plaintext highlighter-rouge">:common</code> label, this differs from the other labels that use the <code class="language-plaintext highlighter-rouge">|</code> to surround them. The <code class="language-plaintext highlighter-rouge">:common</code> config isn’t one that will actually be used, it just provides a place to store the, well, common, configuration. While we don’t yet necessarily have any shared config at this point, it is important to understand how to achieve it. In this example we set an <code class="language-plaintext highlighter-rouge">application-root</code> that all configs will share.</p>

<p>In <code class="language-plaintext highlighter-rouge">envy</code> we use the <code class="language-plaintext highlighter-rouge">defconfig</code> <code class="language-plaintext highlighter-rouge">macro</code> to define a config. Configs take a name, and a <code class="language-plaintext highlighter-rouge">list</code> of items. There is a shared configuration which is called <code class="language-plaintext highlighter-rouge">:common</code>, that any number of other custom configs that inherit from, their names are arbitary, but must be surrounded by <code class="language-plaintext highlighter-rouge">|</code>, for example <code class="language-plaintext highlighter-rouge">|staging|</code>, or <code class="language-plaintext highlighter-rouge">|production|</code>.</p>

<p>This is the <code class="language-plaintext highlighter-rouge">:common</code> we will use in this tutorial:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="ss">:common</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:application-root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:component-pathname</span> <span class="p">(</span><span class="nv">asdf:find-system</span> <span class="ss">:ningle-tutorial-project</span><span class="p">))))</span></code></pre></figure>

<p>We can now define our actual configs, our “development” config will be <code class="language-plaintext highlighter-rouge">sqlite</code>, which will define our database connection, however, because <code class="language-plaintext highlighter-rouge">mito</code> defines database connections as middleware, we can define the middleware section in our config. Each config will have a different middleware section. Unfortunately there will be <em>some</em> repetition with the <code class="language-plaintext highlighter-rouge">(:session)</code> and <code class="language-plaintext highlighter-rouge">(:static ...)</code> middleware sections.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|sqlite|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:sqlite3</span> <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">)))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span></code></pre></figure>

<p>For our <code class="language-plaintext highlighter-rouge">MySQL</code> config we have this:</p>

<p>Note: Please <em>do</em> make sure to use the <code class="language-plaintext highlighter-rouge">(or ...)</code> form, this will ensure that the file can compile correctly.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|mysql|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:mysql</span>
                         <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_DB_NAME"</span><span class="p">)</span> <span class="s">""</span><span class="p">)))</span>
                         <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_USER"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PASSWORD"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_ADDRESS"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:port</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PORT"</span><span class="p">)</span> <span class="mi">0</span><span class="p">))))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span></code></pre></figure>

<p>And finally our <code class="language-plaintext highlighter-rouge">PostgreSQL</code>:</p>

<p>Note: Please <em>do</em> make sure to use the <code class="language-plaintext highlighter-rouge">(or ...)</code> form, this will ensure that the file can compile correctly.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|postgresql|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:postgres</span>
                         <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_DB_NAME"</span><span class="p">)</span> <span class="s">""</span><span class="p">)))</span>
                         <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_USER"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PASSWORD"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_ADDRESS"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:port</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PORT"</span><span class="p">)</span> <span class="mi">0</span><span class="p">))))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span></code></pre></figure>

<p>NOTE: This config is offered for educational purposes highlighting how to write a config that allows the connection of the three major sql databases you are likely to use. In a more practical example one would have a single database and where instead of <code class="language-plaintext highlighter-rouge">(or (uiop:getenv "SOME_ENV") "")</code> to safely fall back, you might instead write:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(or (uiop:getenv "MYSQL_DB_NAME") (error "No MYSQL_DB_NAME envvar"))
</code></pre></div></div>

<p>This would print something out like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#&lt;THREAD tid=259 "main thread" RUNNING {70063603C3}&gt;:
No MYSQL_DB_NAME envvar

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
0: [TRY-RECOMPILING              ] Recompile config and try loading it again
1: [RETRY                        ] Retry
                                 loading FASL for #&lt;CL-SOURCE-FILE "ningle-tutorial-project" "src" "config"&gt;.
2: [ACCEPT                       ] Continue, treating
                                 loading FASL for #&lt;CL-SOURCE-FILE "ningle-tutorial-project" "src" "config"&gt;
                                 as having been successful.
3:                                 Retry ASDF operation.
4: [CLEAR-CONFIGURATION-AND-RETRY] Retry ASDF operation after resetting the
                                 configuration.
5:                                 Retry ASDF operation.
6:                                 Retry ASDF operation after resetting the
                                 configuration.
7: [ABORT                        ] Give up on "ningle-tutorial-project"
8: [REGISTER-LOCAL-PROJECTS      ] Register local projects and try again.
9:                                 Exit debugger, returning to top level.

("top level form") [toplevel]
; Using form offset instead of character position.
error finding frame source: Source path no longer exists.
source: NIL
0]
</code></pre></div></div>

<p>You can see the error message highlighted at the top of the output.</p>

<p>None of this should be especially new, this middleware section should be familiar from last time, simply wrapped up in the <code class="language-plaintext highlighter-rouge">envy:defconfig</code> <code class="language-plaintext highlighter-rouge">macro</code>.</p>

<p>Here is the file in its entirety:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/config</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:envy</span><span class="p">))</span>
<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/config</span><span class="p">)</span>

<span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>
<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">config-env-var</span><span class="p">)</span> <span class="s">"APP_ENV"</span><span class="p">)</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="ss">:common</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:application-root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:component-pathname</span> <span class="p">(</span><span class="nv">asdf:find-system</span> <span class="ss">:ningle-tutorial-project</span><span class="p">))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|sqlite|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:debug</span> <span class="nv">T</span>
    <span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:sqlite3</span> <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">)))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|mysql|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:mysql</span>
                         <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_DB_NAME"</span><span class="p">)</span> <span class="s">""</span><span class="p">)))</span>
                         <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_USER"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PASSWORD"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_ADDRESS"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:port</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PORT"</span><span class="p">)</span> <span class="mi">0</span><span class="p">))))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span>

<span class="p">(</span><span class="nv">defconfig</span> <span class="nv">|postgresql|</span>
  <span class="o">`</span><span class="p">(</span><span class="ss">:middleware</span> <span class="p">((</span><span class="ss">:session</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:mito</span> <span class="p">(</span><span class="ss">:postgres</span>
                         <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_DB_NAME"</span><span class="p">)</span> <span class="s">""</span><span class="p">)))</span>
                         <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_USER"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PASSWORD"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_ADDRESS"</span><span class="p">)</span> <span class="s">""</span><span class="p">)</span>
                         <span class="ss">:port</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="o">,</span><span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PORT"</span><span class="p">)</span> <span class="mi">0</span><span class="p">))))</span>
                 <span class="p">(</span><span class="ss">:static</span> <span class="ss">:root</span> <span class="o">,</span><span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span> <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">))))</span></code></pre></figure>

<h3 id="mainlisp">main.lisp</h3>

<p>As mentioned, we need to do some cleanup in our <code class="language-plaintext highlighter-rouge">main.lisp</code>, the first is to remove the <code class="language-plaintext highlighter-rouge">dotenv</code> code that has been moved into the <code class="language-plaintext highlighter-rouge">config.lisp</code> file, but we will also need to take advantage of the <code class="language-plaintext highlighter-rouge">envy-ningle</code> package to load the active configuration into the <code class="language-plaintext highlighter-rouge">lack</code> builder code.</p>

<p>To remove the <code class="language-plaintext highlighter-rouge">dotenv</code> code:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defvar</span> <span class="vg">*app*</span> <span class="p">(</span><span class="nb">make-instance</span> <span class="ss">'ningle:app</span><span class="p">))</span>

<span class="c1">;; remove the line below</span>
<span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>

<span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/"</span><span class="p">)</span></code></pre></figure>

<p>Now to edit the <code class="language-plaintext highlighter-rouge">start</code> function, it should look like the following:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
     <span class="p">(</span><span class="nv">lack.builder:builder</span> <span class="p">(</span><span class="nv">envy-ningle:build-middleware</span> <span class="ss">:ningle-tutorial-project/config</span> <span class="vg">*app*</span><span class="p">))</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span></code></pre></figure>

<p>As you can see, all of the previous middleware code that had to be changed if you wanted to switch databases, is now a single line, because <code class="language-plaintext highlighter-rouge">envy</code> loads the config based on the environmental variable, the <code class="language-plaintext highlighter-rouge">envy-ningle:build-middleware</code> function will then read that config and insert the middleware into the application. I hope you will agree that it is much simpler and makes your application much easier to manage.</p>

<p>If you are not <em>yet</em> convinced and you think you’re fine to keep things as they were, consider that we have duplicated our database connection logic in <code class="language-plaintext highlighter-rouge">migrations.lisp</code> and if we decide we do need to change our connection we have to do it in two places, possibly more if we have many models and want to break the code up.</p>

<h3 id="migrationslisp">migrations.lisp</h3>

<p>We will use the same structure for how we loaded configuration in our <code class="language-plaintext highlighter-rouge">main.lisp</code> file, the way we use <code class="language-plaintext highlighter-rouge">envy-ningle</code> is different, previously we called the <code class="language-plaintext highlighter-rouge">build-middleware</code> function, which is designed to place the config middleware into the <code class="language-plaintext highlighter-rouge">lack</code> builder, here we want to get only the database connection information and thus we will use the <code class="language-plaintext highlighter-rouge">extract-mito-config</code> (admittedly not the best name), to get the database connection information and use it in <code class="language-plaintext highlighter-rouge">mito:connect-toplevel</code>.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">backend</span> <span class="nv">args</span><span class="p">)</span> <span class="p">(</span><span class="nv">envy-ningle:extract-mito-config</span> <span class="ss">:ningle-tutorial-project/config</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">unless</span> <span class="nv">backend</span>
      <span class="p">(</span><span class="nb">error</span> <span class="s">"No :mito middleware config found in ENVY config."</span><span class="p">))</span>
    <span class="p">(</span><span class="nb">apply</span> <span class="nf">#'</span><span class="nv">mito:connect-toplevel</span> <span class="nv">backend</span> <span class="nv">args</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">mito:disconnect-toplevel</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">)))</span></code></pre></figure>

<p>As you can see here, we use <code class="language-plaintext highlighter-rouge">multiple-value-bind</code> to get the “backend” (which will be one of the three supported SQL databases), and then the arguments that backend expects. If there isn’t a backend, an error is thrown, if there is, we call <code class="language-plaintext highlighter-rouge">apply</code> on the <code class="language-plaintext highlighter-rouge">mito:connect-toplevel</code> with the “backend” and “args” values.</p>

<h2 id="testing-the-config-switching">Testing The Config Switching</h2>

<p>Now that all the code has been written, we will want to test it all works. The simplest way to do this is while the value of “APP_ENV” in your <code class="language-plaintext highlighter-rouge">.env</code> file is “sqlite”, run the migrations.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(ningle-tutorial-project/migrations:migrate)
</code></pre></div></div>

<p>You should see the sqlite specific output, if that works, we can then change the value of “APP_ENV” to be “mysql” or “postgresql”, whichever you have available to you, and we can run the migrations again.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(ningle-tutorial-project/migrations:migrate)
</code></pre></div></div>

<p>This time we would expect to see different sql output, and if you do, you can confirm that the configurating switching is working as expected.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I hope you found that helpful, and that you agree that it’s better to separate our configuration from our actual application code.</p>

<p>To recap, after working your way though this tutorial you should be able to:</p>

<ul>
  <li>Explain what configuration switching is</li>
  <li>Explain why configuration is important</li>
  <li>Discuss the reasons for separating configuration from application code</li>
  <li>Implement your own configurations for applications you write</li>
</ul>

<h2 id="github">Github</h2>

<ul>
  <li>The link for this tutorials code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-7">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="http://clhs.lisp.se/Body/f_apply.htm">apply</a></li>
  <li><a href="https://github.com/fukamachi/envy">envy</a></li>
  <li><a href="https://github.com/nmunro/envy-ningle">envy-ningle</a></li>
  <li><a href="https://github.com/fukamachi/lack">lack</a></li>
  <li><a href="http://clhs.lisp.se/Body/26_glo_m.htm#macro_form">macro</a></li>
  <li><a href="https://github.com/fukamachi/mito">mito</a></li>
  <li><a href="https://github.com/fukamachi/ningle">ningle</a></li>
</ul>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry><entry><title type="html">Ningle Tutorial 6: Database Connections</title><link href="nmunro.github.io/2025/04/30/ningle-6.html" rel="alternate" type="text/html" title="Ningle Tutorial 6: Database Connections" /><published>2025-04-30T21:30:00+00:00</published><updated>2025-04-30T21:30:00+00:00</updated><id>nmunro.github.io/2025/04/30/ningle-6</id><content type="html" xml:base="nmunro.github.io/2025/04/30/ningle-6.html"><![CDATA[<h2 id="contents">Contents</h2>

<ul>
  <li><a href="/2024/12/29/ningle-1.html">Part 1 (Hello World)</a></li>
  <li><a href="/2024/12/30/ningle-2.html">Part 2 (Basic Templates)</a></li>
  <li><a href="/2025/01/30/ningle-3.html">Part 3 (Introduction to middleware and Static File management)</a></li>
  <li><a href="/2025/02/28/ningle-4.html">Part 4 (Forms)</a></li>
  <li><a href="/2025/03/31/ningle-5.html">Part 5 (Environmental Variables)</a></li>
  <li>Part 6 (Database Connections)</li>
  <li><a href="/2025/05/31/ningle-7.html">Part 7 (Envy Configuation Switching)</a></li>
  <li><a href="/2025/06/29/ningle-8.html">Part 8 (Mounting Middleware)</a></li>
  <li><a href="/2025/07/31/ningle-9.html">Part 9 (Authentication System)</a></li>
  <li><a href="/2025/08/28/ningle-10.html">Part 10 (Email)</a></li>
  <li><a href="/2025/09/30/ningle-11.html">Part 11 (Posting Tweets &amp; Advanced Database Queries)</a></li>
  <li><a href="/2025/10/29/ningle-12.html">Part 12 (Clean Up &amp; Bug Fix)</a></li>
  <li><a href="/2025/11/20/ningle-13.html">Part 13 (Adding Comments)</a></li>
  <li><a href="/2026/01/31/ningle-14.html">Part 14 (Pagination, Part 1)</a></li>
  <li><a href="/2026/02/28/ningle-15.html">Part 15 (Pagination, Part 2)</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>Welcome back, in this tutorial we will begin looking at how to work with SQL databases, specifically <a href="https://sqlite.org/index.html">SQLite3</a>, <a href="https://www.mysql.com/">MySQL</a>, and <a href="https://www.postgresql.org/">PostgreSQL</a>. We will be using the <a href="https://github.com/fukamachi/mito">mito</a> <a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping">ORM</a> to create user models and save them to the database using the <code class="language-plaintext highlighter-rouge">form</code> we created previously. <code class="language-plaintext highlighter-rouge">Mito</code> itself is a basic ORM and includes several <a href="https://en.wikipedia.org/wiki/Mixin">mixins</a> to provide additional functionality, we will use one called <a href="https://github.com/fukamachi/mito-auth">mito-auth</a> to provide password hashing and salting.</p>

<p>It is important to know that <code class="language-plaintext highlighter-rouge">mito</code> is based on top of a SQL library known as <a href="https://github.com/fukamachi/sxql">SXQL</a>, we will occasionally use <code class="language-plaintext highlighter-rouge">SXQL</code> to write queries with <code class="language-plaintext highlighter-rouge">mito</code>, while it’s not <em>always</em> required to use SXQL, there are times where it will make life easier. To achieve this, I elected to <code class="language-plaintext highlighter-rouge">:use</code> SXQL in my package definition.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:sxql</span><span class="p">)</span></code></pre></figure>

<p>Part of working with databases using an ORM is creating the initial database/tables and managing changes over time, called <code class="language-plaintext highlighter-rouge">migrations</code>, <code class="language-plaintext highlighter-rouge">mito</code> appears to have a migrations system, although I was unable to get it working, but I developed a means by which to apply migrations, so perhaps in a future tutorial the subject can be revisited. As such, in addition to seeing how to connect to the respective SQL databases, we will write implementation specific migration functions.</p>

<p>We will follow the example of setting up a secure user registration system across all three SQL implementations. One thing to bear in mind is that it is beyond the scope of this tutorial to instruct how to setup MySQL or PostgreSQL, I would recommend learning how to set them up using <a href="https://www.docker.com/">docker</a>. All that having been said, let’s have a look at the different databases and how to connect to them!</p>

<p>Please bear in mind that when working with SQLite remember to add <code class="language-plaintext highlighter-rouge">.db</code> to your <code class="language-plaintext highlighter-rouge">.gitignore</code> as you most certainly don’t want to accidentally commit a database into git! SQLite, being a file based database (unlike MySQL and PostgreSQL) will create a file that represents your database so this step only applies to SQLite.</p>

<h2 id="installing-packages">Installing Packages</h2>

<p>To begin with we will need to ensure we have installed and added the packages we need to our project asd file, there are three that we will be installing:</p>

<ul>
  <li><a href="https://github.com/fukamachi/mito">mito</a></li>
  <li><a href="https://github.com/fukamachi/mito-auth">mito-auth</a></li>
  <li><a href="https://codeberg.org/martianh/ingle/">ingle</a></li>
</ul>

<p>As normal you will need to add them in the <code class="language-plaintext highlighter-rouge">:depends-on</code> section. Please note however that there is an issue in <code class="language-plaintext highlighter-rouge">mito-auth</code> that prevents it from working in MySQL, I have submitted a <a href="https://github.com/fukamachi/mito-auth/pull/5">fix</a> but it has not been merged yet, so for now you can use <a href="https://github.com/nmunro/mito-auth/">my branch</a>, if you do, please ensure you check it out via git into your <code class="language-plaintext highlighter-rouge">quicklisp/local-projects</code> directory.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="ss">:depends-on</span> <span class="p">(</span><span class="ss">:clack</span>
             <span class="ss">:cl-dotenv</span>
             <span class="ss">:djula</span>
             <span class="ss">:cl-forms</span>
             <span class="ss">:cl-forms.djula</span>
             <span class="ss">:cl-forms.ningle</span>
             <span class="ss">:ingle</span>      <span class="c1">;; Add this</span>
             <span class="ss">:mito</span>       <span class="c1">;; Add this</span>
             <span class="ss">:mito-auth</span>  <span class="c1">;; Add this</span>
             <span class="ss">:ningle</span><span class="p">)</span></code></pre></figure>

<p><code class="language-plaintext highlighter-rouge">Mito</code> is a package for managing models/tables in our application, <code class="language-plaintext highlighter-rouge">mito-auth</code> is a <code class="language-plaintext highlighter-rouge">mixin</code> that enables models to have a secure password field, not all models will need this, but our user model will! <code class="language-plaintext highlighter-rouge">ingle</code> is a small library that includes some very useful utilities, one of which is a <code class="language-plaintext highlighter-rouge">redirect</code> function which will be very useful indeed!</p>

<details>
  <summary>Regarding ingle and libmagic on Mac</summary>

  <p>Reader <a href="https://gist.github.com/truedat101">David J. Kordsmeier</a> has reported the need to build and link libmagic on Mac. They have linked a <a href="https://gist.github.com/truedat101/9cb5dfdb71bd5405ef58aa06f4320792">gist</a>. For clarity, if you need to build and link libmagic on Mac.</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>;; This fixes a magic.h not found !

brew reinstall libmagic pkgconf
export CPPFLAGS="-I$(brew --prefix)/include $CPPFLAGS"
export LDFLAGS="-L$(brew --prefix)/lib $LDFLAGS"
export PKG_CONFIG_PATH="$(brew --prefix)/lib/pkgconfig:$PKG_CONFIG_PATH"

;; then run your lisp, and install

(ql:quickload :magicffi)
</code></pre></div>  </div>
</details>

<p>Now that that is done, we must set up the middleware, you might remember from <a href="/2025/01/30/ningle-3.html">Part 3</a> that middleware is placed in the <code class="language-plaintext highlighter-rouge">lack.builder:builder</code> function call in our <code class="language-plaintext highlighter-rouge">start</code> function.</p>

<h2 id="sql-middleware">SQL Middleware</h2>

<p>Mito provides middleware to establish and manage database connections for <code class="language-plaintext highlighter-rouge">SQLite3</code>, <code class="language-plaintext highlighter-rouge">MySQL</code>, and <code class="language-plaintext highlighter-rouge">PostgreSQL</code>, when you build your solution you will need to pick a database implementation, for production systems I suggest <code class="language-plaintext highlighter-rouge">PostgreSQL</code>, but if you are just starting out, you can use <code class="language-plaintext highlighter-rouge">SQLite</code>.</p>

<h3 id="sqlite3">SQLite3</h3>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
      <span class="p">(</span><span class="nv">lack.builder:builder</span>
       <span class="ss">:session</span>
       <span class="o">`</span><span class="p">(</span><span class="ss">:mito</span>
         <span class="p">(</span><span class="ss">:sqlite3</span>
          <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">)))</span>
       <span class="p">(</span><span class="ss">:static</span>
        <span class="ss">:root</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span>
        <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">)</span>
       <span class="vg">*app*</span><span class="p">)</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span></code></pre></figure>

<h3 id="mysql">MySQL</h3>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
      <span class="p">(</span><span class="nv">lack.builder:builder</span>
       <span class="ss">:session</span>
       <span class="o">`</span><span class="p">(</span><span class="ss">:mito</span>
         <span class="p">(</span><span class="ss">:mysql</span>
          <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_DB_NAME"</span><span class="p">)))</span>
          <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_USER"</span><span class="p">)</span>
          <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PASSWORD"</span><span class="p">)</span>
          <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_ADDRESS"</span><span class="p">)</span>
          <span class="ss">:port</span> <span class="o">,</span><span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PORT"</span><span class="p">))))</span>
       <span class="p">(</span><span class="ss">:static</span>
        <span class="ss">:root</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span>
        <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">)</span>
       <span class="vg">*app*</span><span class="p">)</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span></code></pre></figure>

<h3 id="postgresql">PostgreSQL</h3>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defun</span> <span class="nv">start</span> <span class="p">(</span><span class="k">&amp;key</span> <span class="p">(</span><span class="nv">server</span> <span class="ss">:woo</span><span class="p">)</span> <span class="p">(</span><span class="nv">address</span> <span class="s">"127.0.0.1"</span><span class="p">)</span> <span class="p">(</span><span class="nv">port</span> <span class="mi">8000</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:add-template-directory</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/templates/"</span><span class="p">))</span>
    <span class="p">(</span><span class="nv">djula:set-static-url</span> <span class="s">"/public/"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">clack:clackup</span>
      <span class="p">(</span><span class="nv">lack.builder:builder</span>
       <span class="ss">:session</span>
       <span class="o">`</span><span class="p">(</span><span class="ss">:mito</span>
         <span class="p">(</span><span class="ss">:postgres</span>
          <span class="ss">:database-name</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_DB_NAME"</span><span class="p">)))</span>
          <span class="ss">:username</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_USER"</span><span class="p">)</span>
          <span class="ss">:password</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PASSWORD"</span><span class="p">)</span>
          <span class="ss">:host</span> <span class="o">,</span><span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_ADDRESS"</span><span class="p">)</span>
          <span class="ss">:port</span> <span class="o">,</span><span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PORT"</span><span class="p">))))</span>
       <span class="p">(</span><span class="ss">:static</span>
        <span class="ss">:root</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">"src/static/"</span><span class="p">)</span>
        <span class="ss">:path</span> <span class="s">"/public/"</span><span class="p">)</span>
       <span class="vg">*app*</span><span class="p">)</span>
     <span class="ss">:server</span> <span class="nv">server</span>
     <span class="ss">:address</span> <span class="nv">address</span>
     <span class="ss">:port</span> <span class="nv">port</span><span class="p">))</span></code></pre></figure>

<h2 id="testing-the-connection">Testing The Connection</h2>

<p>Before we go further with building models and migration functions, we should test that the connections work and the most basic of SQL statements. We will be working on our register controller, so that seems like as good a place as any to place a simple check.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/register"</span> <span class="ss">:method</span> <span class="o">'</span><span class="p">(</span><span class="ss">:GET</span> <span class="ss">:POST</span><span class="p">))</span>
    <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">query</span> <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="s">"SELECT 2 + 3 AS result"</span><span class="p">)))</span>
            <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Test: ~A~%"</span> <span class="nv">query</span><span class="p">))</span>

        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">form</span> <span class="p">(</span><span class="nv">cl-forms:find-form</span> <span class="ss">'register</span><span class="p">)))</span>
        <span class="o">...</span></code></pre></figure>

<p>Here, in the controller we have added a small (temporary) check to ensure that the database connections are set up correctly, when you run the application and perform a <code class="language-plaintext highlighter-rouge">GET</code> request on this route, you should see the output printed in the console for:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Test: ((RESULT 5))
</code></pre></div></div>

<p>It might look a little odd, but rest assured that this is proof that everything is right and the connection works! We will be removing this later as it serves just as a small check. So with that done, we can begin to look into writing our first model, our user model.</p>

<h2 id="creating-models">Creating Models</h2>

<p>Models are a way to represent both a generic object, and any specific object of that type in a relational database system. For example you might have a Book model, that represents a book table, however a book is just a way to classify something any doesn’t tell you anything specific about any individual book. So here we will create a User model, that refers to all users, but each instance of a User will contain the specific information about any given user.</p>

<p>We will create a new file called <code class="language-plaintext highlighter-rouge">models.lisp</code>:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/models</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:user</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="nv">ningle-tutorial-project/models</span><span class="p">)</span>

<span class="p">(</span><span class="nv">deftable</span> <span class="nv">user</span> <span class="p">(</span><span class="nv">mito-auth:has-secure-password</span><span class="p">)</span>
  <span class="p">((</span><span class="nv">email</span>    <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span> <span class="ss">:initarg</span> <span class="ss">:email</span>    <span class="ss">:accessor</span> <span class="nv">email</span><span class="p">)</span>
   <span class="p">(</span><span class="nv">username</span> <span class="ss">:col-type</span> <span class="p">(</span><span class="ss">:varchar</span> <span class="mi">255</span><span class="p">)</span> <span class="ss">:initarg</span> <span class="ss">:username</span> <span class="ss">:accessor</span> <span class="nv">username</span><span class="p">))</span>
  <span class="p">(</span><span class="ss">:unique-keys</span> <span class="nv">email</span> <span class="nv">username</span><span class="p">))</span></code></pre></figure>

<p>Now, <code class="language-plaintext highlighter-rouge">mito</code> provides a <code class="language-plaintext highlighter-rouge">deftable</code> <code class="language-plaintext highlighter-rouge">macro</code> that hides some of the complexities, there is a way to use a regular class and change the <code class="language-plaintext highlighter-rouge">metaclass</code>, but it’s much less typing and makes the code look nicer to use the <code class="language-plaintext highlighter-rouge">deftable</code> syntax. It is important to note however that we use the <code class="language-plaintext highlighter-rouge">mixin</code> from <code class="language-plaintext highlighter-rouge">mito-auth</code> called <code class="language-plaintext highlighter-rouge">has-secure-password</code>. Obviously this mixin wouldn’t be needed in all of our models, but because we are creating a user that will log into our system, we need to ensure we are handling passwords securely.</p>

<h2 id="writing-migrations">Writing Migrations</h2>

<p>Now that we have this we need to write the migration code I mentioned earlier, databases (and their models) change over time as application requirements change, as columns get added, removed, changed, etc it can be tricky to get right and you certainly would prefer to have these done automatically, a stray SQL query in the wrong connected database can do incredible damage (trust me, I know!), so migrations allow us to track these changes and have the database system manage them for us.</p>

<p>This code will set up connections to the implementation we want to use and delegate migrations to mito, so pick your implementation and place it in <code class="language-plaintext highlighter-rouge">migrations.lisp</code>.</p>

<h3 id="sqlite3-1">SQLite3</h3>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:connect-toplevel</span>
    <span class="ss">:sqlite3</span>
    <span class="ss">:database-name</span> <span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"SQLITE_DB_NAME"</span><span class="p">))))</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:disconnect-toplevel</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span></code></pre></figure>

<h3 id="mysql-1">MySql</h3>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:connect-toplevel</span>
    <span class="ss">:mysql</span>
    <span class="ss">:database-name</span> <span class="p">(</span><span class="nv">uiop:native-namestring</span> <span class="p">(</span><span class="nv">uiop:parse-unix-namestring</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_DB_NAME"</span><span class="p">)))</span>
    <span class="ss">:username</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_USER"</span><span class="p">)</span>
    <span class="ss">:password</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PASSWORD"</span><span class="p">)</span>
    <span class="ss">:host</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_ADDRESS"</span><span class="p">)</span>
    <span class="ss">:port</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"MYSQL_PORT"</span><span class="p">)))</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:disconnect-toplevel</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span></code></pre></figure>

<h3 id="postgresql-1">PostgreSQL</h3>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">defpackage</span> <span class="nv">ningle-tutorial-project/migrations</span>
  <span class="p">(</span><span class="ss">:use</span> <span class="ss">:cl</span> <span class="ss">:mito</span><span class="p">)</span>
  <span class="p">(</span><span class="ss">:export</span> <span class="ss">#:migrate</span><span class="p">))</span>

<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:ningle-tutorial-project/migrations</span><span class="p">)</span>

<span class="p">(</span><span class="nb">defun</span> <span class="nv">migrate</span> <span class="p">()</span>
  <span class="s">"Explicitly apply migrations when called."</span>
  <span class="p">(</span><span class="nv">dotenv:load-env</span> <span class="p">(</span><span class="nv">asdf:system-relative-pathname</span> <span class="ss">:ningle-tutorial-project</span> <span class="s">".env"</span><span class="p">))</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Applying migrations...~%"</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:connect-toplevel</span>
    <span class="ss">:postgres</span>
    <span class="ss">:database-name</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_DB_NAME"</span><span class="p">)</span>
    <span class="ss">:host</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_ADDRESS"</span><span class="p">)</span>
    <span class="ss">:port</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PORT"</span><span class="p">))</span>
    <span class="ss">:username</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_USER"</span><span class="p">)</span>
    <span class="ss">:password</span> <span class="p">(</span><span class="nv">uiop:getenv</span> <span class="s">"POSTGRES_PASSWORD"</span><span class="p">))</span>
  <span class="p">(</span><span class="nv">mito:ensure-table-exists</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:migrate-table</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">mito:disconnect-toplevel</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Migrations complete.~%"</span><span class="p">))</span></code></pre></figure>

<p>It will be necessary to add these two files into the <code class="language-plaintext highlighter-rouge">:components</code> section of your project asd file.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp">  <span class="ss">:components</span> <span class="p">((</span><span class="ss">:module</span> <span class="s">"src"</span>
                <span class="ss">:components</span>
                <span class="p">((</span><span class="ss">:file</span> <span class="s">"models"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"migrations"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"forms"</span><span class="p">)</span>
                 <span class="p">(</span><span class="ss">:file</span> <span class="s">"main"</span><span class="p">))))</span></code></pre></figure>

<p>Just remember if you are using MySQL or PostgreSQL, you will need to ensure that the database you want to connect to exists (in our case ntp), and that your connecting user has the correct permissions!</p>

<h2 id="running-migrations">Running Migrations</h2>

<p>Now that everything is set up, we will need to perform our initial migrations:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">ningle-tutorial-project/migrations:migrate</span><span class="p">)</span></code></pre></figure>

<p>If this has worked, you will see a lot of output SQL statements, it’s quite verbose, however this only means that it is working and we can move onto actually creating and saving models.</p>

<h2 id="removing-connection-check">Removing Connection Check</h2>

<p>Now that we have migrations and models working we should remember to remove this verification code that we wrote earlier.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">query</span> <span class="p">(</span><span class="nv">mito:retrieve-by-sql</span> <span class="s">"SELECT 2 + 3 AS result"</span><span class="p">)))</span>
    <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Test: ~A~%"</span> <span class="nv">query</span><span class="p">))</span></code></pre></figure>

<h2 id="registering--querying-users">Registering &amp; Querying Users</h2>

<p>What we are going to do now is use the user register form and connect it to our database, because we are registering users we will have to do some checks to ensure since we stated that usernames and email addresses are unique, we might want to raise an error.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
    <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">username</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
        <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span>
                                 <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                                             <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
                            <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span></code></pre></figure>

<p>We can see from this snippet here that mito uses the SXQL <a href="https://en.wikipedia.org/wiki/Domain-specific_language">Domain Specific Language</a> for expressing SQL statements. Using the <code class="language-plaintext highlighter-rouge">select-dao</code> we can query the user table and apply <code class="language-plaintext highlighter-rouge">where</code> clauses using a more Lispy like syntax to check to see if an account with the username or email already exists. Such DSLs are common when interacting with SQL inside another programming language, but it’s good to know that from what we learned earlier that it can handle arbitrary SQL strings or this more Lispy syntax, so you can use pure SQL syntax, if necessary.</p>

<p>While having this check isn’t necessary, it does make the error handling somewhat nicer, as well as exploring parts of the mito api. We will also add a check to raise an error if the passwords submitted in the form do not match each other.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span></code></pre></figure>

<p>If both of these pass (and you can test different permutations of course), we can continue to using mito to create our first user object!</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span>
                                           <span class="ss">:email</span> <span class="nv">email</span>
                                           <span class="ss">:username</span> <span class="nv">username</span>
                                           <span class="ss">:password</span> <span class="nv">password</span><span class="p">)</span></code></pre></figure>

<p>The final thing to add is that we should redirect to another route, which we can do with the <code class="language-plaintext highlighter-rouge">ingle:redirect</code> function.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/people"</span><span class="p">)</span></code></pre></figure>

<p>You will notice that we are redirecting to a route that doesn’t (yet) exist, we will write the controller below after we have finished this controller, the <code class="language-plaintext highlighter-rouge">multiple-value-bind</code> section of which, when completed, looks like this:</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">multiple-value-bind</span> <span class="p">(</span><span class="nv">valid</span> <span class="nv">errors</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">cl-forms:validate-form</span> <span class="nv">form</span><span class="p">)</span>
    <span class="p">(</span><span class="nb">when</span> <span class="nv">errors</span>
        <span class="p">(</span><span class="nb">format</span> <span class="no">t</span> <span class="s">"Errors: ~A~%"</span> <span class="nv">errors</span><span class="p">))</span>

    <span class="p">(</span><span class="nb">when</span> <span class="nv">valid</span>
        <span class="p">(</span><span class="nv">cl-forms:with-form-field-values</span> <span class="p">(</span><span class="nv">email</span> <span class="nv">username</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span> <span class="nv">form</span>
            <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nv">mito:select-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span>
                    <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">username</span><span class="p">)</span>
                                <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">email</span><span class="p">))))</span>
                <span class="p">(</span><span class="nb">error</span> <span class="s">"Either username or email is already registered"</span><span class="p">))</span>

            <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">string/=</span> <span class="nv">password</span> <span class="nv">password-verify</span><span class="p">)</span>
                <span class="p">(</span><span class="nb">error</span> <span class="s">"Passwords do not match"</span><span class="p">))</span>

            <span class="p">(</span><span class="nv">mito:create-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span>
                <span class="ss">:email</span> <span class="nv">email</span>
                <span class="ss">:username</span> <span class="nv">username</span>
                <span class="ss">:password</span> <span class="nv">password</span><span class="p">)</span>
        <span class="p">(</span><span class="nv">ingle:redirect</span> <span class="s">"/people"</span><span class="p">))))</span></code></pre></figure>

<h2 id="getting-all-users">Getting All Users</h2>

<p>We will look at two final examples of using mito before we finish this tutoral, as mentioned earlier we will write a new <code class="language-plaintext highlighter-rouge">/people</code> controller, which will list all the users registered in the system, and we will create a <code class="language-plaintext highlighter-rouge">/people/:person</code> controller to show the details of an individual user.</p>

<p>Starting with the <code class="language-plaintext highlighter-rouge">/people</code> controller, we create a controller like we have seen before, we then use a <code class="language-plaintext highlighter-rouge">let</code> binding to store the result of <code class="language-plaintext highlighter-rouge">(mito:retrieve-dao 'ningle-tutoral-project/model:user)</code>, this is how we would get all rows from a table represented by the class <code class="language-plaintext highlighter-rouge">'ningle-tutorial-project/models:user</code>. We then pass the results into a template.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nv">users</span> <span class="p">(</span><span class="nv">mito:retrieve-dao</span> <span class="ss">'ningle-tutorial-project/models:user</span><span class="p">)))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"people.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"People"</span> <span class="ss">:users</span> <span class="nv">users</span><span class="p">))))</span></code></pre></figure>

<p>The html for this is written as such:</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span>    <span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            {% for user in users %}
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card"</span><span class="nt">&gt;</span>
                            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                                <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title"</span><span class="nt">&gt;&lt;a</span> <span class="na">href=</span><span class="s">"/people/{{ user.username }}"</span><span class="nt">&gt;</span>{{ user.username }}<span class="nt">&lt;/a&gt;&lt;/h5&gt;</span>
                                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-text"</span><span class="nt">&gt;&lt;a</span> <span class="na">href=</span><span class="s">"/people/{{ user.email }}"</span><span class="nt">&gt;</span>{{ user.email }}<span class="nt">&lt;/a&gt;&lt;/p&gt;</span>
                                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted small"</span><span class="nt">&gt;&lt;/p&gt;</span>
                            <span class="nt">&lt;/div&gt;</span>
                        <span class="nt">&lt;/div&gt;</span>
                    <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            {% endfor %}
            {% if not users %}
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col text-center"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted"</span><span class="nt">&gt;</span>No users to display.<span class="nt">&lt;/p&gt;</span>
                    <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            {% endif %}
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}</code></pre></figure>

<h2 id="getting-a-single-user">Getting A Single User</h2>

<p>In our individual person view, we see how a route may have variable data, our <code class="language-plaintext highlighter-rouge">:person</code> component of the URL string, this will be either a username or email, it doesn’t really matter which as we can have a SQL query that will find a record that will match the <code class="language-plaintext highlighter-rouge">:person</code> string with either the username or email. We also take advantage of another <code class="language-plaintext highlighter-rouge">ingle</code> function, the <code class="language-plaintext highlighter-rouge">get-param</code>, which will get the value out of <code class="language-plaintext highlighter-rouge">:person</code>. We use a <code class="language-plaintext highlighter-rouge">let*</code> binding to store the <code class="language-plaintext highlighter-rouge">user</code> derived from <code class="language-plaintext highlighter-rouge">:person</code> and the result of <code class="language-plaintext highlighter-rouge">mito:select-dao</code> (using the <code class="language-plaintext highlighter-rouge">person</code>), we then pass the <code class="language-plaintext highlighter-rouge">user</code> object into a template.</p>

<p>As we saw before this query was used to check for the existence of a username or email address in our <code class="language-plaintext highlighter-rouge">register</code> controller.</p>

<figure class="highlight"><pre><code class="language-common_lisp" data-lang="common_lisp"><span class="p">(</span><span class="nb">setf</span> <span class="p">(</span><span class="nv">ningle:route</span> <span class="vg">*app*</span> <span class="s">"/people/:person"</span><span class="p">)</span>
      <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">params</span><span class="p">)</span>
        <span class="p">(</span><span class="k">let*</span> <span class="p">((</span><span class="nv">person</span> <span class="p">(</span><span class="nv">ingle:get-param</span> <span class="ss">:person</span> <span class="nv">params</span><span class="p">))</span>
               <span class="p">(</span><span class="nv">user</span> <span class="p">(</span><span class="nb">first</span> <span class="p">(</span><span class="nv">mito:select-dao</span>
                              <span class="ss">'ningle-tutorial-project/models:user</span>
                              <span class="p">(</span><span class="nv">where</span> <span class="p">(</span><span class="ss">:or</span> <span class="p">(</span><span class="ss">:=</span> <span class="ss">:username</span> <span class="nv">person</span><span class="p">)</span>
                                          <span class="p">(</span><span class="ss">:=</span> <span class="ss">:email</span> <span class="nv">person</span><span class="p">)))))))</span>
          <span class="p">(</span><span class="nv">djula:render-template*</span> <span class="s">"person.html"</span> <span class="no">nil</span> <span class="ss">:title</span> <span class="s">"Person"</span> <span class="ss">:user</span> <span class="nv">user</span><span class="p">))))</span></code></pre></figure>

<p>And here is the template for an individual user.</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html">{% extends "base.html" %}

{% block content %}
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-12"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row mb-4"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col"</span><span class="nt">&gt;</span>
                    {% if not user %}
                        <span class="nt">&lt;h1&gt;</span>No users<span class="nt">&lt;/h1&gt;</span>
                    {% else %}
                        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card"</span><span class="nt">&gt;</span>
                            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card-body"</span><span class="nt">&gt;</span>
                                <span class="nt">&lt;h5</span> <span class="na">class=</span><span class="s">"card-title"</span><span class="nt">&gt;</span>{{ user.username }}<span class="nt">&lt;/h5&gt;</span>
                                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"card-text"</span><span class="nt">&gt;</span>{{ user.email }}<span class="nt">&lt;/p&gt;</span>
                                <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-muted small"</span><span class="nt">&gt;&lt;/p&gt;</span>
                            <span class="nt">&lt;/div&gt;</span>
                        <span class="nt">&lt;/div&gt;</span>
                    {% endif %}
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
{% endblock %}</code></pre></figure>

<h2 id="conclusion">Conclusion</h2>

<p>This was a rather large chapter and we covered a lot, looking at the different means by which to connect to a SQL database, defining models, running migrations and executing queries, of course we are just getting started but this is a massive step forward and our application is beginning to take shape. I certainly hope you have enjoyed it and found it useful!</p>

<p>To recap, after working your way though this tutorial you should be able to:</p>

<ul>
  <li>Explain what a model is</li>
  <li>Explain what a migration is</li>
  <li>Write code to connect to a SQL database</li>
  <li>Implement a model</li>
  <li>Implement a migration</li>
  <li>Execute a migration</li>
  <li>Write controllers that write information to a database via a model</li>
  <li>Write controllers that read information from a database via a model</li>
</ul>

<h2 id="github">Github</h2>

<ul>
  <li>The link for the SQLite version of this tutorial code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-6-sqlite">here</a>.</li>
  <li>The link for the MySQL version of this tutorial code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-6-mysql">here</a>.</li>
  <li>The link for the PostgreSQL version of this tutorial code is available <a href="https://github.com/nmunro/ningle-tutorial-project/releases/tag/tutorial-6-postgresql">here</a>.</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="http://clhs.lisp.se/Body/f_car_c.htm#car">car</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Domain-specific_language">Domain Specific Language</a></li>
  <li><a href="http://clhs.lisp.se/Body/f_firstc.htm#first">first</a></li>
  <li><a href="https://codeberg.org/martianh/ingle/">ingle</a></li>
  <li><a href="http://clhs.lisp.se/Body/m_defmac.htm">macro</a></li>
  <li><a href="https://github.com/fukamachi/mito">mito</a></li>
  <li><a href="https://github.com/fukamachi/mito-auth">mito-auth</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Mixin">mixin</a></li>
  <li><a href="https://www.mysql.com/">MySQL</a></li>
  <li><a href="https://www.postgresql.org/">PostgreSQL</a></li>
  <li><a href="https://sqlite.org/index.html">SQLite3</a></li>
</ul>]]></content><author><name>NMunro</name></author><category term="CommonLisp" /><category term="Lisp" /><category term="tutorial" /><category term="YouTube" /><category term="web" /><category term="dev" /><summary type="html"><![CDATA[Contents]]></summary></entry></feed>