<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Shahid Foy</title>
    <description>The latest articles on Forem by Shahid Foy (@shahidfoy).</description>
    <link>https://forem.com/shahidfoy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1470091%2F9ffce5c1-825b-4842-87e7-20cf0bd125d9.jpg</url>
      <title>Forem: Shahid Foy</title>
      <link>https://forem.com/shahidfoy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/shahidfoy"/>
    <language>en</language>
    <item>
      <title>How to Send Discord Webhook Alerts in Java (Spring Boot Example)</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Tue, 24 Feb 2026 00:59:12 +0000</pubDate>
      <link>https://forem.com/shahidfoy/how-to-send-discord-webhook-alerts-in-java-spring-boot-example-h51</link>
      <guid>https://forem.com/shahidfoy/how-to-send-discord-webhook-alerts-in-java-spring-boot-example-h51</guid>
      <description>&lt;p&gt;If you're building a &lt;strong&gt;Java or Spring Boot application&lt;/strong&gt; and want to send &lt;strong&gt;real-time alerts to Discord&lt;/strong&gt;, this guide will show you exactly how to do it using a lightweight webhook client.&lt;/p&gt;

&lt;p&gt;No Discord bot.&lt;br&gt;
No OAuth flow.&lt;br&gt;
Just clean, structured webhook messages sent directly from your backend.&lt;/p&gt;

&lt;p&gt;In this article, we’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ How Discord webhooks work&lt;/li&gt;
&lt;li&gt;✅ How to send webhook messages in Java&lt;/li&gt;
&lt;li&gt;✅ A detailed Spring Boot alert example&lt;/li&gt;
&lt;li&gt;✅ How to structure rich embeds for production alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll be using the &lt;strong&gt;n1netails-discord-webhook-client&lt;/strong&gt; library to make this easy.&lt;/p&gt;

&lt;p&gt;You can also follow along by watching the YouTube video here: &lt;a href="https://www.youtube.com/watch?v=sBgH_2bOv5Y" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=sBgH_2bOv5Y&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Send Alerts to Discord from Java?
&lt;/h2&gt;

&lt;p&gt;Many teams already use Discord for communication. Instead of checking dashboards or waiting for email alerts, you can push real-time notifications directly into a channel.&lt;/p&gt;

&lt;p&gt;Common use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚨 Production error alerts&lt;/li&gt;
&lt;li&gt;📦 Deployment notifications&lt;/li&gt;
&lt;li&gt;🔐 Security findings&lt;/li&gt;
&lt;li&gt;💰 Trading bot alerts&lt;/li&gt;
&lt;li&gt;📊 Monitoring &amp;amp; uptime notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Discord webhooks allow you to send messages using a simple HTTP POST request - perfect for backend systems.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 1: Add the Dependency
&lt;/h1&gt;
&lt;h3&gt;
  
  
  Maven
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.n1netails&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;n1netails-discord-webhook-client&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.3.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Gradle
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'com.n1netails:n1netails-discord-webhook-client:0.3.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This gives you a simple Java client for sending Discord webhook messages.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 2: Configure in Spring Boot
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DiscordWebhookConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;WebhookService&lt;/span&gt; &lt;span class="nf"&gt;webhookService&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebhookService&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;DiscordWebhookClient&lt;/span&gt; &lt;span class="nf"&gt;discordWebhookClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebhookService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DiscordWebhookClientImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you're not using Spring Boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebhookService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebhookService&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;DiscordWebhookClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DiscordWebhookClientImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to autowire the DiscordWebhookClient in the service you wish to use it on&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DiscordWebhookClient&lt;/span&gt; &lt;span class="n"&gt;webhookClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DiscordWebhookDemoApplication&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DiscordWebhookClient&lt;/span&gt; &lt;span class="n"&gt;webhookClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;webhookClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webhookClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Step 3: Send a Basic Discord Webhook Message in Java
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;webhookUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://discord.com/api/webhooks/xxx/yyy"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTitle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Build Notification"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDescription&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The build has succeeded! ✅"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DiscordColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BLUE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="nc"&gt;WebhookMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebhookMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CI Bot"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Deployment update"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmbeds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;webhookClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webhookUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the simplest possible example.&lt;/p&gt;

&lt;p&gt;Now let’s build something production-ready.&lt;/p&gt;




&lt;h1&gt;
  
  
  Sending a Detailed Discord Embed Alert (Production Example)
&lt;/h1&gt;

&lt;p&gt;Discord embeds let you send structured messages with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Description&lt;/li&gt;
&lt;li&gt;Colored severity indicator&lt;/li&gt;
&lt;li&gt;Fields&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;Footer&lt;/li&gt;
&lt;li&gt;Buttons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a real-world &lt;strong&gt;Spring Boot production error alert example&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;webhookUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://discord.com/api/webhooks/xxx/yyy"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N1netails"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://n1netails.com"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIcon_url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/n1netails/n1netails/refs/heads/main/n1netails_icon_transparent.png"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EmbedField&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EmbedField&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Environment"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Development"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setInline&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Footer&lt;/span&gt; &lt;span class="n"&gt;footer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Footer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;footer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N1netails @ 2026"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;footer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIcon_url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/n1netails/n1netails/refs/heads/main/n1netails_icon_transparent.png"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Image&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/n1netails/n1netails/refs/heads/main/n1netails_icon_transparent.png"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Thumbnail&lt;/span&gt; &lt;span class="n"&gt;thumbnail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Thumbnail&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;thumbnail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/n1netails/n1netails/refs/heads/main/n1netails_icon_transparent.png"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Embed&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmbedBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withTitle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Build Notification"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDescription&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The build has succeeded! ✅"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://n1netails.com/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DiscordColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ORANGE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withAuthor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFields&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;singletonList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFooter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;footer&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withImage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withThumbnail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thumbnail&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withTimestamp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;WebhookMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebhookMessageBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CI Bot"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;withContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Deployment update"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withEmbeds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;singletonList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;webhookClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webhookUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a clean, structured Discord alert that your team can immediately scan and act on.&lt;/p&gt;

&lt;p&gt;Instead of dumping stack traces into chat, you send contextual, readable notifications.&lt;/p&gt;




&lt;h1&gt;
  
  
  When Should You Use Discord Webhooks in Spring Boot?
&lt;/h1&gt;

&lt;p&gt;You should use Discord webhooks when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want lightweight alerting without managing a bot&lt;/li&gt;
&lt;li&gt;Your team already communicates in Discord&lt;/li&gt;
&lt;li&gt;You need fast, visible notifications&lt;/li&gt;
&lt;li&gt;You want structured alert formatting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many internal tools, Discord webhooks are faster to implement than full monitoring systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;If you're building backend systems in &lt;strong&gt;Java or Spring Boot&lt;/strong&gt;, sending alerts to Discord via webhooks is one of the simplest ways to improve visibility.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;n1netails-discord-webhook-client&lt;/strong&gt; makes this process clean, structured, and developer-friendly.&lt;/p&gt;

&lt;p&gt;No heavy frameworks.&lt;br&gt;
No bot complexity.&lt;br&gt;
Just fast alerts where your team already is.&lt;/p&gt;

</description>
      <category>java</category>
      <category>discord</category>
      <category>springboot</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Build a Telegram Bot in Java with the N1netails Telegram Client</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Sun, 15 Feb 2026 22:43:42 +0000</pubDate>
      <link>https://forem.com/shahidfoy/build-a-telegram-bot-in-java-with-the-n1netails-telegram-client-3p4c</link>
      <guid>https://forem.com/shahidfoy/build-a-telegram-bot-in-java-with-the-n1netails-telegram-client-3p4c</guid>
      <description>&lt;p&gt;If you’ve ever wanted to send automated alerts, notifications, or rich media messages to Telegram from your Java application, you’ve probably discovered that working directly with the Telegram Bot API can get repetitive fast.&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;N1netails Telegram Client&lt;/strong&gt; comes in. N1netails a lightweight, open-source Java library designed to make sending Telegram messages simple, structured, and production-ready.&lt;/p&gt;

&lt;p&gt;In this guide, you’ll learn:&lt;/p&gt;

&lt;p&gt;✅ How to create a Telegram bot&lt;br&gt;
✅ How to configure the N1netails client&lt;br&gt;
✅ How to send text, GIFs, images, videos, and media groups&lt;br&gt;
✅ How to add call-to-action buttons&lt;/p&gt;


&lt;h2&gt;
  
  
  What is N1netails Telegram Client?
&lt;/h2&gt;

&lt;p&gt;N1netails is an open-source project focused on practical alerting and monitoring. The Telegram client module lets you easily send structured Telegram messages from Java applications perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alerting systems&lt;/li&gt;
&lt;li&gt;Automation tools&lt;/li&gt;
&lt;li&gt;Monitoring dashboards&lt;/li&gt;
&lt;li&gt;DevOps workflows&lt;/li&gt;
&lt;li&gt;App notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of hand-crafting API requests, you work with clean Java objects.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the full video tutorial:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=T7drCapES6U" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=T7drCapES6U&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1 - Set Up Telegram
&lt;/h2&gt;

&lt;p&gt;Before using the client, you need a Telegram bot.&lt;/p&gt;
&lt;h3&gt;
  
  
  Install Telegram
&lt;/h3&gt;

&lt;p&gt;Download Telegram on your smartphone first:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://telegram.org/" rel="noopener noreferrer"&gt;https://telegram.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Telegram requires initial mobile setup before desktop use.&lt;/p&gt;


&lt;h2&gt;
  
  
  Create Your Telegram Bot
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Talk to BotFather
&lt;/h3&gt;

&lt;p&gt;Open Telegram and search for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@BotFather
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Telegram’s official bot manager.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Create a new bot
&lt;/h3&gt;

&lt;p&gt;Send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/newbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll be prompted to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name your bot&lt;/li&gt;
&lt;li&gt;Choose a username (must end in &lt;code&gt;bot&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n1netails_alertbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Save your bot token
&lt;/h3&gt;

&lt;p&gt;BotFather will generate something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;123456789:ABCdefGhIJKlmNoPQRstuVWXyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠ Keep this private — anyone with it controls your bot.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Add the bot to a chat
&lt;/h3&gt;

&lt;p&gt;Add your bot to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A group chat&lt;/li&gt;
&lt;li&gt;Or a direct chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ensure it has permission to send messages.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Get the chat ID
&lt;/h3&gt;

&lt;p&gt;Option A — Use a helper bot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@getidsbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option B — Use Telegram API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.telegram.org/bot&amp;lt;YOUR_BOT_TOKEN&amp;gt;/getUpdates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"chat":{"id":...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have:&lt;/p&gt;

&lt;p&gt;✔ Bot token&lt;br&gt;
✔ Chat ID&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2 - Install the Library
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Maven
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.n1netails&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;n1netails-telegram-client&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.3.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Gradle
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'com.n1netails:n1netails-telegram-client:0.3.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 - Configuration
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Spring Boot Setup
&lt;/h2&gt;

&lt;p&gt;Create a configuration class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TelegramConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="nd"&gt;@Bean&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BotService&lt;/span&gt; &lt;span class="nf"&gt;botService&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BotService&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Bean&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;TelegramClient&lt;/span&gt; &lt;span class="nf"&gt;telegramClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BotService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TelegramClientImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Plain Java Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;BotService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BotService&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;TelegramClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TelegramClientImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4 - Sending Messages
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Simple Text Message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TelegramMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TelegramMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Telegram works!"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;botToken&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  GIF Message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TelegramMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TelegramMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Check this GIF!"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"https://giphy-url.gif"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  CTA Button Message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Visit Site"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;InlineKeyboardMarkup&lt;/span&gt; &lt;span class="n"&gt;markup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InlineKeyboardMarkup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

&lt;span class="nc"&gt;TelegramMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TelegramMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Click below!"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;markup&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Image Message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TelegramMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TelegramMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Photo alert!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setImages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://image-url.png"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Video Message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TelegramMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TelegramMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Video alert!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setVideos&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://video-url.mp4"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Media Group (Images + Videos)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TelegramMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TelegramMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Media bundle!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setImages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"image1.png"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image2.png"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setVideos&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"video.mp4"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠ Note: Telegram does not allow CTA buttons with media groups.&lt;/p&gt;




&lt;h1&gt;
  
  
  Important Notes
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Media must be &lt;strong&gt;public URLs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CTA buttons only work with single media messages&lt;/li&gt;
&lt;li&gt;Keep bot tokens secure&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Why Use This Client?
&lt;/h1&gt;

&lt;p&gt;Without the library:&lt;/p&gt;

&lt;p&gt;❌ Manual API formatting&lt;br&gt;
❌ Boilerplate HTTP calls&lt;br&gt;
❌ Error-prone message building&lt;/p&gt;

&lt;p&gt;With N1netails:&lt;/p&gt;

&lt;p&gt;✅ Clean Java message models&lt;br&gt;
✅ Structured API calls&lt;br&gt;
✅ Rich media support&lt;br&gt;
✅ Production-ready workflow&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The N1netails Telegram Client removes friction when integrating Telegram messaging into Java applications. Whether you’re building alerts, automation, or monitoring systems, it gives you a simple and extensible foundation.&lt;/p&gt;

&lt;p&gt;If you want fast Telegram integration without wrestling the raw API, this library is a solid choice.&lt;/p&gt;

&lt;p&gt;👉 GitHub repository:&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails-telegram-client" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails-telegram-client&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🦊 N1netails:&lt;br&gt;
&lt;a href="https://n1netails.com/" rel="noopener noreferrer"&gt;https://n1netails.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Join the Discord:&lt;br&gt;
&lt;a href="https://discord.gg/ma9CCw7F2x" rel="noopener noreferrer"&gt;https://discord.gg/ma9CCw7F2x&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If this helped you, consider starring the repo and sharing it with other Java developers. Happy building 🚀&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>springboot</category>
      <category>java</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Used Google Jules to Implement Google OAuth2 in My Spring Boot + Angular Application</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Wed, 26 Nov 2025 23:51:48 +0000</pubDate>
      <link>https://forem.com/shahidfoy/how-i-used-google-jules-to-implement-google-oauth2-in-my-spring-boot-angular-application-27lf</link>
      <guid>https://forem.com/shahidfoy/how-i-used-google-jules-to-implement-google-oauth2-in-my-spring-boot-angular-application-27lf</guid>
      <description>&lt;p&gt;Google Jules has been out for a few months now, and I decided to finally give it a real development task:&lt;br&gt;
&lt;strong&gt;implement Google OAuth2 login in my Spring Boot + Angular application (N1netails)&lt;/strong&gt;—while following the exact same authentication pattern I previously implemented for GitHub OAuth2.&lt;/p&gt;

&lt;p&gt;A while back, I wrote a detailed guide covering the &lt;strong&gt;GitHub OAuth2 integration&lt;/strong&gt;, which serves as the prequel to this article:&lt;br&gt;
👉 &lt;a href="https://dev.to/shahidfoy/how-i-implemented-github-oauth2-into-my-spring-boot-angular-app-n1netails-5bo"&gt;https://dev.to/shahidfoy/how-i-implemented-github-oauth2-into-my-spring-boot-angular-app-n1netails-5bo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this update, I reused the same structure and simply extended it for &lt;strong&gt;Google OAuth2&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tag:&lt;/strong&gt; v0.3.14
&lt;a href="https://github.com/n1netails/n1netails/tree/v0.3.14" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/tree/v0.3.14&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull Request:&lt;/strong&gt;
&lt;a href="https://github.com/n1netails/n1netails/pull/203/files" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/pull/203/files&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Google Jules, I asked it to analyze the existing GitHub OAuth2 logic and apply the same pattern for the Google OAuth2 flow—making the integration faster, more consistent, and less error-prone.&lt;/p&gt;


&lt;h1&gt;
  
  
  &lt;strong&gt;🔐 Google OAuth2 in Spring Boot&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The most important configuration piece is the &lt;code&gt;registrationId&lt;/code&gt; Spring Security uses to differentiate OAuth providers.&lt;br&gt;
It corresponds directly to your YAML keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;spring.security.oauth2.client.registration.github&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spring.security.oauth2.client.registration.google&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;OAuth2 Configuration (application-oauth.yml)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.3.14/n1netails-api/src/main/resources/application-oauth.yml" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/blob/v0.3.14/n1netails-api/src/main/resources/application-oauth.yml&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;registration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_ID:your-github-client-id}&lt;/span&gt;
            &lt;span class="na"&gt;client-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_SECRET:your-github-client-secret}&lt;/span&gt;
            &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read:user,user:email&lt;/span&gt;
            &lt;span class="na"&gt;redirect-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{baseUrl}/login/oauth2/code/{registrationId}"&lt;/span&gt;  &lt;span class="c1"&gt;# /login/oauth2/code/github&lt;/span&gt;
            &lt;span class="na"&gt;client-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub&lt;/span&gt;
          &lt;span class="na"&gt;google&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOOGLE_CLIENT_ID:your-google-client-id}&lt;/span&gt;
            &lt;span class="na"&gt;client-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOOGLE_CLIENT_SECRET:your-google-client-secret}&lt;/span&gt;
            &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openid,email,profile&lt;/span&gt;
            &lt;span class="na"&gt;redirect-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{baseUrl}/login/oauth2/code/{registrationId}"&lt;/span&gt; &lt;span class="c1"&gt;# /login/oauth2/code/google&lt;/span&gt;
            &lt;span class="na"&gt;client-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Google&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;authorization-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/login/oauth/authorize&lt;/span&gt;
            &lt;span class="na"&gt;token-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/login/oauth/access_token&lt;/span&gt;
            &lt;span class="na"&gt;user-info-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.github.com/user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  &lt;strong&gt;🔄 OAuth2 Success Handler&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;After a successful login, Spring Boot passes the OAuth token + user info to my &lt;code&gt;OAuth2Service&lt;/code&gt;. From there, the service generates a &lt;strong&gt;JWT token&lt;/strong&gt; for N1netails and redirects to the Angular frontend.&lt;/p&gt;

&lt;p&gt;Code reference:&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails/blob/89f84bbd13b845888e8cc279b0334603ef8207ab/n1netails-api/src/main/java/com/n1netails/n1netails/api/config/security/Oauth2LoginSecurityConfig.java#L47-L54" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/blob/89f84bbd13b845888e8cc279b0334603ef8207ab/n1netails-api/src/main/java/com/n1netails/n1netails/api/config/security/Oauth2LoginSecurityConfig.java#L47-L54&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2Login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;successHandler&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2AuthenticationToken&lt;/span&gt; &lt;span class="n"&gt;oauthToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2AuthenticationToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2User&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2User&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrincipal&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauthToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthorizedClientRegistrationId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jwtToken&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"github"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equalsIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;jwtToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loginGithub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oauthToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"google"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equalsIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;jwtToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loginGoogle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oauthToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unexpected provider: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendRedirect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2RedirectsSuccess&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jwtToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  &lt;strong&gt;🧩 Google Login Service Logic&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;If you’re curious how &lt;code&gt;oAuth2Service.loginGoogle()&lt;/code&gt; is implemented, you can see the full logic here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/89f84bbd13b845888e8cc279b0334603ef8207ab/n1netails-api/src/main/java/com/n1netails/n1netails/api/service/impl/OAuth2ServiceImpl.java#L135-L200" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/blob/89f84bbd13b845888e8cc279b0334603ef8207ab/n1netails-api/src/main/java/com/n1netails/n1netails/api/service/impl/OAuth2ServiceImpl.java#L135-L200&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Security sends the Google OAuth user details.&lt;/li&gt;
&lt;li&gt;The service creates or updates the user in the N1netails database.&lt;/li&gt;
&lt;li&gt;A JWT token is generated for the SPA (Angular).&lt;/li&gt;
&lt;li&gt;No passwords are stored—identity is validated by Google.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the biggest benefits of OAuth2:&lt;br&gt;
&lt;strong&gt;authentication is delegated to the provider&lt;/strong&gt;, reducing security risk in your system.&lt;/p&gt;


&lt;h1&gt;
  
  
  &lt;strong&gt;🖥️ Google Login on the Angular Frontend&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Google Jules also helped mirror the GitHub login button structure on the Angular side.&lt;/p&gt;

&lt;p&gt;HTML reference:&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails/blob/v0.3.14/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.html#L71-L81" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/blob/v0.3.14/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.html#L71-L81&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;*ngIf=&lt;/span&gt;&lt;span class="s"&gt;"googleAuthEnabled"&lt;/span&gt;
  &lt;span class="na"&gt;nz-button&lt;/span&gt;
  &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"loginWithGoogle()"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"login-btn google-login-btn"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nz-icon&lt;/span&gt; &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"google"&lt;/span&gt; &lt;span class="na"&gt;nzTheme=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"margin-right: 8px;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  Login with Google
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Angular Login Method&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/89f84bbd13b845888e8cc279b0334603ef8207ab/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.ts#L57-L61" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/blob/89f84bbd13b845888e8cc279b0334603ef8207ab/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.ts#L57-L61&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;loginWithGoogle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// window.location.href = 'http://localhost:9901/oauth2/authorization/google';&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uiConfigService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getApiUrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/oauth2/authorization/google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commented line (&lt;code&gt;http://localhost:9901/...&lt;/code&gt;) is just the default local Spring Boot OAuth2 endpoint during development.&lt;/p&gt;




&lt;h1&gt;
  
  
  &lt;strong&gt;📌 Full Pull Request&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;To see every change involved in adding Google OAuth2 to both Spring Boot and Angular:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/n1netails/n1netails/pull/203/files" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/pull/203/files&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  ⭐ Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Using &lt;strong&gt;Google Jules&lt;/strong&gt; to extend my OAuth2 implementation saved time and helped keep the pattern consistent across providers.&lt;br&gt;
If you're building a Spring Boot + Angular app and want to offer multiple OAuth providers (GitHub, Google, etc.), this approach is clean, reusable, and secure.&lt;/p&gt;

&lt;p&gt;If you found this helpful, please consider &lt;strong&gt;starring the repo&lt;/strong&gt; 💙&lt;br&gt;
👉 &lt;a href="https://github.com/n1netails/n1netails" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>springsecurity</category>
      <category>springboot</category>
    </item>
    <item>
      <title>📨 Mocking Emails in a Spring Boot App Using MailHog – How I Integrated Email Testing into N1netails</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Thu, 07 Aug 2025 21:53:20 +0000</pubDate>
      <link>https://forem.com/shahidfoy/mocking-emails-in-a-spring-boot-app-using-mailhog-how-i-integrated-email-testing-into-n1netails-1p5</link>
      <guid>https://forem.com/shahidfoy/mocking-emails-in-a-spring-boot-app-using-mailhog-how-i-integrated-email-testing-into-n1netails-1p5</guid>
      <description>&lt;p&gt;Recently, I added a new email notification feature to my N1netails project — a developer-focused alerting tool built with Spring Boot. One of the contributors suggested we look into using MailHog for local email testing, which turned out to be a great fit.&lt;/p&gt;

&lt;p&gt;📬 Now, new users receive a welcome email when signing up, and existing users get alerts via email when something important happens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffeupbq5io66ylwuqr1br.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffeupbq5io66ylwuqr1br.png" alt="Welcome email screenshot" width="712" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgd46xu846f1t7085tt2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgd46xu846f1t7085tt2w.png" alt="Alert email screenshot" width="720" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When deploying to production via &lt;a href="https://app.n1netails.com" rel="noopener noreferrer"&gt;N1netails Dashboard&lt;/a&gt;, I registered a real email address via Outlook. But for local development and testing, I needed a way to simulate sending emails without actually sending them.&lt;/p&gt;

&lt;p&gt;That’s where &lt;a href="https://github.com/mailhog/MailHog" rel="noopener noreferrer"&gt;&lt;strong&gt;MailHog&lt;/strong&gt;&lt;/a&gt; came in — a lightweight, fake SMTP server with a built-in web UI for viewing test emails.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 What is MailHog and Why Use It in a Spring Boot Project?
&lt;/h2&gt;

&lt;p&gt;MailHog is a tool that captures outgoing emails sent from your app, so you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Test email templates without spamming real inboxes&lt;/li&gt;
&lt;li&gt;✅ Debug SMTP configurations locally&lt;/li&gt;
&lt;li&gt;✅ Preview emails in a browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s ideal for Spring Boot developers building user signup flows, alerting systems, or any feature involving transactional emails.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Step-by-Step: How to Use MailHog with Spring Boot
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Install MailHog&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Option A: Binary Download
&lt;/h4&gt;

&lt;p&gt;Download the binary from &lt;a href="https://github.com/mailhog/MailHog/releases" rel="noopener noreferrer"&gt;MailHog GitHub Releases&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example for Windows:&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;MailHog_windows_amd64.exe mailhog.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Option B: Go
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/mailhog/MailHog@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Option C: Docker (Recommended)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 1025:1025 &lt;span class="nt"&gt;-p&lt;/span&gt; 8025:8025 mailhog/mailhog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. &lt;strong&gt;Run MailHog&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you're using Docker, it's already running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SMTP Server → &lt;code&gt;localhost:1025&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Web UI → &lt;a href="http://localhost:8025" rel="noopener noreferrer"&gt;http://localhost:8025&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. &lt;strong&gt;Configure Spring Boot to Use MailHog&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Update your &lt;code&gt;application.yml&lt;/code&gt; (or use Spring profiles):&lt;br&gt;
View how it is configured in n1netails here &lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.18/n1netails-api/src/main/resources/application-email.yml" rel="noopener noreferrer"&gt;application-email.yml&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1025&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support@n1netails.com&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mail.smtp.auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;mail.smtp.starttls.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional: Make it toggleable using environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;n1netails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${EMAIL_ENABLED:false}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. &lt;strong&gt;Trigger an Email&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You can test by creating an account on the live &lt;a href="https://app.n1netails.com" rel="noopener noreferrer"&gt;N1netails Dashboard&lt;/a&gt; — or just simulate it in your code.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. &lt;strong&gt;Preview Emails in the Web UI&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Go to &lt;a href="http://localhost:8025" rel="noopener noreferrer"&gt;http://localhost:8025&lt;/a&gt; to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View email subject and content&lt;/li&gt;
&lt;li&gt;Inspect headers&lt;/li&gt;
&lt;li&gt;Test HTML rendering&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ How N1netails Uses Email Templates (Spring Boot + Liquibase)
&lt;/h2&gt;

&lt;p&gt;Here’s how I wired the backend email flow inside N1netails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Email templates&lt;/strong&gt; are stored in the DB&lt;/li&gt;
&lt;li&gt;I load them via a Spring Data repository&lt;/li&gt;
&lt;li&gt;Dynamic content is injected using &lt;code&gt;{{placeholders}}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Emails are sent using &lt;code&gt;JavaMailSender&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Templates are pre-loaded using Liquibase.&lt;/p&gt;

&lt;h3&gt;
  
  
  📥 SQL Table for Templates
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.18/n1netails-liquibase/src/main/resources/db/changelog/sql/db.changelog-00012-create-notification-template-table.sql" rel="noopener noreferrer"&gt;&lt;code&gt;db.changelog-00012&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ntail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_notification_template&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;html_body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;email_notification_template_pkey&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🎨 HTML Welcome &amp;amp; Alert Email Templates
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.18/n1netails-liquibase/src/main/resources/db/changelog/sql/db.changelog-00013-create-email-welcome-and-alert-templates.sql" rel="noopener noreferrer"&gt;Welcome &amp;amp; Alert Template SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Includes &lt;code&gt;{{username}}&lt;/code&gt;, &lt;code&gt;{{n1netailsEmail}}&lt;/code&gt;, and more&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧬 Spring Boot Code for Sending Emails in N1netails
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Entity:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.18/n1netails-api/src/main/java/com/n1netails/n1netails/api/model/entity/EmailNotificationTemplateEntity.java" rel="noopener noreferrer"&gt;&lt;code&gt;EmailNotificationTemplateEntity.java&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.18/n1netails-api/src/main/java/com/n1netails/n1netails/api/repository/EmailNotificationTemplateRepository.java" rel="noopener noreferrer"&gt;&lt;code&gt;EmailNotificationTemplateRepository.java&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.18/n1netails-api/src/main/java/com/n1netails/n1netails/api/service/impl/EmailServiceImpl.java" rel="noopener noreferrer"&gt;&lt;code&gt;EmailServiceImpl.java&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It fetches the email template by name, applies dynamic parameters, and sends using &lt;code&gt;JavaMailSender&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Summary
&lt;/h2&gt;

&lt;p&gt;With MailHog, I was able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add robust email notifications to N1netails&lt;/li&gt;
&lt;li&gt;Test locally with no risk of spamming users&lt;/li&gt;
&lt;li&gt;Preview and iterate on email designs&lt;/li&gt;
&lt;li&gt;Keep emails stored and maintainable in a database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a &lt;strong&gt;Spring Boot SaaS&lt;/strong&gt;, consider adding MailHog to your local dev workflow!&lt;/p&gt;




&lt;p&gt;💬 If you found this useful or want to see more behind-the-scenes of &lt;a href="https://n1netails.com" rel="noopener noreferrer"&gt;N1netails&lt;/a&gt;, feel free to follow or ask questions. I'm building this open and dev-first. If you are interested in contributing you can view the &lt;a href="https://github.com/n1netails/n1netails" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🦊 Happy coding!&lt;/p&gt;

</description>
      <category>mailhog</category>
      <category>email</category>
      <category>springboot</category>
      <category>programming</category>
    </item>
    <item>
      <title>How I Implemented GitHub OAuth2 into My Spring Boot + Angular App N1netails</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Sat, 02 Aug 2025 04:28:00 +0000</pubDate>
      <link>https://forem.com/shahidfoy/how-i-implemented-github-oauth2-into-my-spring-boot-angular-app-n1netails-5bo</link>
      <guid>https://forem.com/shahidfoy/how-i-implemented-github-oauth2-into-my-spring-boot-angular-app-n1netails-5bo</guid>
      <description>&lt;p&gt;Not everyone wants to log into a website using a basic email and password form. Sometimes it’s due to a lack of trust, and other times, just pure convenience. That’s why many websites now offer OAuth2 login options—reducing the cognitive load of remembering passwords and adding an extra layer of trust via a known provider.&lt;/p&gt;

&lt;p&gt;I recently implemented GitHub OAuth2 login in my &lt;a href="https://app.n1netails.com" rel="noopener noreferrer"&gt;N1netails&lt;/a&gt; application and wanted to share how I got it working.&lt;/p&gt;

&lt;p&gt;N1netails is built using the JASP stack (Java, Angular, Spring Boot, and PostgreSQL). Okay, I made up the acronym, but you get the idea. Spring Boot greatly simplifies OAuth2 integration—it handles most of the "magic" behind the scenes. But integrating it smoothly with an Angular frontend still takes some work. I initially followed &lt;a href="https://spring.io/guides/tutorials/spring-boot-oauth2" rel="noopener noreferrer"&gt;Spring Boot and OAuth2&lt;/a&gt;, but found it lacking guidance for an Angular setup. So here’s the full walkthrough based on what I learned.&lt;/p&gt;

&lt;p&gt;You can also check out the actual pull request here: &lt;a href="https://github.com/n1netails/n1netails/pull/133" rel="noopener noreferrer"&gt;GitHub OAuth2 Implementation Pull Request&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up a GitHub OAuth2 App
&lt;/h3&gt;

&lt;p&gt;Before getting started, you’ll need to create a GitHub OAuth2 App via &lt;a href="https://github.com/settings/developers" rel="noopener noreferrer"&gt;Developer Settings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what I used for the configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application name:&lt;/strong&gt; &lt;code&gt;n1netails-local&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage URL:&lt;/strong&gt; &lt;code&gt;https://localhost:4200&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application description:&lt;/strong&gt; &lt;em&gt;(Add a brief description of your app)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization callback URL:&lt;/strong&gt; &lt;code&gt;http://localhost:9901/login/oauth2/code/github&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sj2nebi9zbwb9bdyuii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sj2nebi9zbwb9bdyuii.png" alt="GitHub OAuth2 App Settings" width="657" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've completed the setup, GitHub will generate a &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt;. Make sure to save these values—they’ll be added to your &lt;code&gt;application.properties&lt;/code&gt; (or &lt;code&gt;application.yml&lt;/code&gt;) later for Spring Boot to use.&lt;/p&gt;




&lt;h3&gt;
  
  
  The OAuth2 Flow (Spring Boot + Angular)
&lt;/h3&gt;

&lt;p&gt;Here’s the basic flow I implemented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Angular login page presents a GitHub login button.&lt;/li&gt;
&lt;li&gt;Clicking the button redirects the user to Spring Boot's OAuth2 authorization endpoint.&lt;/li&gt;
&lt;li&gt;After authentication, Spring Boot generates a JWT and redirects the user back to Angular.&lt;/li&gt;
&lt;li&gt;Angular captures the token and fetches the user profile.&lt;/li&gt;
&lt;li&gt;The app is now authenticated and fully functional for the user.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  1. GitHub Login Button in Angular
&lt;/h3&gt;

&lt;p&gt;On the login page, I created a GitHub login button. When clicked, it redirects the user to the backend for GitHub authorization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.html" rel="noopener noreferrer"&gt;login.component.html&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; 
  &lt;span class="na"&gt;*ngIf=&lt;/span&gt;&lt;span class="s"&gt;"githubAuthEnabled"&lt;/span&gt;
  &lt;span class="na"&gt;nz-button&lt;/span&gt;
  &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"loginWithGithub()"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"login-btn github-login-btn"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;nz-icon&lt;/span&gt; &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"github"&lt;/span&gt; &lt;span class="na"&gt;nzTheme=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"margin-right: 8px;"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
 Login with GitHub
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.ts" rel="noopener noreferrer"&gt;login.component.ts&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;loginWithGithub&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Redirects to Spring Boot's OAuth2 authorization endpoint&lt;/span&gt;
  &lt;span class="c1"&gt;// window.location.href = 'http://localhost:9901/oauth2/authorization/github';&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uiConfigService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getApiUrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/oauth2/authorization/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Backend: Spring Boot OAuth2 Configuration
&lt;/h3&gt;

&lt;p&gt;Start by adding the required Spring Security OAuth2 dependency to your backend:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-oauth2-client&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically enables the &lt;code&gt;/oauth2/authorization/github&lt;/code&gt; endpoint, which Spring Security uses to initiate the OAuth2 login flow.&lt;/p&gt;




&lt;p&gt;Next, configure your OAuth2 settings in &lt;code&gt;application.properties&lt;/code&gt; or &lt;code&gt;application.yml&lt;/code&gt;. Here's what my setup looks like using YAML:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/resources/application-oauth.yml" rel="noopener noreferrer"&gt;application-oauth.yml&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_OAUTH2_ENABLED:true}&lt;/span&gt;
  &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;redirects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${AUTH_OAUTH2_REDIRECT_SUCCESS:http://localhost:4200/#/oauth2/success?token=}&lt;/span&gt;

&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;registration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_ID:your-github-client-id}&lt;/span&gt;
            &lt;span class="na"&gt;client-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_SECRET:your-github-client-secret}&lt;/span&gt;
            &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read:user,user:email&lt;/span&gt;
            &lt;span class="na"&gt;redirect-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{baseUrl}/login/oauth2/code/github"&lt;/span&gt;
            &lt;span class="na"&gt;client-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;authorization-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/login/oauth/authorize&lt;/span&gt;
            &lt;span class="na"&gt;token-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/login/oauth/access_token&lt;/span&gt;
            &lt;span class="na"&gt;user-info-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.github.com/user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;Note:&lt;/strong&gt; Replace &lt;code&gt;your-github-client-id&lt;/code&gt; and &lt;code&gt;your-github-client-secret&lt;/code&gt; with the actual credentials you generated from GitHub. If you're using environment variables (recommended for security), make sure &lt;code&gt;GITHUB_CLIENT_ID&lt;/code&gt; and &lt;code&gt;GITHUB_CLIENT_SECRET&lt;/code&gt; are defined in your environment. Also make sure the GitHub OAuth2 process is enabled for &lt;code&gt;GITHUB_OAUTH2_ENABLED&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This configuration tells Spring Security how to interact with GitHub's OAuth2 endpoints and how to handle the redirect after successful authentication.&lt;/p&gt;




&lt;p&gt;Since my app already supports email/password login via JWT, I created a separate Spring Security config for OAuth2. This allows GitHub auth to be handled with higher priority by using &lt;code&gt;@Order&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/java/com/n1netails/n1netails/api/config/security/Oauth2LoginSecurityConfig.java" rel="noopener noreferrer"&gt;Oauth2LoginSecurityConfig.java&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"auth.github"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"enabled"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Oauth2LoginSecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityMatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/oauth2/**"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/login/**"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/oauth2/**"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/login/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;AbstractHttpConfigurer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionManagement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionCreationPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SessionCreationPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IF_REQUIRED&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2Login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;successHandler&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;OAuth2AuthenticationToken&lt;/span&gt; &lt;span class="n"&gt;oauthToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2AuthenticationToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                    &lt;span class="nc"&gt;OAuth2User&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2User&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrincipal&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

                    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jwtToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loginGithub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oauthToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                    &lt;span class="c1"&gt;// Redirect to Angular app with the token as query param (ex. "http://localhost:4200/oauth2/success?token=" + jwtToken)&lt;/span&gt;
                    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendRedirect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2RedirectsSuccess&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jwtToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="o"&gt;})&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: I used &lt;code&gt;SessionCreationPolicy.IF_REQUIRED&lt;/code&gt; to allow Spring Security to maintain session state only when needed, without affecting my JWT-based setup.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Handling the OAuth2 Login in Backend
&lt;/h3&gt;

&lt;p&gt;This is where most of the GitHub integration logic lives—inside the &lt;code&gt;OAuth2Service&lt;/code&gt;. It handles user retrieval, creation, linking accounts, and generating JWTs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/java/com/n1netails/n1netails/api/service/impl/OAuth2ServiceImpl.java" rel="noopener noreferrer"&gt;OAuth2ServiceImpl.java&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Slf4j&lt;/span&gt;
&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="nd"&gt;@Qualifier&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"oAuth2Service"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuth2ServiceImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Service&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrganizationRepository&lt;/span&gt; &lt;span class="n"&gt;organizationRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;JwtTokenUtil&lt;/span&gt; &lt;span class="n"&gt;jwtTokenUtil&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OAuth2AuthorizedClientService&lt;/span&gt; &lt;span class="n"&gt;authorizedClientService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;RestTemplate&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RestTemplate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;loginGithub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2User&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OAuth2AuthenticationToken&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginGithub"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getting user info from OAuth2User"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GITHUB"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"providerId"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;providerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofNullable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Object:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// may be null&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"avatarUrl"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;avatarUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"avatar_url"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oAuth2User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getting user info from OAuth2User COMPLETED"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;OAuth2AuthorizedClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorizedClientService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAuthorizedClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthorizedClientRegistrationId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccessToken&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTokenValue&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetchPrimaryEmailFromGithub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"@users.noreply.github.com"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 1: Try to find existing user by provider+providerId&lt;/span&gt;
        &lt;span class="nc"&gt;UsersEntity&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByProviderAndProviderId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Step 2: Try to find by email&lt;/span&gt;
            &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findUserByEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Step 3: Link GitHub to existing user&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Linking GitHub to existing user: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProviderId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Step 4: New user registration&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Creating new user from GitHub OAuth2"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UsersEntity&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProviderId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUserId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generateUserId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setJoinDate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setActive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setNotLocked&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;n1netails&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;n1netails&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enumeration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ROLE_USER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAuthorities&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Authority&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;USER_AUTHORITIES&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

                &lt;span class="nc"&gt;OrganizationEntity&lt;/span&gt; &lt;span class="n"&gt;n1netailsOrg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;organizationRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"n1netails"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Default 'n1netails' organization not found."&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setOrganizations&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n1netailsOrg&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Common fields for all paths&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProfileImageUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avatarUrl&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Set names&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;lastSpace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lastIndexOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastSpace&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setFirstName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastSpace&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLastName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastSpace&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setFirstName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLastLoginDateDisplay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLastLoginDate&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLastLoginDate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Saving user from GitHub OAuth2"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jwtTokenUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserPrincipal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;


    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;fetchPrimaryEmailFromGithub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.github.com/user/emails"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;HttpHeaders&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"token "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;HttpEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;HttpMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ParameterizedTypeReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusCode&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;is2xxSuccessful&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBody&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emailObj&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBody&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emailObj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emailObj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"verified"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emailObj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// fallback if no primary email found&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tries to find a user by &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;providerId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If not found, checks by email to potentially link accounts&lt;/li&gt;
&lt;li&gt;If still not found, creates a new user&lt;/li&gt;
&lt;li&gt;Finally, returns a JWT for the session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If GitHub doesn’t provide the email, we fetch it manually using the access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;fetchPrimaryEmailFromGithub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// if primary + verified found&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. Updating the User Entity
&lt;/h3&gt;

&lt;p&gt;I added &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;providerId&lt;/code&gt; fields to track OAuth2 providers like GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/java/com/n1netails/n1netails/api/model/entity/UsersEntity.java" rel="noopener noreferrer"&gt;UsersEntity.java&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// ex. "GITHUB"&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// GitHub user ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. Updating the Repository
&lt;/h3&gt;

&lt;p&gt;To support provider-based lookups:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/java/com/n1netails/n1netails/api/repository/UserRepository.java" rel="noopener noreferrer"&gt;UserRepository.java&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UsersEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByProviderAndProviderId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  6. Redirecting Back to Angular After Successful OAuth2 Login
&lt;/h3&gt;

&lt;p&gt;After successful login, Spring redirects the user to Angular with the JWT token:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/java/com/n1netails/n1netails/api/config/security/Oauth2LoginSecurityConfig.java" rel="noopener noreferrer"&gt;Oauth2LoginSecurityConfig.java&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendRedirect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2RedirectsSuccess&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jwtToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  7. Angular: Capturing the JWT Token
&lt;/h3&gt;

&lt;p&gt;In Angular, I created a route &lt;code&gt;/oauth2/success&lt;/code&gt; and a component to process the token:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-ui/src/main/typescript/src/app/oauth2-success/oauth2-success.component.ts" rel="noopener noreferrer"&gt;oauth2-success.component.ts&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;ngOnInit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelf&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addUserToLocalCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  8. Getting the Authenticated User
&lt;/h3&gt;

&lt;p&gt;The frontend calls the &lt;code&gt;/self&lt;/code&gt; endpoint to fetch the user’s profile using the JWT token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-ui/src/main/typescript/src/app/service/user.service.ts" rel="noopener noreferrer"&gt;user.service.ts&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getSelf&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uiConfigService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getApiUrl&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;/ninetails/user/self`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.16/n1netails-api/src/main/java/com/n1netails/n1netails/api/controller/UserController.java" rel="noopener noreferrer"&gt;UserController.java&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/self"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UsersEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getCurrentUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AUTHORIZATION&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;authorizationHeader&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Wrap-Up and Improvements
&lt;/h3&gt;

&lt;p&gt;That’s the full OAuth2 login integration with GitHub in my Spring Boot + Angular app.&lt;/p&gt;

&lt;p&gt;A few potential improvements I’m considering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a failure handler inside &lt;code&gt;Oauth2LoginSecurityConfig&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Simplifying the logic in the &lt;code&gt;/self&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;Adding support for other providers like Google or LinkedIn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check out the full PR again here: &lt;a href="https://github.com/n1netails/n1netails/pull/133" rel="noopener noreferrer"&gt;GitHub OAuth2 Implementation Pull Request&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re curious about Project N1netails, here are some links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://n1netails.com/" rel="noopener noreferrer"&gt;N1netails Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.n1netails.com/" rel="noopener noreferrer"&gt;N1netails App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>oauth</category>
      <category>springboot</category>
      <category>springsecurity</category>
      <category>angular</category>
    </item>
    <item>
      <title>Fixing OutOfMemoryError in Spring Boot: Implementing Pagination (With Angular Example)</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Sun, 20 Jul 2025 00:35:16 +0000</pubDate>
      <link>https://forem.com/shahidfoy/fixing-outofmemoryerror-in-spring-boot-implementing-pagination-with-angular-example-lp</link>
      <guid>https://forem.com/shahidfoy/fixing-outofmemoryerror-in-spring-boot-implementing-pagination-with-angular-example-lp</guid>
      <description>&lt;p&gt;Earlier this week, I asked other Spring Boot developers to test my side project, &lt;a href="https://app.n1netails.com/" rel="noopener noreferrer"&gt;N1netails Dashboard&lt;/a&gt;—a self-hosted, developer-friendly alerting and monitoring platform built with Spring Boot (backend) and Angular (frontend).&lt;/p&gt;

&lt;p&gt;The goal of N1netails is to provide an open-source alternative to heavy SaaS solutions like PagerDuty, making it easy for developers to send and manage alerts from their applications. It’s designed to be lightweight, customizable, and perfect for small teams or embedded use cases.&lt;/p&gt;

&lt;p&gt;During testing, we discovered a critical issue: my small 1GB DigitalOcean droplet kept crashing with an OutOfMemoryError. The culprit? Some API endpoints were returning all rows from certain database tables at once.&lt;/p&gt;

&lt;p&gt;I knew this was bound to happen eventually, but I didn’t expect it so soon. This reminded me why early testing—especially with QA engineers or security-minded red teamers—is crucial for side projects. Catching performance bottlenecks early saves you from production headaches later.&lt;/p&gt;

&lt;p&gt;The fix was simple but powerful: implementing pagination in both my Spring Boot backend and Angular frontend. Here’s how I did it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is pagination?
&lt;/h2&gt;

&lt;p&gt;Pagination splits large datasets into smaller, manageable pages instead of loading everything at once. This keeps your app responsive and avoids memory crashes.&lt;/p&gt;

&lt;p&gt;Think of Google Search—when you search for something, it doesn’t show millions of results at once. Instead, it loads 10–20 results per page, improving performance and user experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq247qe8xuthr0qd95d8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq247qe8xuthr0qd95d8.png" alt="Google Pagination" width="413" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my case, returning thousands of database rows in a single API response was eating up memory. Pagination limited each request to 50 rows at a time, fixing the OutOfMemoryError instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementing pagination in my Spring Boot application
&lt;/h2&gt;

&lt;p&gt;I implemented pagination in four steps.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;1. Create a &lt;code&gt;PageRequest&lt;/code&gt; POJO&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This object lets the frontend specify page size, sorting, and optional search terms. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-api/src/main/java/com/n1netails/n1netails/api/model/request/PageRequest.java" rel="noopener noreferrer"&gt;PageRequest&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;lombok.Getter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;lombok.Setter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.data.domain.Sort&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Getter&lt;/span&gt;
&lt;span class="nd"&gt;@Setter&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pageNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Direction&lt;/span&gt; &lt;span class="n"&gt;sortDirection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ASC&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sortBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;searchTerm&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ &lt;strong&gt;What each field does&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pageNumber&lt;/code&gt; → Which page to load (starts at 0).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageSize&lt;/code&gt; → How many rows per page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sortDirection&lt;/code&gt; → ASC or DESC.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sortBy&lt;/code&gt; → Which column to sort by.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;searchTerm&lt;/code&gt; → Optional filter for the results.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;2. Update the Repository&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For my &lt;code&gt;tail_type&lt;/code&gt; table, I updated the repository to use Spring Data JPA’s &lt;code&gt;Page&lt;/code&gt; and &lt;code&gt;Pageable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-api/src/main/java/com/n1netails/n1netails/api/repository/TailTypeRepository.java" rel="noopener noreferrer"&gt;TailTypeRepository&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Repository&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;TailTypeRepository&lt;/span&gt; 
    &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PagingAndSortingRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt; 
            &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findTailTypeByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByNameContainingIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;searchTerm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, Spring converts this method into SQL like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tail_type&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;3. Update the Service Layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The service takes the &lt;code&gt;PageRequest&lt;/code&gt;, builds a &lt;code&gt;Pageable&lt;/code&gt;, and maps &lt;code&gt;TailTypeEntity&lt;/code&gt; → &lt;code&gt;TailTypeResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-api/src/main/java/com/n1netails/n1netails/api/service/impl/TailTypeServiceImpl.java" rel="noopener noreferrer"&gt;TailTypeServiceImpl&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getTailTypes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PageRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Sort&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSortDirection&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSortBy&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;springframework&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPageNumber&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPageSize&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tailTypeEntities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
        &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSearchTerm&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSearchTerm&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;tailTypeRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByNameContainingIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSearchTerm&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tailTypeRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tailTypeResponseList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tailTypeEntities&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;generateTailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PageImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;tailTypeResponseList&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tailTypeEntities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalElements&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;4. Expose Pagination in the Controller&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Finally, the controller accepts a &lt;code&gt;PageRequest&lt;/code&gt; and returns a paginated response.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-api/src/main/java/com/n1netails/n1netails/api/controller/TailTypeController.java" rel="noopener noreferrer"&gt;TailTypeController&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Operation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get all tail types"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ApiResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"200"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Paginated tail types"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@Content&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@ArraySchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@Schema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;))))&lt;/span&gt;
&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getTailTypes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@ParameterObject&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tailTypeService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTailTypes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;Angular Implementation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the backend was ready, I added pagination support to the Angular frontend.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;1. Define Interfaces&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;These match the backend request and response.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-ui/src/main/typescript/src/app/model/interface/page.interface.ts" rel="noopener noreferrer"&gt;Page Interface&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PageRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;sortDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;totalElements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// current page number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;2. Create a Pagination Utility Service&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A helper service to generate default pagination params.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-ui/src/main/typescript/src/app/shared/page-util.service.ts" rel="noopener noreferrer"&gt;PageUtilService&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PageUtilService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getPageRequestParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HttpParams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pageNumber&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pageSize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sortDirection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sortDirection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sortBy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;searchTerm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;setDefaultPageRequest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;PageRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sortDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;setDefaultPageRequestWithSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PageRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDefaultPageRequest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;3. Call the API in a Service&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-ui/src/main/typescript/src/app/service/tail-type.service.ts" rel="noopener noreferrer"&gt;TailTypeService&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getTailTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageUtilService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageRequestParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;4. Display Paginated Data&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Finally, I used the service in my settings component:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-ui/src/main/typescript/src/app/pages/settings/settings.component.ts" rel="noopener noreferrer"&gt;Setting component&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;listTailTypes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageUtilService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDefaultPageRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tailTypeService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTailTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TailTypeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tailTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And rendered it in the template:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.12/n1netails-ui/src/main/typescript/src/app/pages/settings/settings.component.html" rel="noopener noreferrer"&gt;Setting component HTML&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h4&amp;gt;&lt;/span&gt;Tail Types&lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;nz-input&lt;/span&gt; &lt;span class="na"&gt;[(ngModel)]=&lt;/span&gt;&lt;span class="s"&gt;"searchTailType"&lt;/span&gt; &lt;span class="na"&gt;(ngModelChange)=&lt;/span&gt;&lt;span class="s"&gt;"searchType()"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"searchTailType"&lt;/span&gt;
      &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search type"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-input"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;nz-button&lt;/span&gt; &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"searchType()"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;nz-icon&lt;/span&gt; &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"search"&lt;/span&gt;
        &lt;span class="na"&gt;nzTheme=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;*ngFor=&lt;/span&gt;&lt;span class="s"&gt;"let type of tailTypes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      {{ type.name }}
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;*ngIf=&lt;/span&gt;&lt;span class="s"&gt;"type.deletable"&lt;/span&gt; &lt;span class="na"&gt;nz-button&lt;/span&gt; &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt; &lt;span class="na"&gt;nzDanger&lt;/span&gt;
        &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"removeAlertType(type.name)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Remove&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;nz-input&lt;/span&gt; &lt;span class="na"&gt;[(ngModel)]=&lt;/span&gt;&lt;span class="s"&gt;"newTailType"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"newTailType"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add new type"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-input"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;nz-button&lt;/span&gt; &lt;span class="na"&gt;nzType=&lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"addAlertType()"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert-btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before pagination, my &lt;code&gt;/ninetails/tail-type&lt;/code&gt; endpoint returned thousands of rows, causing my 1GB DigitalOcean droplet to crash. After implementing pagination, &lt;strong&gt;each request now loads only 50 rows&lt;/strong&gt;, fixing the issue and making the app much faster.&lt;/p&gt;

&lt;p&gt;If you want to test it yourself, check out &lt;a href="https://app.n1netails.com" rel="noopener noreferrer"&gt;N1netails Dashboard&lt;/a&gt; or the &lt;a href="https://github.com/n1netails" rel="noopener noreferrer"&gt;N1netails GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions? Contributions? Join me on &lt;a href="https://discord.gg/ma9CCw7F2x" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>springboot</category>
      <category>java</category>
      <category>angular</category>
    </item>
    <item>
      <title>How I Secured Passwords in My Spring Boot Project (N1netails) Using BCrypt</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Sun, 06 Jul 2025 19:07:40 +0000</pubDate>
      <link>https://forem.com/shahidfoy/how-i-secured-passwords-in-my-spring-boot-project-n1netails-using-bcrypt-4o31</link>
      <guid>https://forem.com/shahidfoy/how-i-secured-passwords-in-my-spring-boot-project-n1netails-using-bcrypt-4o31</guid>
      <description>&lt;p&gt;With the rise of LLMs and the 'vibe coding' movement, many cool ideas are going from concept to code in days or even hours. I'm all for this creative energy—I've always seen programming as a tool to bring ideas to life. But one thing that concerns me is whether these quickly built apps include proper security measures—especially for protecting sensitive data like user passwords.&lt;/p&gt;

&lt;p&gt;If you do not use the right methods for password hashing or if you use outdated hashing methods you risk leaking your customers sensitive information, like their passwords. It's best practice to use a unique password for every site, but most people end up reusing the same one—or small variations of it—because remembering dozens of passwords is nearly impossible without a password manager.&lt;/p&gt;

&lt;p&gt;The reality is I am sure that a lot of these vibe coded websites might be littered with software vulnerabilities. That’s just how things are right now—LLMs often don’t provide solutions for the latest versions of a framework. Instead, they usually suggest code based on older versions they were trained on. And if you're relying on an LLM to generate your backend code without explicitly handling password security, it might suggest saving passwords as plain text—introducing a huge security risk.&lt;/p&gt;

&lt;p&gt;Today I wanted to share how I implemented Bcrypt hashing on my open-source project &lt;a href="https://n1netails.com" rel="noopener noreferrer"&gt;N1netails&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  What are hashing algorithms and how do they help you with protecting user passwords?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxa6di7ysfcc38xhx51ae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxa6di7ysfcc38xhx51ae.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hashing algorithms are mathematical functions that take an input (or "message") and return a fixed-size string of characters, which is typically a hash. The key properties of cryptographic hashing algorithms are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic: The same input always gives the same output.&lt;/li&gt;
&lt;li&gt;Fast computation: Quickly produce the hash.&lt;/li&gt;
&lt;li&gt;Irreversibility: It’s computationally infeasible to reverse the hash to get the original input.&lt;/li&gt;
&lt;li&gt;Collision resistance: It’s hard to find two different inputs that produce the same hash.&lt;/li&gt;
&lt;li&gt;Avalanche effect: A small change in input drastically changes the hash output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Hashing Helps Protect User Passwords
&lt;/h2&gt;

&lt;p&gt;When users create accounts, you don’t store the password directly. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You hash the password using a secure algorithm.&lt;/li&gt;
&lt;li&gt;Store the hashed value in your database.&lt;/li&gt;
&lt;li&gt;During login, hash the password attempt and compare it with the stored hash.
This means that even if someone steals your database, they don’t get actual passwords.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To further improve security, we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Salts: Random values added to the password before hashing to prevent precomputed attacks (rainbow tables).&lt;/li&gt;
&lt;li&gt;Slow hashing: To resist brute-force attacks (e.g., bcrypt, scrypt, Argon2).&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  MD5 vs Bcrypt
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;MD5&lt;/th&gt;
&lt;th&gt;Bcrypt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm Type&lt;/td&gt;
&lt;td&gt;Fast cryptographic hash function&lt;/td&gt;
&lt;td&gt;Adaptive key derivation function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Designed For&lt;/td&gt;
&lt;td&gt;Checksums, not password storage&lt;/td&gt;
&lt;td&gt;Secure password hashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Extremely fast&lt;/td&gt;
&lt;td&gt;Intentionally slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salt Support&lt;/td&gt;
&lt;td&gt;No (must be added manually)&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collision Resistance&lt;/td&gt;
&lt;td&gt;Weak (many known collisions)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adjustable Cost&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;work factor&lt;/code&gt; or &lt;code&gt;cost&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secure?&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes (as of now)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Why MD5 is Bad for Passwords
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt;: Makes brute-force attacks much easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No salting&lt;/strong&gt; by default: Allows precomputed attacks (rainbow tables).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken cryptography&lt;/strong&gt;: Collisions are trivial to generate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not designed for passwords&lt;/strong&gt;: Originally used for checksums (e.g., file integrity).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;MD5 is considered cryptographically broken and should never be used for passwords.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How I used Bcrypt with Spring Security to hash user passwords before they are saved into the database
&lt;/h2&gt;

&lt;p&gt;Hashing passwords can be tricky if you're trying to implement it from scratch. That’s why I decided to use the existing Spring Security framework and the &lt;code&gt;BCryptPasswordEncoder&lt;/code&gt; it provides.&lt;/p&gt;

&lt;p&gt;View the source code here:&lt;br&gt;
&lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.9/n1netails-api/src/main/java/com/n1netails/n1netails/api/config/ProjectSecurityConfig.java" rel="noopener noreferrer"&gt;https://github.com/n1netails/n1netails/blob/v0.2.9/n1netails-api/src/main/java/com/n1netails/n1netails/api/config/ProjectSecurityConfig.java&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectSecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BCryptPasswordEncoder&lt;/span&gt; &lt;span class="n"&gt;passwordEncoder&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationManager&lt;/span&gt; &lt;span class="nf"&gt;authenticationManagerBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;AuthenticationManagerBuilder&lt;/span&gt; &lt;span class="n"&gt;authenticationManagerBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthenticationManagerBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;authenticationManagerBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userDetailsService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userDetailsService&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;passwordEncoder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passwordEncoder&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authenticationManagerBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method above defines a Spring &lt;code&gt;@Bean&lt;/code&gt; that provides a custom &lt;code&gt;AuthenticationManager&lt;/code&gt; for your application. Here’s what it’s doing under the hood:&lt;/p&gt;




&lt;h3&gt;
  
  
  🔍 Method Breakdown
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationManager&lt;/span&gt; &lt;span class="nf"&gt;authenticationManagerBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Declares a Spring bean named &lt;code&gt;authenticationManagerBean&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Returns an instance of &lt;code&gt;AuthenticationManager&lt;/code&gt;, which is used by Spring Security to authenticate users.&lt;/li&gt;
&lt;li&gt;Takes &lt;code&gt;HttpSecurity&lt;/code&gt; as a parameter, so it can access shared security configuration.&lt;/li&gt;
&lt;/ul&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AuthenticationManagerBuilder&lt;/span&gt; &lt;span class="n"&gt;authenticationManagerBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthenticationManagerBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Retrieves the shared &lt;code&gt;AuthenticationManagerBuilder&lt;/code&gt; from the &lt;code&gt;HttpSecurity&lt;/code&gt; context.&lt;/li&gt;
&lt;li&gt;This builder is used to configure how authentication should work.&lt;/li&gt;
&lt;/ul&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;authenticationManagerBuilder&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userDetailsService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userDetailsService&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;passwordEncoder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passwordEncoder&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;userDetailsService(userDetailsService)&lt;/code&gt;&lt;/strong&gt;: Tells Spring how to load user information (e.g., from a database).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;passwordEncoder(passwordEncoder)&lt;/code&gt;&lt;/strong&gt;: Tells Spring how to verify the password (e.g., using &lt;code&gt;BCryptPasswordEncoder&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This configuration links your authentication mechanism to your custom logic.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authenticationManagerBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Finalizes the configuration and returns a fully built &lt;code&gt;AuthenticationManager&lt;/code&gt; bean that will be used by Spring Security to authenticate login attempts.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ What This Achieves
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You provide a &lt;strong&gt;custom way to authenticate users&lt;/strong&gt; (via your &lt;code&gt;UserDetailsService&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;You ensure &lt;strong&gt;secure password handling&lt;/strong&gt; using a specific encoder (e.g., &lt;code&gt;bcrypt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;You expose this &lt;code&gt;AuthenticationManager&lt;/code&gt; bean so it can be used in other parts of your app, like a custom login filter or controller.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔒 Why It's Needed
&lt;/h3&gt;

&lt;p&gt;Spring Security (since version 5.7) recommends defining your own &lt;code&gt;AuthenticationManager&lt;/code&gt; bean if you're doing custom authentication, instead of relying on the default global one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Without this, Spring might not know how to authenticate users, or might not use your desired password encoder.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When used with my &lt;code&gt;SecurityFilterChain&lt;/code&gt; the &lt;code&gt;AuthenticationManager&lt;/code&gt; bean defined above is automatically injected into the filter chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SecurityProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BASIC_AUTH_ORDER&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;securityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PUBLIC_URLS&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔐 The Result
&lt;/h2&gt;

&lt;p&gt;Now users no longer need to worry about their passwords being saved into the database in plain text. The result will now be hashed and will not be useful to hackers if they managed to get into the N1netails database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3j8ypwmvvh81llnuxgar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3j8ypwmvvh81llnuxgar.png" alt=" " width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may notice that some users don’t even have passwords. That’s because I added support for passkey-based logins, which you can read more about here:&lt;br&gt;
&lt;a href="https://dev.to/shahidfoy/adding-passkey-login-to-my-open-source-project-n1netails-using-yubico-and-webauthn-passkeys-2iae"&gt;https://dev.to/shahidfoy/adding-passkey-login-to-my-open-source-project-n1netails-using-yubico-and-webauthn-passkeys-2iae&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sadly after working on my passkey implementation for some time I decided to make passkeys as a secondary login method because I currently do not have a way to validate user emails before sign up which I am planning to implement in the future.&lt;/p&gt;

&lt;p&gt;If you're vibe-coding your next side project, I'm rooting for you! Just remember: security matters, especially when you're handling user data.&lt;/p&gt;

&lt;p&gt;Cheers, and happy coding!&lt;/p&gt;

</description>
      <category>bcrypt</category>
      <category>vibecoding</category>
      <category>springboot</category>
      <category>security</category>
    </item>
    <item>
      <title>🚨 Build Better Alerts: Meet N1netails – A Lightweight, Self-Hosted Alerting System</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Sun, 06 Jul 2025 04:42:54 +0000</pubDate>
      <link>https://forem.com/shahidfoy/build-better-alerts-meet-n1netails-a-lightweight-self-hosted-alerting-system-4kcf</link>
      <guid>https://forem.com/shahidfoy/build-better-alerts-meet-n1netails-a-lightweight-self-hosted-alerting-system-4kcf</guid>
      <description>&lt;p&gt;Managing alerts across modern applications can quickly get complex—or expensive. Whether you're a solo dev, a small team, or running microservices across environments, you need a tool that just works without locking you into a platform or draining your budget.&lt;/p&gt;

&lt;p&gt;That’s why I built &lt;strong&gt;N1netails&lt;/strong&gt; – a developer-first, open-source alerting platform.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧠 What is N1netails?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;N1netails&lt;/strong&gt; is a self-hosted alerting system designed for developers who want more control and less complexity. Inspired by the simplicity of SLF4J for logging, N1netails lets you send and manage alerts with pluggable integrations and full extensibility.&lt;/p&gt;

&lt;p&gt;✅ Built in Java&lt;br&gt;
✅ Send alerts via simple HTTP POST&lt;br&gt;
✅ Works with your existing stack (Spring Boot, Angular, Node, Bash, etc.)&lt;br&gt;
✅ Integrates via &lt;code&gt;n1netails-kuda&lt;/code&gt; (Java library) or REST API&lt;br&gt;
✅ Self-hosted and open-source — no vendor lock-in&lt;br&gt;
✅ Lightweight enough to embed in microservices&lt;br&gt;
✅ Supports real-time notification channels like Discord and Telegram&lt;/p&gt;




&lt;h3&gt;
  
  
  🔧 How It Works
&lt;/h3&gt;

&lt;p&gt;You deploy the &lt;strong&gt;N1netails API&lt;/strong&gt; and &lt;strong&gt;N1netails UI&lt;/strong&gt; and send alerts from your app using the &lt;strong&gt;Kuda SDK&lt;/strong&gt; (or directly via HTTP):&lt;/p&gt;

&lt;h4&gt;
  
  
  📦 Example: &lt;a href="https://n1netails.com/docs/n1netails/n1netails-docker-quickstart" rel="noopener noreferrer"&gt;Docker quickstart&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;If you want to try N1netails out without hosting it yourself you can send posts request to the &lt;a href="https://app.n1netails.com" rel="noopener noreferrer"&gt;N1netails Dashboad&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Alerts appear on the N1netails dashboard.Here is &lt;a href="https://n1netails.com/docs/n1netails/n1netails-post-tail-alert" rel="noopener noreferrer"&gt;how to make a post request to N1netails&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;N1netails Kuda Java SDK&lt;br&gt;
If you are building your services using Java you can use the kuda library to easily send alerts from your app to the N1netails project&lt;br&gt;
&lt;a href="https://n1netails.com/docs/n1netails-kuda/install-and-configure" rel="noopener noreferrer"&gt;N1netails Kuda&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  💡 Use Cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Error alerting&lt;/li&gt;
&lt;li&gt;Health checks for self-hosted systems&lt;/li&gt;
&lt;li&gt;Lightweight incident tracking for indie SaaS&lt;/li&gt;
&lt;li&gt;Default exception handler for Java apps (n1netails-kuda)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🛠️ Under the Hood
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Angular (for the UI)&lt;/li&gt;
&lt;li&gt;RESTful API&lt;/li&gt;
&lt;li&gt;Pluggable alert delivery system  (n1netails-kuda)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🙌 Contribute or Try It Out
&lt;/h3&gt;

&lt;p&gt;If you’re looking for a clean, hackable, dev-focused alternative to PagerDuty, give &lt;code&gt;n1netails&lt;/code&gt; a spin.&lt;/p&gt;

&lt;p&gt;⭐ Star the project: &lt;a href="https://github.com/n1netails/n1netails" rel="noopener noreferrer"&gt;github.com/n1netails/n1netails&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🛠️ Pull requests and feedback are always welcome!&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Built by &lt;a href="https://github.com/shahidfoy" rel="noopener noreferrer"&gt;@shahidfoy&lt;/a&gt; with 🔥 and Tea&lt;/p&gt;
&lt;/blockquote&gt;




</description>
      <category>opensource</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>alerting</category>
    </item>
    <item>
      <title>How to Deploy a Spring Boot and Angular App on DigitalOcean with Docker, Nginx, and HTTPS</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Fri, 04 Jul 2025 18:33:32 +0000</pubDate>
      <link>https://forem.com/shahidfoy/how-to-deploy-a-spring-boot-and-angular-app-on-digitalocean-with-docker-nginx-and-https-2eh2</link>
      <guid>https://forem.com/shahidfoy/how-to-deploy-a-spring-boot-and-angular-app-on-digitalocean-with-docker-nginx-and-https-2eh2</guid>
      <description>&lt;p&gt;I have been working a side project called &lt;a href="https://n1netails.com" rel="noopener noreferrer"&gt;N1netails&lt;/a&gt; which is an open-source project that provides practical alerts and monitoring for applications. I wanted to share what I learned on how to deploy my application to DigitalOcean. One thing to note is the project is primarily written in Springboot. Even the UI is an Angular application wrapped inside of Springboot. So if you are deploying an Angular application by itself you will have to modify some of steps for docker compose and nginx. You can view the result of my deployment here &lt;a href="https://app.n1netails.com" rel="noopener noreferrer"&gt;N1netails Dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  🌐 N1neTails DigitalOcean Deployment Guide
&lt;/h1&gt;

&lt;p&gt;This guide walks you through setting up the &lt;strong&gt;N1neTails&lt;/strong&gt; application with Docker Compose and configuring Nginx as a reverse proxy on a Digitalocean Ubuntu server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents

&lt;ul&gt;
&lt;li&gt;Postgres Database&lt;/li&gt;
&lt;li&gt;Domain or subdomain&lt;/li&gt;
&lt;li&gt;Docker setup&lt;/li&gt;
&lt;li&gt;Nginx setup&lt;/li&gt;
&lt;li&gt;Setup HTTPS with certbot&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Create Postgres Database
&lt;/h2&gt;

&lt;p&gt;For this implementation I decided to use DigitalOcean to host my Database Cluster. &lt;br&gt;
You can read more about setting up Database Clusters on DigitalOcean here: &lt;br&gt;
&lt;a href="https://docs.digitalocean.com/products/databases/postgresql/how-to/create/" rel="noopener noreferrer"&gt;https://docs.digitalocean.com/products/databases/postgresql/how-to/create/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DigitalOcean makes it easy to create new users and databases within your cluster.&lt;br&gt;
Steps 1 &amp;amp; 2 can be done on DigitalOcean but are provided in case your database is hosted elsewhere.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Create &lt;code&gt;n1netails&lt;/code&gt; user (can be done on DigitalOcean):
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;n1netails&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'your_password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Create &lt;code&gt;n1netails&lt;/code&gt; database (can be done on DigitalOcean):
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="nv"&gt;"n1netails"&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="k"&gt;OWNER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n1netails&lt;/span&gt;
  &lt;span class="k"&gt;ENCODING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UTF8'&lt;/span&gt;
  &lt;span class="n"&gt;LOCALE_PROVIDER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'libc'&lt;/span&gt;
  &lt;span class="k"&gt;CONNECTION&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="n"&gt;IS_TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;False&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Grant n1netails user access to n1netails database:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;n1netails&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;n1netails&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 4: Generate &lt;code&gt;ntail&lt;/code&gt; schema:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ntail&lt;/span&gt; &lt;span class="k"&gt;AUTHORIZATION&lt;/span&gt; &lt;span class="n"&gt;n1netails&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 5: Set timezone to UTC:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;n1netails&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'Etc/UTC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Create a Subdomain (e.g., &lt;code&gt;app.n1netails.com&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;You can set up n1netails using your own domain or subdomain.&lt;br&gt;
You can configure DNS settings where you registered your domain (e.g., Squarespace, GoDaddy, Namecheap).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add an A Record&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host&lt;/strong&gt;: &lt;code&gt;app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: &lt;code&gt;A&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt;: Your server’s public IP address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL&lt;/strong&gt;: Default (3600)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the DNS settings.&lt;/p&gt;

&lt;p&gt;📌 &lt;em&gt;It may take a few minutes (or up to 24 hours) for DNS to propagate.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Docker Compose Setup
&lt;/h2&gt;

&lt;p&gt;Set up docker compose on ubuntu by following these docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository" rel="noopener noreferrer"&gt;Docker Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/install/linux/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the following directories within the digital ocean server &lt;code&gt;projects/n1netails&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file inside of &lt;code&gt;projects/n1netails&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shahidfo/n1netails-api:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n1netails-api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9901:9901"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;liquibase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_completed_successfully&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_PROFILE_ACTIVE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_DATASOURCE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://&amp;lt;digital-ocean-postgres-db-url&amp;gt;:&amp;lt;digital-ocean-postgres-db-port&amp;gt;/n1netails&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_DATASOURCE_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n1netails&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_DATASOURCE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;n1netails_password&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;N1NETAILS_PASSKEY_RELYING_PARTY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;domain-url&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# e.g. app.n1netails.com&lt;/span&gt;
      &lt;span class="na"&gt;N1NETAILS_PASSKEY_ORIGINS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;domain-url&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# e.g. https://app.n1netails.com&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your_openai_api_key&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.openai.com&lt;/span&gt;

  &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shahidfo/n1netails-ui:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n1netails-ui&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9900:9900"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_PROFILE_ACTIVE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
      &lt;span class="na"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;domain-url&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;liquibase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shahidfo/n1netails-liquibase:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n1netails-liquibase&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_PROFILE_ACTIVE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_DATASOURCE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://&amp;lt;digital-ocean-postgres-db-url&amp;gt;:&amp;lt;digital-ocean-postgres-db-port&amp;gt;/n1netails&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_DATASOURCE_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n1netails&lt;/span&gt;
      &lt;span class="na"&gt;SPRING_DATASOURCE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;n1netails_password&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prepare Docker Compose&lt;/strong&gt; with above config and deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop and remove old containers&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# Start containers in detached mode&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# tail docker logs (optional)&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Nginx Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: &lt;strong&gt;Install Nginx&lt;/strong&gt; on your server if not installed:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update the package list to ensure you get the latest available versions&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="c"&gt;# Install the Nginx web server&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create the Nginx config file
&lt;/h3&gt;

&lt;p&gt;Create the file &lt;code&gt;/etc/nginx/sites-available/n1netails.conf&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;domain-url&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-XSS-Protection&lt;/span&gt; &lt;span class="s"&gt;"1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;mode=block"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Route API calls to backend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/ninetails/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9901/ninetails/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Route OpenAPI config (Swagger needs this)&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/v3/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9901/v3/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Route Swagger UI to backend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/swagger-ui/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9901/swagger-ui/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Route all other requests to frontend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9900/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Enable the site and disable default
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable the new site&lt;/span&gt;
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/n1netails.conf /etc/nginx/sites-enabled/n1netails.conf

&lt;span class="c"&gt;# Disable the default site&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/nginx/sites-enabled/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Test and reload Nginx
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test the Nginx configuration for syntax errors&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;

&lt;span class="c"&gt;# Reload Nginx to apply the new configuration without downtime&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx

&lt;span class="c"&gt;# Check nginx logs&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/nginx/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Install Certbot &amp;amp; Enable HTTPS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update packages&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="c"&gt;# Install Certbot and Nginx plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Automatically obtain and configure SSL&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose your subdomain (e.g., &lt;code&gt;app.n1netails.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Redirect HTTP to HTTPS when prompted ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ You now have HTTPS enabled.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Verify HTTPS
&lt;/h3&gt;

&lt;p&gt;Visit: &lt;/p&gt;

&lt;p&gt;Visit your domain to varify https&lt;/p&gt;

&lt;p&gt;Example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.n1netails.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check for the padlock icon in the address bar.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Enable SSL Certificate Auto-Renewal
&lt;/h3&gt;

&lt;p&gt;Let’s Encrypt certificates expire every 90 days. Certbot sets up a systemd timer automatically.&lt;/p&gt;

&lt;p&gt;Verify with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-timers | &lt;span class="nb"&gt;grep &lt;/span&gt;certbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the renewal process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>springboot</category>
      <category>angular</category>
      <category>docker</category>
      <category>nginx</category>
    </item>
    <item>
      <title>Adding Passkey Login to My Open Source Project n1netails Using Yubico and WebAuthn</title>
      <dc:creator>Shahid Foy</dc:creator>
      <pubDate>Thu, 03 Jul 2025 00:14:10 +0000</pubDate>
      <link>https://forem.com/shahidfoy/adding-passkey-login-to-my-open-source-project-n1netails-using-yubico-and-webauthn-passkeys-2iae</link>
      <guid>https://forem.com/shahidfoy/adding-passkey-login-to-my-open-source-project-n1netails-using-yubico-and-webauthn-passkeys-2iae</guid>
      <description>&lt;p&gt;Passwords suck.&lt;/p&gt;

&lt;p&gt;They're reused, guessed, phished, and forgotten. So for my open-source alerting platform &lt;a href="https://n1netails.com" rel="noopener noreferrer"&gt;&lt;strong&gt;n1netails&lt;/strong&gt;&lt;/a&gt;, I wanted something better—&lt;strong&gt;passkey authentication&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔐 Why Passkeys?
&lt;/h3&gt;

&lt;p&gt;Passkeys are the modern answer to broken password security. They’re based on &lt;strong&gt;WebAuthn&lt;/strong&gt;, a W3C and FIDO standard that uses &lt;strong&gt;public-key cryptography&lt;/strong&gt; instead of secrets.&lt;/p&gt;

&lt;p&gt;Here's what makes passkeys powerful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phishing-resistant&lt;/strong&gt; – There's no shared secret to steal, so attackers can't trick users into giving up credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device-native&lt;/strong&gt; – Built into OS-level authenticators like Face ID, Touch ID, PIN, Windows Hello, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-friendly&lt;/strong&gt; – One tap or face scan instead of typing a password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-device sync&lt;/strong&gt; – Platforms like Apple and Google sync passkeys across your devices using end-to-end encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server-side secrets&lt;/strong&gt; – Even if your database is compromised, passkey-based credentials can’t be reused or reverse-engineered.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧠 How Passkeys Actually Work (Under the Hood)
&lt;/h3&gt;

&lt;p&gt;Here’s a quick breakdown of the passkey flow from a technical perspective:&lt;/p&gt;

&lt;h4&gt;
  
  
  🔐 Registration (Creating a Passkey)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;The server generates a &lt;strong&gt;challenge&lt;/strong&gt; (random data) and sends it to the browser.&lt;/li&gt;
&lt;li&gt;The browser asks the user to verify themselves using a &lt;strong&gt;platform authenticator&lt;/strong&gt; (e.g., biometric or device PIN).&lt;/li&gt;
&lt;li&gt;If verified, the authenticator creates a &lt;strong&gt;new key pair&lt;/strong&gt; (public/private keys).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;public key&lt;/strong&gt; is sent to the server and stored alongside a unique credential ID.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;private key&lt;/strong&gt; remains securely stored in the device and &lt;strong&gt;never leaves it&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  🔑 Authentication (Logging In)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;The server generates a new challenge and asks the browser to authenticate.&lt;/li&gt;
&lt;li&gt;The browser prompts the user to verify with their platform authenticator.&lt;/li&gt;
&lt;li&gt;The private key on the device signs the challenge.&lt;/li&gt;
&lt;li&gt;The browser sends this &lt;strong&gt;signed challenge&lt;/strong&gt;, along with the credential ID, back to the server.&lt;/li&gt;
&lt;li&gt;The server verifies the signature using the stored public key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the signature is valid, the user is authenticated.&lt;/p&gt;




&lt;h3&gt;
  
  
  📱 What This Means for Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No password hashing or resets&lt;/li&gt;
&lt;li&gt;No secret storage headaches&lt;/li&gt;
&lt;li&gt;Improved security and UX out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Passkeys can be used as a &lt;strong&gt;primary&lt;/strong&gt; or &lt;strong&gt;secondary&lt;/strong&gt; authentication factor. In my project, I'm using them as a secondary login option.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚨 About the Project: n1netails
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/n1netails/n1netails" rel="noopener noreferrer"&gt;N1netails&lt;/a&gt; is a self-hosted, developer-friendly alerting platform designed to be embedded or run independently—think lightweight, programmable PagerDuty.&lt;/p&gt;

&lt;p&gt;I’ve been building this as a way to learn and explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean architecture in Springboot&lt;/li&gt;
&lt;li&gt;Real-time alerts via APIs&lt;/li&gt;
&lt;li&gt;Secure self-hosting with modern auth like passkeys&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔧 How I Implemented Passkeys (WebAuthn)
&lt;/h2&gt;

&lt;p&gt;To implement passkeys, I used the &lt;a href="https://developers.yubico.com/" rel="noopener noreferrer"&gt;Yubico WebAuthn Server library&lt;/a&gt; along with Google &lt;a href="https://jules.google.com/" rel="noopener noreferrer"&gt;Jules&lt;/a&gt; as my peer programming buddy. Since I don't yet have email validation set up in the project, I decided to only allow registered users to generate passkeys. Here's a simplified breakdown of the approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Registration Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An existing user chooses to register a passkey.&lt;/li&gt;
&lt;li&gt;The backend generates a WebAuthn challenge using Yubico's server libraries.&lt;/li&gt;
&lt;li&gt;The client (browser) handles the platform authenticator (Face ID, Touch ID, PIN, Windows Hello, etc.).&lt;/li&gt;
&lt;li&gt;The resulting credential is verified and stored.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Login Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server issues a challenge.&lt;/li&gt;
&lt;li&gt;The client authenticates using a previously registered passkey.&lt;/li&gt;
&lt;li&gt;The server validates the signature and logs the user in.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Yubico library took care of most of the protocol details like challenge generation, signature validation, and credential parsing.&lt;/p&gt;




&lt;h2&gt;
  
  
  📁 Code and Examples
&lt;/h2&gt;

&lt;p&gt;You can view the source code and passkey implementation here:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/n1netails/n1netails/tree/v0.2.7" rel="noopener noreferrer"&gt;GitHub - n1netails passkey implementation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Relevant components include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;N1ntails Liquibase - Generate &lt;code&gt;PasskeyCredentials&lt;/code&gt; table &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-liquibase/src/main/resources/db/changelog/sql/db.changelog-00010-create-table-passkey-credentials.sql" rel="noopener noreferrer"&gt;Create table passkey credentials&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails API - POM Dependencies &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-api/pom.xml" rel="noopener noreferrer"&gt;Yubico Webauthn Dependencies&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails API - Passkey Data Transfer Objects (DTO) &lt;a href="https://github.com/n1netails/n1netails/tree/v0.2.7/n1netails-api/src/main/java/com/n1netails/n1netails/api/model/dto/passkey" rel="noopener noreferrer"&gt;Passkey DTO's&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails API - Registration and login controllers &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-api/src/main/java/com/n1netails/n1netails/api/controller/PasskeyController.java" rel="noopener noreferrer"&gt;Passkey Controller&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails API - Integration with session-based login system &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-api/src/main/java/com/n1netails/n1netails/api/service/impl/PasskeyServiceImpl.java" rel="noopener noreferrer"&gt;Passkey Service&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails API - Saving passkey data in the database &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-api/src/main/java/com/n1netails/n1netails/api/repository/impl/PasskeyCredentialRepositoryImpl.java" rel="noopener noreferrer"&gt;Passkey Credential Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails API - Yubico credential repository helper &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-api/src/main/java/com/n1netails/n1netails/api/repository/impl/YubicoCredentialRepositoryImpl.java" rel="noopener noreferrer"&gt;Yubico Credential Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails UI - Passkey service &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-ui/src/main/typescript/src/app/service/passkey.service.ts" rel="noopener noreferrer"&gt;Angular passkey service&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails UI - Edit profile register passkey &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-ui/src/main/typescript/src/app/pages/edit-profile/edit-profile.component.ts" rel="noopener noreferrer"&gt;Start passkey Registration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N1netails UI - Login user with passkey &lt;a href="https://github.com/n1netails/n1netails/blob/v0.2.7/n1netails-ui/src/main/typescript/src/app/pages/login/login.component.ts" rel="noopener noreferrer"&gt;Login with passkey&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Passkeys aren’t the future—they’re &lt;strong&gt;now&lt;/strong&gt;. If you’re building a product where security and usability matter, adding WebAuthn is well worth it. Using &lt;a href="https://jules.google.com" rel="noopener noreferrer"&gt;Google Jules&lt;/a&gt; as my peer programming buddy helped me get started, but it didn’t work completely out of the box. I had to tweak the database schema and refactor parts of the Passkey service and repository layers to get it working properly. If you are using Passkeys with email it is probably a good idea to make sure the user can validate their email before generating a passkey.&lt;/p&gt;

&lt;p&gt;n1netails is still in its early stages, but you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ Star the repo: &lt;a href="https://github.com/n1netails/n1netails" rel="noopener noreferrer"&gt;Github N1netails&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📄 Read the docs: &lt;a href="https://n1netails.com" rel="noopener noreferrer"&gt;n1netails.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 Try it out and share feedback!&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>passkeys</category>
      <category>security</category>
      <category>fullstack</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
