<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr_FR"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://2metz.fr/blog/atom.xml" rel="self" type="application/atom+xml" /><link href="https://2metz.fr/" rel="alternate" type="text/html" hreflang="fr_FR" /><updated>2026-06-20T23:21:10+02:00</updated><id>https://2metz.fr/blog/atom.xml</id><title type="html">2metz</title><entry xml:lang="fr"><title type="html">Récupérer tous ses avis</title><link href="https://2metz.fr/blog/recuperer-tous-avis/" rel="alternate" type="text/html" title="Récupérer tous ses avis" /><published>2026-06-20T00:00:00+02:00</published><updated>2026-06-20T00:00:00+02:00</updated><id>https://2metz.fr/blog/recuperer-tous-avis</id><content type="html" xml:base="https://2metz.fr/blog/recuperer-tous-avis/"><![CDATA[<p>J’ai récemment amélioré ma page web d’<a href="https://2maker.fr">artisan électricien sur
Annecy</a>, et je voulais récupérer l’ensemble de mes avis sur
les différentes plateformes. Le sujet est pénible, car tout est bien
siloté.</p>

<p>J’ai 3 endroits à récupérer, <a href="https://www.allovoisins.com/p/2metz">AlloVoisins</a>, <a href="https://www.bilik.fr/presentations/2850-2metz-maker">Bilik</a> et <a href="https://maps.app.goo.gl/8D3bmnyc5z1gPLqU8">Google</a>.</p>

<p><a href="https://www.allovoisins.com/p/2metz">AlloVoisins</a> ne permet de ne récupérer que 10 commentaires en mode non
connecté. Une fois connecté en revanche, on peut récupérer les avis
via une API privée avec un jeton d’authentification.</p>

<p>Solution: utiliser <a href="https://playwright.dev/">playwright</a> pour me connecter et faire
les requêtes vers l’API.</p>

<p><a href="https://www.bilik.fr/presentations/2850-2metz-maker">Bilik</a> est similaire à AlloVoisins. L’API privée est cette fois du
graphql pour changer un peu.</p>

<p>Et Google ? L’API place publique ne permet de récupérer que 5
commentaires. Mais il existe une API spéciale Business Profile API. Il faut
<a href="https://developers.google.com/my-business/content/prereqs#request-access">demander l’accès et avoir (peut être) l’accès au bout de 10 jours</a>.</p>

<p>J’ai demandé l’accès, sans succès pour le moment. Pas d’avis Google
pour le moment.</p>

<p>Le code est encapsulé dans une
<a href="https://www.bridgetownrb.com/docs/automations">automatisation</a> de
<a href="https://www.bridgetownrb.com/">bridgetown</a>, le générateur du site.</p>

<p><img integrity="sha512-0ZPOE6oAccfXzCQAwSzAEfVgJ2a+rVtRBOkhyid/n3GrM46TtaLW9fgldNW2pLMPE/CUgEWmW134hSRP2regfA==" crossorigin="anonymous" src="/assets/blog/avis-bridgetown-d193ce13aa0071c7d7cc2400c12cc011f5602766bead5b5104e921ca277f9f71ab338e93b5a2d6f5f82574d5b6a4b30f13f0948045a65b5df885244fdab7a07c.png" /></p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[J’ai récemment amélioré ma page web d’artisan électricien sur Annecy, et je voulais récupérer l’ensemble de mes avis sur les différentes plateformes. Le sujet est pénible, car tout est bien siloté.]]></summary></entry><entry xml:lang="fr"><title type="html">Un bug websocket avec Phoenix et Traefik en production</title><link href="https://2metz.fr/blog/phoenix-websocket-et-traefik/" rel="alternate" type="text/html" title="Un bug websocket avec Phoenix et Traefik en production" /><published>2026-03-16T00:00:00+01:00</published><updated>2026-03-16T00:00:00+01:00</updated><id>https://2metz.fr/blog/phoenix-websocket-et-traefik</id><content type="html" xml:base="https://2metz.fr/blog/phoenix-websocket-et-traefik/"><![CDATA[<p>Il y a des années, au moment du premier déploiement d’un projet avec le framework <a href="https://www.phoenixframework.org/">Phoenix</a> sur mon serveur j’avais rencontré un problème avec les websockets.
Les requêtes étaient systématiquement redirigées vers la même url en https.</p>

<p>Je n’y avais pas prêté particulièrement attention. Ma solution avait été de désactiver la redirection http -&gt; https coté Phoenix, ceci étant déjà réalisé coté <a href="https://github.com/traefik/traefik">Traefik</a>, mon reverse proxy.</p>

<p>Les années passent et les problèmes cachés sous le tapis finissent par revenir. En voulant déployer une instance de <a href="https://github.com/nerves-hub/nerves_hub_web">Nerves Hub</a>, je suis tombé a nouveau sur le problème. J’ai décidé cette fois d’enquêter.</p>

<p>Je me suis douté tout de suite que le problème soit dû au fait de forcer https sur l’instance. Lorsque c’est une requête https classique ca passe, mais websocket non.</p>

<ol>
  <li>Désactiver la redirection http -&gt; https coté Traefik: Problème toujours présent.</li>
  <li>Chercher sur les forums des problèmes similaires: j’en trouve 2, mais sans solution satisfaisante.
    <ul>
      <li><a href="https://elixirforum.com/t/plug-ssl-redirects-socket-connection-problem-301/25347">https://elixirforum.com/t/plug-ssl-redirects-socket-connection-problem-301/25347</a></li>
      <li><a href="https://elixirforum.com/t/force-ssl-fails-to-upgrade-websocket-but-manual-plug-ssl-plug-works-fine/73990">https://elixirforum.com/t/force-ssl-fails-to-upgrade-websocket-but-manual-plug-ssl-plug-works-fine/73990</a></li>
    </ul>
  </li>
  <li>Lire la doc et le code du <a href="https://github.com/elixir-plug/plug/blob/main/lib/plug/rewrite_on.ex">Plug.RewriteOn</a> qui s’occupe de la logique. Relire la doc de Traefik pour vérifier qu’il envoie bien l’entête http <code class="language-plaintext highlighter-rouge">X-Forwarded-Proto</code>. La réponse est oui.</li>
  <li>Inspecter les entêtes https fournis par le serveur. Je suis tombé sur le projet <a href="https://github.com/mendhak/docker-http-https-echo">docker-http-https-echo</a> qui est juste parfait pour cela.
Je le lance sur mon serveur:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="se">\</span>
  run  <span class="se">\</span>
  <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--network</span> traefik <span class="se">\</span>
  <span class="nt">--label</span> traefik.enable<span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nt">--label</span> <span class="s1">'traefik.http.routers.test.rule=Host(`test.2metz.fr`)'</span> <span class="se">\</span>
  mendhak/http-https-echo:39
</code></pre></div>    </div>

    <p>En effectuant ma requête websocket, je me rends compte que la valeur de <code class="language-plaintext highlighter-rouge">X-Forwarded-Proto</code> est <code class="language-plaintext highlighter-rouge">wss</code>.</p>

    <p>Je tombe sur l’issue de Traefik qui parle justement de cette valeur pas très standard <a href="https://github.com/traefik/traefik/issues/6388">https://github.com/traefik/traefik/issues/6388</a></p>
  </li>
</ol>

<p>A ce moment la, je pense avoir trouvé la source du problème. La valeur <code class="language-plaintext highlighter-rouge">wss</code> n’est pas attendu par <code class="language-plaintext highlighter-rouge">Plug.RewriteOn</code>, et redirige donc vers la version https pensant que c’est une requêtes non chiffré. Reste à trouver une solution.</p>

<p>Je tente alors de forcer la valeur de l’entête <code class="language-plaintext highlighter-rouge">X-Forwarded-Proto</code> a <code class="language-plaintext highlighter-rouge">https</code> par Traefik:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  docker <span class="se">\</span>
    run <span class="se">\</span>
    <span class="nt">--rm</span> <span class="se">\</span>
    <span class="nt">--network</span> traefik <span class="se">\</span>
    <span class="nt">--label</span> traefik.http.routers.test.middlewares<span class="o">=</span><span class="nb">test</span>@docker <span class="se">\</span>
    <span class="nt">--label</span> traefik.http.middlewares.test.headers.customrequestheaders.X-Forwarded-Proto<span class="o">=</span>https <span class="se">\</span>
    <span class="nt">--label</span> traefik.enable<span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
    <span class="nt">--label</span> traefik.http.routers.test.rule<span class="o">=</span><span class="s1">'Host(`test.2metz.fr`)'</span> <span class="se">\</span>
    mendhak/http-https-echo:39
</code></pre></div></div>

<p>Et cela fonctionne ! Mon instance Nerves Hub fonctionnelle, je peux désormais proposer un patch <a href="https://github.com/elixir-plug/plug/pull/1302/">https://github.com/elixir-plug/plug/pull/1302/</a>.</p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[Il y a des années, au moment du premier déploiement d’un projet avec le framework Phoenix sur mon serveur j’avais rencontré un problème avec les websockets. Les requêtes étaient systématiquement redirigées vers la même url en https.]]></summary></entry><entry xml:lang="fr"><title type="html">Des nouvelles de tiime-carddav</title><link href="https://2metz.fr/blog/tiime-carddav-release/" rel="alternate" type="text/html" title="Des nouvelles de tiime-carddav" /><published>2026-02-18T00:00:00+01:00</published><updated>2026-02-18T00:00:00+01:00</updated><id>https://2metz.fr/blog/tiime-carddav-release</id><content type="html" xml:base="https://2metz.fr/blog/tiime-carddav-release/"><![CDATA[<p>Pour faire suite a la première version de <a href="/blog/tiime-carddav-server/">tiime-carddav</a>, un certains nombres de fonctionnalités ont été rajoutés:</p>

<p>Désormais chaque contact d’un client a droit a sa vcard. En conséquence si un client n’a pas de contact, aucune entrée n’est affichée.</p>

<p>Les contacts des clients professionnels sont traités un peu différemment. Une vcard de type <code class="language-plaintext highlighter-rouge">group</code> est créé et les contacts du client y sont associés. Cela permet de retrouver rapidement les contacts d’une entreprise particulière.</p>

<p>Un contact spécial, avec le nom de la société, a été rajouté pour l’envoi des factures ou justificatifs dans Tiime. Fini le copier-coller depuis l’interface web, vous pouvez faire suivre les factures en 2 clics.</p>

<p>En plus de cela quelques corrections ont été faites:</p>

<ul>
  <li>les clients archivés ne sont plus affichés</li>
  <li>le champ etag a été correctement rempli pour éviter des problèmes de cache pour certains clients</li>
</ul>

<p>Le code est ici: <a href="https://github.com/francois2metz/tiime-carddav">https://github.com/francois2metz/tiime-carddav</a></p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[Pour faire suite a la première version de tiime-carddav, un certains nombres de fonctionnalités ont été rajoutés:]]></summary></entry><entry xml:lang="fr"><title type="html">Un serveur carddav pour Tiime</title><link href="https://2metz.fr/blog/tiime-carddav-server/" rel="alternate" type="text/html" title="Un serveur carddav pour Tiime" /><published>2025-12-23T00:00:00+01:00</published><updated>2025-12-23T00:00:00+01:00</updated><id>https://2metz.fr/blog/tiime-carddav-server</id><content type="html" xml:base="https://2metz.fr/blog/tiime-carddav-server/"><![CDATA[<p>En tant qu’artisan depuis un an, j’ai été confronté a un problème récurrent: la gestion de mes contacts clients. Une partie de mes contacts clients se font directement par téléphone et une autre via messagerie (email ou via <a href="https://www.bilik.fr/">Bilik</a>). J’ai tenté de garder une certaine habitude de rajouter en tant que contacts mes clients dans mon téléphone, ce qui permet de m’y retrouver facilement. Dû a mon manque de rigueur sur le sujet, je pouvais facilement oublier de le faire.</p>

<p>Une fois le devis réalisé, ma base de données de référence de mes clients est <a href="https://tiime.fr/">Tiime</a>. J’ai toutes les informations sur ceux-ci dessus, le nom, adresse, email et téléphone. Ces informations sont nécessaires pour avoir un devis correct, mais aussi pour que la <a href="/blog/signature-electronique-devis-tiime/">signature électronique que je propose fonctionne</a>.</p>

<p>Bref, tout cela pour dire que en cherchant une solution a ce problème, la solution technique la plus simple et la plus universelle était d’exposer un serveur carddav de mes clients à partir de Tiime.
De cette façon je peux les récupérer sur mon téléphone (via <a href="https://www.davx5.com/">DAVx5</a>) ou sur mon ordinateur (via vdirsyncer). J’ai réutilisé le client go que j’avais développé pour le plugin steampipe, et fait la logique nécessaire pour avoir un serveur carddav fonctionnel.</p>

<p>Comme j’aime bien m’amuser, j’ai géré les multiples sociétés dans Tiime (sous forme de carnet d’adresses séparé) et il peut gérer n’importe quel compte Tiime, pour peu que les données d’authentification soient correctes.</p>

<p>De cette façon, je peux envoyer un email a un client sur mon ordinateur, savoir qu’il m’appelle ou encore lancer la navigation sur mon téléphone portable.</p>

<p>Le code est ici: <a href="https://github.com/francois2metz/tiime-carddav">https://github.com/francois2metz/tiime-carddav</a></p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[En tant qu’artisan depuis un an, j’ai été confronté a un problème récurrent: la gestion de mes contacts clients. Une partie de mes contacts clients se font directement par téléphone et une autre via messagerie (email ou via Bilik). J’ai tenté de garder une certaine habitude de rajouter en tant que contacts mes clients dans mon téléphone, ce qui permet de m’y retrouver facilement. Dû a mon manque de rigueur sur le sujet, je pouvais facilement oublier de le faire.]]></summary></entry><entry xml:lang="fr"><title type="html">Siign: Faites signer vos devis Tiime électroniquement</title><link href="https://2metz.fr/blog/signature-electronique-devis-tiime/" rel="alternate" type="text/html" title="Siign: Faites signer vos devis Tiime électroniquement" /><published>2024-10-10T00:00:00+02:00</published><updated>2024-10-10T00:00:00+02:00</updated><id>https://2metz.fr/blog/signature-electronique-devis-tiime</id><content type="html" xml:base="https://2metz.fr/blog/signature-electronique-devis-tiime/"><![CDATA[<p>En cette rentrée 2024, j’ai fait évoluer mon activité principale, de développeur freelance à <a href="https://2maker.fr">électricien et menuisier dans la région d’Annecy</a>.</p>

<p>J’ai été confronté à la nécessité de faire parvenir les devis a mes clients, et faire signer ceux-ci sans frictions.</p>

<p>J’avais quelques contraintes:</p>

<ul>
  <li>pour mon suivi de facturation, je voulais continuer a utilisé <a href="https://tiime.fr/">Tiime</a>, le logiciel de comptabilité fourni par mon cabinet comptable</li>
  <li>étant en vélo, je ne voulais pas d’imprimante pour faire les signer directement sur place</li>
  <li>pour limiter la friction, je ne voulais pas forcer mes clients à avoir une imprimante ou utiliser un logiciel tiers d’édition PDF</li>
  <li>pour ne pas avoir de problèmes légaux, je voulais être irréprochable sur la signature du devis</li>
</ul>

<p>Tiime étant un logiciel mature, une application mobile <a href="https://tiime.fr/">Tiime</a> est disponible qui me permet de faire mes devis facilement et de les envoyer par email. Mais la signature restait le principal problème.</p>

<p>Comme partenaire de signature électronique, j’ai sélectionné <a href="https://www.docage.com/">Docage</a>. Étonnement, c’est le service qui me permettait d’utiliser l’API de signature sans avoir a payé un prix conséquent par mois. Docage a un cout mensuel par utilisateur et par devis signé modique.</p>

<p>Mais préparer la transaction (dans le jargon de Docage) est un peu chronophage et redondant par rapport au devis, et Tiime ne proposant pas ce service nativement, j’ai réalisé l’intégration qu’il me fallait.</p>

<p>Et c’est ainsi que Siign est né. Ce service s’interface avec Tiime et Docage et permet de:</p>

<ul>
  <li>lister les devis du compte Tiime</li>
  <li>créer la transaction Docage en 1 clic (informations du client, devis, etc …) en reprenant les informations de Tiime</li>
  <li>partager le lien de la transaction Docage au client</li>
  <li>faire signer le client dans une interface en marque blanche</li>
  <li>mettre à jour le devis dans Tiime une fois que celui-ci est signé (ou refusé, expiré, …)</li>
  <li>recevoir une notification SMS avec le nouveau statut du devis</li>
</ul>

<p>Il est bien sur toujours possible pour le client de procéder d’une manière plus ancestrale, en téléchargeant le devis et en le signant via impression ou en utilisant un logiciel d’édition PDF pour opposer sa signature.</p>

<p>Siign a été utilisé par mes premiers clients assez naturellement, je suis pour l’instant assez content du résultat.</p>

<p>Si cela vous intéresse je serais ravi de déployer un tel service pour vous. L’instance est déployée sur <a href="https://signature.2metz.fr">signature.2metz.fr</a>.</p>

<p>Bien sur, le code est libre (licence AGPL v3) et disponible sur GitHub: <a href="https://github.com/francois2metz/siign">github.com/francois2metz/siign</a>.</p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[En cette rentrée 2024, j’ai fait évoluer mon activité principale, de développeur freelance à électricien et menuisier dans la région d’Annecy.]]></summary></entry><entry xml:lang="en"><title type="html">Vue 3 integration of maplibre-gl</title><link href="https://2metz.fr/blog/vue-maplibre/" rel="alternate" type="text/html" title="Vue 3 integration of maplibre-gl" /><published>2024-05-29T00:00:00+02:00</published><updated>2024-05-29T00:00:00+02:00</updated><id>https://2metz.fr/blog/vue-maplibre</id><content type="html" xml:base="https://2metz.fr/blog/vue-maplibre/"><![CDATA[<p>I’ve been slowy working on indoor= maintenance while adding some new features. Part of the maintenance work was to upgrade the <a href="https://indoorequal.org">indoorequal.org</a> website from vue 2 to vue 3.</p>

<p>One of the challenge was to find a replacement of <a href="https://github.com/soal/vue-mapbox/">vue-mapbox</a> library I was using. I quickly found <a href="https://github.com/razorness/vue-maplibre-gl">vue-maplibre-gl</a> that is mostly a drop-in replacement. Some features were missing so I forked the project to add them. While at it, I added documentation, updated dependencies, removed some features, etc …</p>

<p>The fork is published on npm at <a href="https://www.npmjs.com/package/@indoorequal/vue-maplibre-gl">@indoorequal/vue-maplibre-gl</a> and the <a href="https://indoorequal.github.io/vue-maplibre-gl/">documentation is available</a>.</p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[I’ve been slowy working on indoor= maintenance while adding some new features. Part of the maintenance work was to upgrade the indoorequal.org website from vue 2 to vue 3.]]></summary></entry><entry xml:lang="en"><title type="html">Monitoring the drift of our dependencies</title><link href="https://2metz.fr/blog/follow-dependency-drift/" rel="alternate" type="text/html" title="Monitoring the drift of our dependencies" /><published>2023-12-04T00:00:00+01:00</published><updated>2023-12-04T00:00:00+01:00</updated><id>https://2metz.fr/blog/follow-dependency-drift</id><content type="html" xml:base="https://2metz.fr/blog/follow-dependency-drift/"><![CDATA[<h2 id="the-observation">The observation</h2>

<p>Monitoring our software dependencies has never been a subject followed by Pix. The updates were therefore made by a few people.</p>

<p>In the past, a small team had been created to update Ember on our client applications. It works well in a swing, but does not solve the follow-up problem.</p>

<p>That is why <a href="https://www.mend.io/renovate/">Renovate</a> was set up. Renovate is present on all our repositories and constantly pushes us with updates to our dependencies. With the <a href="https://docs.renovatebot.com/key-concepts/dashboard/">Dependency dashboard</a> (<a href="https://github.com/1024pix/pix/issues/5637">see 1024pix/pix</a>), we can have the different updates on hold. This gives us an idea of the delay, but it is still very rude.</p>

<p>That is why, at the Tech Days 2023 (technical work done during the summer), a team was tasked with putting effort back on addiction updates. The job has been to switch to node 18 on our main app and configure Renovate so that everything runs.</p>

<p>The need to quantify in a common way the delay in updating dependencies on the various projects has led us to interest in an interesting metric, <a href="https://libyear.com/">libyear</a>.</p>

<h2 id="libyear-to-the-rescue">Libyear to the rescue</h2>

<p>In short, <a href="https://libyear.com/">libyear</a> makes it possible to quantify the age of all dependencies.</p>

<p>By calculating this metric on our different projects, we have a comparable measure to see and decide on the updating actions. That’s why we’ve developed <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker">dependency-drift-tracker</a> that allows us to know the current drift of our dependencies, see evolution and compare that delay with others.</p>

<p>In addition to the delay in our dependencies, we also show the measurement in years since the last release, called Pulse. This makes it possible to identify dependencies that have become obsolete or devalued. This requires a slightly more manual analysis.</p>

<h2 id="what-does-it-look-like">What does it look like?</h2>

<p><img integrity="sha512-UZiGiiFHs/fAd4R91onpxvfO3t9/doK+4ViqMA5ceTulUQ5+v4aVVFcv/iWD8WtvpT5JiiyfGj/amD62Eare7w==" crossorigin="anonymous" src="/assets/blog/dependency-drift-tracker-5198868a2147b3f7c077847dd689e9c6f7cededf7f7682bee158aa300e5c793ba5510e7ebf869554572ffe2583f16b6fa53e498a2c9f1a3fda983eb611aadeef.png" /></p>

<p>On the left, in the blink of an eye, we see the deposits followed with the information of the delay. Then in the main panel, 2 metrics are displayed, the delay and the pulse.</p>

<p>The following 2 graphs make it possible to monitor the evolution of these figures over time.</p>

<p>Finally, the final table displays the result of the last launch of libyear with the information of individual dependencies.</p>

<p>The data are refreshed every day. A delay that is regular enough to enhance the work done the day before, and to plan the rest.</p>

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

<p>Making our delay visible has enabled us to improve on the tracking of versions. Seeing the decrease in the curve after an update is always a pleasure. Even if, in a continuous way and does it seem inexorable, the curve goes back as a result of the updates of the dozens of dependencies used by our applications.</p>

<p>Curious, here’s the follow-up <a href="https://1024pix.github.io/dependency-drift-tracker/">of the Pix applications</a>.</p>

<h2 id="try-it">Try it</h2>

<p>We made the generic tracking code usable by any javascript project. You can easily manage tracking files manually via <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker">dependency-drift-tracker</a>, or even more simply with the GitHub <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker-action">dependency-drift-tracker-action</a> action.</p>

<p>And read the <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker?tab=readme-ov-file#usage">documentation</a>.</p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[The observation]]></summary></entry><entry xml:lang="fr"><title type="html">Suivi de la dérive de nos dépendances</title><link href="https://engineering.pix.fr/2023/11/27/suivre-la-derive-de-nos-dependances.html" rel="alternate" type="text/html" title="Suivi de la dérive de nos dépendances" /><published>2023-11-27T00:00:00+01:00</published><updated>2023-11-27T00:00:00+01:00</updated><id>https://engineering.pix.fr/2023/11/27/suivre-la-derive-de-nos-dependances</id><content type="html" xml:base="https://engineering.pix.fr/2023/11/27/suivre-la-derive-de-nos-dependances.html"><![CDATA[<h2 id="le-constat">Le constat</h2>

<p>Le suivi de nos dépendances logiciels n’a jamais été un sujet suivi chez Pix. Les mises à jour se sont fait donc au bon vouloir de quelques personnes.</p>

<p>Dans le passé, une petite équipe avait été créée pour mettre à jour Ember sur nos applications clientes. Cela fonctionne bien par à-coups, mais ne règle pas le problème de suivi.</p>

<p>C’est pourquoi <a href="https://www.mend.io/renovate/">Renovate</a> a été mis en place. Renovate est présent sur tous nos dépôts et nous pousse en permanence des mises à jour de nos dépendances. Avec le “<a href="https://docs.renovatebot.com/key-concepts/dashboard/">Dependency dashboard</a>” (<a href="https://github.com/1024pix/pix/issues/5637">voir celui de 1024pix/pix</a>), nous pouvons avoir les différentes mises à jour en attente. Cela nous donne une idée du retard mais cela reste très grossier.</p>

<p>C’est pourquoi lors des Tech Days 2023 (du travail technique réalisé pendant l’été), une équipe s’est chargée de remettre de l’effort sur les mises à jour de dépendances. Le travail a été de passer à node 18 sur notre application principale et de configurer Renovate pour que tout roule.</p>

<p>Le besoin de quantifier d’une manière commune le retard de mise à jour des dépendances sur les différents projets nous a amené à nous intéresser à une métrique intéressante, <a href="https://libyear.com/">libyear</a>.</p>

<h2 id="libyear-à-la-rescousse">Libyear à la rescousse</h2>

<p>En bref <a href="https://libyear.com/">libyear</a> permet de quantifier l’âge de l’ensemble des dépendances.</p>

<p>En calculant cette métrique sur nos différents projets, nous avons une mesure <strong>comparable</strong> pour <strong>constater</strong> et <strong>décider</strong> des actions de mise à jour.
C’est pourquoi nous avons développé <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker">dependency-drift-tracker</a> qui nous permet de connaitre la dérive actuelle de nos dépendances, de voir l’évolution et de comparer ce retard avec les autres.</p>

<p>En plus du retard de nos dépendances, nous affichons également la mesure en années depuis la dernière release, appelée Pulse. Cela permet de repérer des dépendances qui sont devenues obsolètes ou dépréciées. Cela nécessite une analyse un peu plus poussée manuelle.</p>

<h2 id="a-quoi-cela-ressemble-">A quoi cela ressemble ?</h2>

<p><img integrity="sha512-UZiGiiFHs/fAd4R91onpxvfO3t9/doK+4ViqMA5ceTulUQ5+v4aVVFcv/iWD8WtvpT5JiiyfGj/amD62Eare7w==" crossorigin="anonymous" src="/assets/blog/dependency-drift-tracker-5198868a2147b3f7c077847dd689e9c6f7cededf7f7682bee158aa300e5c793ba5510e7ebf869554572ffe2583f16b6fa53e498a2c9f1a3fda983eb611aadeef.png" /></p>

<p>À gauche, en un clin d’œil, on voit les dépôts suivis avec l’information du retard. Ensuite dans le panneau principal, 2 métriques sont affichées, le retard et et le pulse.</p>

<p>Les 2 graphs suivants permettent de suivre l’évolution de ces chiffres à travers le temps.</p>

<p>Enfin le tableau final affiche le résultat du dernier lancement de libyear avec les informations de dépendances individuelles.</p>

<p>Les données sont rafraîchies tous les jours. Un retour suffisamment régulier pour valoriser le travail accompli la veille, et planifier la suite.</p>

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

<p>Rendre visible notre retard nous a permis de nous améliorer sur le suivi des versions. Voir la baisse de la courbe après une mise à jour est toujours un plaisir. Même si, d’une manière continue et semble t’il inexorable, la courbe remonte suite aux mises à jour des dizaines de dépendances utilisées par nos applications.</p>

<p>Curieux·ses, voici le <a href="https://1024pix.github.io/dependency-drift-tracker/">suivi des applications Pix</a>.</p>

<h2 id="essayez-le">Essayez-le</h2>

<p>Nous avons rendu le code de suivi générique pour être utilisable par n’importe quel projet javascript.
Vous pouvez facilement gérer les fichiers de suivi manuellement via <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker">dependency-drift-tracker</a>, ou encore plus simplement avec l’action GitHub <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker-action">dependency-drift-tracker-action</a>.</p>

<p>Et lisez la <a href="https://github.com/Dependency-Drift-Tracker/dependency-drift-tracker?tab=readme-ov-file#usage">documentation</a>.</p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[Le constat]]></summary></entry><entry xml:lang="en"><title type="html">Preview your indoor edits with JOSM</title><link href="https://2metz.fr/blog/preview-indoor-edits-in-josm/" rel="alternate" type="text/html" title="Preview your indoor edits with JOSM" /><published>2023-07-31T00:00:00+02:00</published><updated>2023-07-31T00:00:00+02:00</updated><id>https://2metz.fr/blog/preview-indoor-edits-in-josm</id><content type="html" xml:base="https://2metz.fr/blog/preview-indoor-edits-in-josm/"><![CDATA[<p><em>For those who have never heard of indoor=, you may want to read the <a href="/blog/indoorequal-openstreetmap-indoor-viewer/">introductory blog post</a>. In short, it is a map that displays the indoor data of OpenStreetmap with a level selector.</em></p>

<p>Thanks to a new <a href="https://github.com/indoorequal/indoorequal-josm-plugin">JOSM plugin</a>, it is now possible to preview your changes from <a href="https://josm.openstreetmap.de/">JOSM</a> with one click, similar to the <a href="https://osminedit.pavie.info/">OsmInEdit</a> integration.</p>

<video controls="true" controlList="nodownload" integrity="sha512-ftyKXnbaiXiPiANIhXtf6iKNmoaw6pyW20U4rSUG7ZJHFEoZu+MfTE3SkJlY/Lnv7+3A5DxlreBEBlCrHv96BA==" crossorigin="anonymous" src="/assets/blog/indoorequal-josm-7edc8a5e76da89788f880348857b5fea228d9a86b0ea9c96db4538ad2506ed9247144a19bbe31f4c4dd2909958fcb9efefedc0e43c65ade0440650ab1eff7a04.mp4">No support for video.</video>

<p>It adds a menu entry in the “More tools” entry and on click it open the the current layer on indoor= as a preview.</p>]]></content><author><name>François</name></author><summary type="html"><![CDATA[For those who have never heard of indoor=, you may want to read the introductory blog post. In short, it is a map that displays the indoor data of OpenStreetmap with a level selector.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://2metz.fr/indoorequal_social.png" /><media:content medium="image" url="https://2metz.fr/indoorequal_social.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Migrating from pow to phx.gen.auth</title><link href="https://2metz.fr/blog/migrate-from-pow-to-phx-gen-auth/" rel="alternate" type="text/html" title="Migrating from pow to phx.gen.auth" /><published>2023-05-03T00:00:00+02:00</published><updated>2023-05-03T00:00:00+02:00</updated><id>https://2metz.fr/blog/migrate-from-pow-to-phx.gen.auth</id><content type="html" xml:base="https://2metz.fr/blog/migrate-from-pow-to-phx-gen-auth/"><![CDATA[<p>For years I have been using <a href="https://github.com/pow-auth/pow">Pow</a> on the <a href="https://indoorequal.com">indoorequal.com</a> website. While I have been happy so far, the not-so-new kid in the block, <a href="https://hexdocs.pm/phoenix/mix_phx_gen_auth.html">phx.gen.auth</a> has been around for some times. I thought it was a good time to migrate to it.</p>

<p><a href="https://indoorequal.com">indoorequal.com</a> is a small app with only a handful of authenticated endpoints and an API to check the keys. Besides register and login workflow, the reset password and confirmation email extensions are enabled. There is also a role system that is used only for administration purposes.</p>

<p>My goal was to switch the authentication system without breaking anything from a user point of view while keeping the same features.</p>

<h2 id="login-with-previous-accounts">Login with previous accounts</h2>

<p>After generating the initial boilerplate I saw that <a href="https://hexdocs.pm/phoenix/mix_phx_gen_auth.html">phx.gen.auth</a> use different column names than pow</p>
<ul>
  <li>the column <code class="language-plaintext highlighter-rouge">email_confirmed_at</code> is named <code class="language-plaintext highlighter-rouge">confirmed_at</code></li>
  <li>the column <code class="language-plaintext highlighter-rouge">password_hash</code> is now <code class="language-plaintext highlighter-rouge">hashed_password</code></li>
</ul>

<p>So I renamed I updated the code to use <code class="language-plaintext highlighter-rouge">email_confirmed_at</code> instead of <code class="language-plaintext highlighter-rouge">confirmed_at</code> (enabling me to go back to the master branch without resetting my database).</p>

<p>For the password, my initial thought was to use the same hashing library than Pow, namely pbkdf2. So I renamed <code class="language-plaintext highlighter-rouge">hashed_password</code> to  <code class="language-plaintext highlighter-rouge">password_hash</code> in the code. But soon after trying to login with an account created with pow, I got the following error: <strong>(FunctionClauseError) no function clause matching in Pbkdf2.Base64.dec64/1</strong>.</p>

<p>The library <a href="https://hex.pm/packages/pbkdf2_elixir">pbkdf2_elixir</a> used by phx.gen.auth and the pbkdf2 implementation by Pow have some incompatibilies. <a href="https://hex.pm/packages/pbkdf2_elixir">pbkdf2_elixir</a> encode the different parts of the string using a restricted version of base64 (no padding, no point). My solution was to migrate the previous password to the new hashing algorithm on first login.</p>

<p>The initial migration was also updated to not create the users table (commented out for future references).</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Repo</span><span class="o">.</span><span class="no">Migrations</span><span class="o">.</span><span class="no">CreateUsersAuthTables</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Ecto</span><span class="o">.</span><span class="no">Migration</span>

  <span class="k">def</span> <span class="n">change</span> <span class="k">do</span>
    <span class="n">execute</span> <span class="s2">"CREATE EXTENSION IF NOT EXISTS citext"</span><span class="p">,</span> <span class="s2">""</span>

    <span class="c1">#create table(:users) do</span>
    <span class="c1">#  add :email, :citext, null: false</span>
    <span class="c1">#  add :hashed_password, :string, null: false</span>
    <span class="c1">#  add :confirmed_at, :naive_datetime</span>
    <span class="c1">#  timestamps()</span>
    <span class="c1">#end</span>

    <span class="c1">#create unique_index(:users, [:email])</span>

    <span class="n">create</span> <span class="n">table</span><span class="p">(</span><span class="ss">:users_tokens</span><span class="p">)</span> <span class="k">do</span>
      <span class="n">add</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="n">references</span><span class="p">(</span><span class="ss">:users</span><span class="p">,</span> <span class="ss">on_delete:</span> <span class="ss">:delete_all</span><span class="p">),</span> <span class="ss">null:</span> <span class="no">false</span>
      <span class="n">add</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">:binary</span><span class="p">,</span> <span class="ss">null:</span> <span class="no">false</span>
      <span class="n">add</span> <span class="ss">:context</span><span class="p">,</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">null:</span> <span class="no">false</span>
      <span class="n">add</span> <span class="ss">:sent_to</span><span class="p">,</span> <span class="ss">:string</span>
      <span class="n">timestamps</span><span class="p">(</span><span class="ss">updated_at:</span> <span class="no">false</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="n">create</span> <span class="n">index</span><span class="p">(</span><span class="ss">:users_tokens</span><span class="p">,</span> <span class="p">[</span><span class="ss">:user_id</span><span class="p">])</span>
    <span class="n">create</span> <span class="n">unique_index</span><span class="p">(</span><span class="ss">:users_tokens</span><span class="p">,</span> <span class="p">[</span><span class="ss">:context</span><span class="p">,</span> <span class="ss">:token</span><span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I created a new migration for the email column that has to be changed to citext, and the new column to the store the password:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Repo</span><span class="o">.</span><span class="no">Migrations</span><span class="o">.</span><span class="no">UpdateUsersEmail</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Ecto</span><span class="o">.</span><span class="no">Migration</span>

  <span class="k">def</span> <span class="n">change</span> <span class="k">do</span>
    <span class="n">alter</span> <span class="n">table</span><span class="p">(</span><span class="ss">:users</span><span class="p">)</span> <span class="k">do</span>
      <span class="n">modify</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:citext</span><span class="p">,</span> <span class="ss">from:</span> <span class="ss">:string</span>
      <span class="n">add</span> <span class="ss">:hashed_password</span><span class="p">,</span> <span class="ss">:string</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It’s now time to try the login with a previous user account. I created a test case like that in <code class="language-plaintext highlighter-rouge">accounts_test.ex</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">AccountsTest</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="n">describe</span> <span class="s2">"get_user_by_email_and_password/2"</span> <span class="k">do</span>
    <span class="o">...</span>
    <span class="n">test</span> <span class="s2">"returns the user if the password has been generated by pow"</span> <span class="k">do</span>
      <span class="p">%{</span><span class="ss">id:</span> <span class="n">id</span><span class="p">}</span> <span class="o">=</span> <span class="n">user</span> <span class="o">=</span> <span class="n">user_fixture</span><span class="p">()</span>
      <span class="n">user</span>
      <span class="o">|&gt;</span> <span class="no">Ecto</span><span class="o">.</span><span class="no">Changeset</span><span class="o">.</span><span class="n">change</span><span class="p">(%{</span><span class="ss">password_hash:</span> <span class="no">Pow</span><span class="o">.</span><span class="no">Ecto</span><span class="o">.</span><span class="no">Schema</span><span class="o">.</span><span class="no">Password</span><span class="o">.</span><span class="n">pbkdf2_hash</span><span class="p">(</span><span class="n">valid_user_password</span><span class="p">())})</span>
      <span class="o">|&gt;</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Repo</span><span class="o">.</span><span class="n">update</span><span class="p">()</span>

      <span class="n">assert</span> <span class="p">%</span><span class="no">User</span><span class="p">{</span><span class="ss">id:</span> <span class="o">^</span><span class="n">id</span><span class="p">}</span> <span class="o">=</span>
        <span class="no">Accounts</span><span class="o">.</span><span class="n">get_user_by_email_and_password</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">email</span><span class="p">,</span> <span class="n">valid_user_password</span><span class="p">())</span>
    <span class="k">end</span>
    <span class="o">...</span>
  <span class="k">end</span>
  <span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The implementation in <code class="language-plaintext highlighter-rouge">accounts.ex</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Accounts</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="k">def</span> <span class="n">get_user_by_email_and_password</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
      <span class="ow">when</span> <span class="n">is_binary</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="ow">and</span> <span class="n">is_binary</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get_by</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">email:</span> <span class="n">email</span><span class="p">)</span>
    <span class="k">case</span> <span class="no">User</span><span class="o">.</span><span class="n">has_password_from_pow?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">do</span>
      <span class="no">true</span> <span class="o">-&gt;</span>
        <span class="k">if</span> <span class="no">User</span><span class="o">.</span><span class="n">valid_password_from_pow?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="k">do</span>
          <span class="n">user</span>
          <span class="o">|&gt;</span> <span class="no">User</span><span class="o">.</span><span class="n">password_from_pow_changeset</span><span class="p">(%{</span><span class="ss">password:</span> <span class="n">password</span><span class="p">})</span>
          <span class="o">|&gt;</span> <span class="no">Repo</span><span class="o">.</span><span class="n">update!</span><span class="p">()</span>
        <span class="k">end</span>
      <span class="no">false</span> <span class="o">-&gt;</span>
        <span class="k">if</span> <span class="no">User</span><span class="o">.</span><span class="n">valid_password?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">password</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">user</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And the functions in <code class="language-plaintext highlighter-rouge">user.ex</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Accounts</span><span class="o">.</span><span class="no">User</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="nv">@doc</span> <span class="sd">"""
  A user changeset for changing the password from pow one to phx.gen.auth
  ## Options

    * `:hash_password` - Hashes the password so it can be stored securely
      in the database and ensures the password field is cleared to prevent
      leaks in the logs. If password hashing is not needed and clearing the
      password field is not desired (like when using this changeset for
      validations on a LiveView form), this option can be set to `false`.
      Defaults to `true`.
  """</span>
  <span class="k">def</span> <span class="n">password_from_pow_changeset</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="n">opts</span> <span class="p">\\</span> <span class="p">[])</span> <span class="k">do</span>
    <span class="n">user</span>
    <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
    <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:password_hash</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">maybe_hash_password</span><span class="p">(</span><span class="n">opts</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="nv">@doc</span> <span class="sd">"""
  Check is there is a password previously set from pow
  """</span>
  <span class="k">def</span> <span class="n">has_password_from_pow?</span><span class="p">(%{</span><span class="ss">password_hash:</span> <span class="n">password_hash</span><span class="p">})</span>
    <span class="ow">when</span> <span class="n">is_binary</span><span class="p">(</span><span class="n">password_hash</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">has_password_from_pow?</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">false</span>
  <span class="k">end</span>

  <span class="nv">@doc</span> <span class="sd">"""
  Verify the password from pow
  """</span>
  <span class="k">def</span> <span class="n">valid_password_from_pow?</span><span class="p">(%</span><span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Accounts</span><span class="o">.</span><span class="no">User</span><span class="p">{</span><span class="ss">password_hash:</span> <span class="n">password_hash</span><span class="p">},</span> <span class="n">password</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Pow</span><span class="o">.</span><span class="no">Ecto</span><span class="o">.</span><span class="no">Schema</span><span class="o">.</span><span class="no">Password</span><span class="o">.</span><span class="n">pbkdf2_verify</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">password_hash</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The solution was to look for the content of the <code class="language-plaintext highlighter-rouge">password_hash</code> column. If the column is non empty, then it means that the account was created via pow. The password is checked against Pow pbkdf2 implementation and if it matches, the password is hased with argon2 and stored in the column <code class="language-plaintext highlighter-rouge">hashed_password</code> and the previous column is resetted.</p>

<h2 id="check-user-email-before-login">Check user email before login</h2>

<p>To reproduce the Pow’ user email confirmation, there is 2 places to update:</p>

<ul>
  <li>on registration to not login the user</li>
  <li>on login to check that the user is confirmed</li>
</ul>

<p>After registration, update the file <code class="language-plaintext highlighter-rouge">user_registration_controller.ex</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">IndoorequalcomWeb</span><span class="o">.</span><span class="no">UserRegistrationController</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="k">def</span> <span class="n">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="p">%{</span><span class="s2">"user"</span> <span class="o">=&gt;</span> <span class="n">user_params</span><span class="p">})</span> <span class="k">do</span>
    <span class="k">case</span> <span class="no">Accounts</span><span class="o">.</span><span class="n">register_user</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span> <span class="k">do</span>
      <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">user</span><span class="p">}</span> <span class="o">-&gt;</span>
        <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">=</span>
          <span class="no">Accounts</span><span class="o">.</span><span class="n">deliver_user_confirmation_instructions</span><span class="p">(</span>
            <span class="n">user</span><span class="p">,</span>
            <span class="o">&amp;</span><span class="n">url</span><span class="p">(</span><span class="sx">~p"/users/confirm/#{&amp;1}"</span><span class="p">)</span>
          <span class="p">)</span>

        <span class="n">conn</span>
        <span class="o">|&gt;</span> <span class="n">put_flash</span><span class="p">(</span><span class="ss">:info</span><span class="p">,</span> <span class="s2">"You'll need to confirm your e-mail before you can sign in. An e-mail confirmation link has been sent to you."</span><span class="p">)</span>
        <span class="o">|&gt;</span> <span class="n">redirect</span><span class="p">(</span><span class="ss">to:</span> <span class="sx">~p"/"</span><span class="p">)</span>

      <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="p">%</span><span class="no">Ecto</span><span class="o">.</span><span class="no">Changeset</span><span class="p">{}</span> <span class="o">=</span> <span class="n">changeset</span><span class="p">}</span> <span class="o">-&gt;</span>
        <span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:new</span><span class="p">,</span> <span class="ss">changeset:</span> <span class="n">changeset</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>On login, the code involved an update to <code class="language-plaintext highlighter-rouge">accounts.ex</code></p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Indoorequalcom</span><span class="o">.</span><span class="no">Accounts</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="k">def</span> <span class="n">get_user_by_email_and_password</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
      <span class="ow">when</span> <span class="n">is_binary</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="ow">and</span> <span class="n">is_binary</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="k">do</span>
    <span class="k">case</span> <span class="n">get_valid_user_password</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="k">do</span>
      <span class="no">nil</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="ss">:invalid</span><span class="p">}</span>
      <span class="n">user</span> <span class="o">-&gt;</span>
        <span class="k">case</span> <span class="n">user</span><span class="o">.</span><span class="n">email_confirmed_at</span> <span class="k">do</span>
          <span class="no">nil</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="ss">:email_not_confirmed</span><span class="p">,</span> <span class="n">user</span><span class="p">}</span>
         <span class="n">_</span> <span class="o">-&gt;</span> <span class="n">user</span>
        <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">get_valid_user_password</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get_by</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">email:</span> <span class="n">email</span><span class="p">)</span>
    <span class="k">case</span> <span class="no">User</span><span class="o">.</span><span class="n">has_password_from_pow?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">do</span>
      <span class="no">true</span> <span class="o">-&gt;</span>
        <span class="k">if</span> <span class="no">User</span><span class="o">.</span><span class="n">valid_password_from_pow?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="k">do</span>
          <span class="n">user</span>
          <span class="o">|&gt;</span> <span class="no">User</span><span class="o">.</span><span class="n">password_from_pow_changeset</span><span class="p">(%{</span><span class="ss">password:</span> <span class="n">password</span><span class="p">})</span>
          <span class="o">|&gt;</span> <span class="no">Repo</span><span class="o">.</span><span class="n">update!</span><span class="p">()</span>
        <span class="k">end</span>
      <span class="no">false</span> <span class="o">-&gt;</span>
        <span class="k">if</span> <span class="no">User</span><span class="o">.</span><span class="n">valid_password?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">password</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">user</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And in the session controller to catch the error to display a specific message and (re)sending the email.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">IndoorequalcomWeb</span><span class="o">.</span><span class="no">UserSessionController</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="k">def</span> <span class="n">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="p">%{</span><span class="s2">"user"</span> <span class="o">=&gt;</span> <span class="n">user_params</span><span class="p">})</span> <span class="k">do</span>
    <span class="p">%{</span><span class="s2">"email"</span> <span class="o">=&gt;</span> <span class="n">email</span><span class="p">,</span> <span class="s2">"password"</span> <span class="o">=&gt;</span> <span class="n">password</span><span class="p">}</span> <span class="o">=</span> <span class="n">user_params</span>

    <span class="k">case</span> <span class="no">Accounts</span><span class="o">.</span><span class="n">get_user_by_email_and_password</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span> <span class="k">do</span>
      <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="ss">:invalid</span><span class="p">}</span> <span class="o">-&gt;</span>
        <span class="c1"># In order to prevent user enumeration attacks, don't disclose whether the email is registered.</span>
        <span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:new</span><span class="p">,</span> <span class="ss">error_message:</span> <span class="s2">"Invalid email or password"</span><span class="p">)</span>
      <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="ss">:email_not_confirmed</span><span class="p">,</span> <span class="n">user</span><span class="p">}</span> <span class="o">-&gt;</span>
        <span class="no">Accounts</span><span class="o">.</span><span class="n">deliver_user_confirmation_instructions</span><span class="p">(</span>
          <span class="n">user</span><span class="p">,</span>
          <span class="o">&amp;</span><span class="n">url</span><span class="p">(</span><span class="sx">~p"/users/confirm/#{&amp;1}"</span><span class="p">)</span>
        <span class="p">)</span>
        <span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:new</span><span class="p">,</span> <span class="ss">error_message:</span> <span class="s2">"You'll need to confirm your e-mail before you can sign in. An e-mail confirmation link has been sent to you"</span><span class="p">)</span>
      <span class="n">user</span> <span class="o">-&gt;</span>
        <span class="n">conn</span>
        <span class="o">|&gt;</span> <span class="no">UserAuth</span><span class="o">.</span><span class="n">log_in_user</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">user_params</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

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

<p>Once I got the way to migrate from Pow with password, the process to migrate went smoothly.
I only described the main points to look at, but there a lot of things to do before going live:</p>
<ul>
  <li>tests to update the new log_in workflow</li>
  <li>style all forms</li>
  <li>plug and customize emails</li>
  <li>…</li>
</ul>]]></content><author><name>François</name></author><summary type="html"><![CDATA[For years I have been using [Pow][] on the indoorequal.com website. While I have been happy so far, the not-so-new kid in the block, phx.gen.auth has been around for some times. I thought it was a good time to migrate to it.]]></summary></entry></feed>