Add resend confirmation
Seto Elkahfi committed
Jun 17, 2026 at 07:30 UTC
c78b320d9a9610039bf3a4ce03fe5f5d956c33b4
7 files changed
+174
-8
app/controllers/confirmations_controller.rb
+42
new file mode 100644
index 0000000..821dd99
--- /dev/null
+++ b/app/controllers/confirmations_controller.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class ConfirmationsController < ApplicationController
+ before_action :redirect_if_signed_in
+
+ # GET /auth/confirmation/resend
+ def new
+ end
+
+ # POST /auth/confirmation/resend
+ def create
+ email = params[:email].to_s.strip.downcase
+
+ if email.blank?
+ flash.now[:alert] = "Email is required."
+ return render :new, status: :unprocessable_entity
+ end
+
+ SmbcloudAuthService.resend_confirmation(email: email)
+
+ # The endpoint never reveals whether the account exists or is already
+ # confirmed, so we always show the same neutral message.
+ redirect_to signin_path,
+ notice: "If that email has an unconfirmed account, we've sent a new confirmation link. Please check your inbox."
+
+ rescue KeyError => e
+ Rails.logger.error("smbCloud configuration error during resend confirmation: #{e.message}")
+ flash.now[:alert] = "Authentication service is not configured. Please contact support."
+ render :new, status: :internal_server_error
+
+ rescue SmbcloudAuthService::AuthenticationError => e
+ Rails.logger.warn("resend confirmation failed for #{email.inspect}: #{e.message}")
+ flash.now[:alert] = "Something went wrong sending the confirmation email. Please try again."
+ render :new, status: :internal_server_error
+ end
+
+ private
+
+ def redirect_if_signed_in
+ redirect_to root_path, notice: "You are already signed in." if signed_in?
+ end
+end
app/services/smbcloud_auth_service.rb
+66
-5
index 64cd969..611fd84 100644
--- a/app/services/smbcloud_auth_service.rb
+++ b/app/services/smbcloud_auth_service.rb
@@ -24,6 +24,8 @@
# SIGITSI_SMBCLOUD_ENVIRONMENT – "production" (default) or "dev"
#
require "auth"
+require "net/http"
+require "json"
class SmbcloudAuthService
class AuthenticationError < StandardError
@@ -113,11 +115,23 @@ class SmbcloudAuthService
raise AuthenticationError.new(e.message, error_code: e.error_code)
end
+ # Asks smbCloud to resend the confirmation email for an unconfirmed account.
+ #
+ # The gem does not wrap this endpoint, so we call it directly. The endpoint
+ # intentionally always returns 200 with a neutral message (no account
+ # enumeration), so this returns the parsed body and never reveals whether the
+ # email belongs to a real or still-unconfirmed account.
+ def self.resend_confirmation(email:)
+ post_client("v1/client/users/resend_confirmation", user: { email: email })
+ end
+
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
- def self.smbcloud_environment
+ # Resolves the configured environment to :dev or :production.
+ # In development the dev panel can override via Rails.cache.
+ def self.resolved_environment_name
raw = if Rails.env.development?
Rails.cache.read("dev:smbcloud_environment") ||
ENV.fetch("SIGITSI_SMBCLOUD_ENVIRONMENT", "dev")
@@ -126,12 +140,58 @@ class SmbcloudAuthService
end.strip.downcase
case raw
- when "dev", "development" then SmbCloud::Auth::Environment::DEV
- when "production" then SmbCloud::Auth::Environment::PRODUCTION
+ when "dev", "development" then :dev
+ when "production" then :production
else
Rails.logger.warn("Unknown SIGITSI_SMBCLOUD_ENVIRONMENT '#{raw}', defaulting to production.")
- SmbCloud::Auth::Environment::PRODUCTION
+ :production
+ end
+ end
+
+ def self.smbcloud_environment
+ case resolved_environment_name
+ when :dev then SmbCloud::Auth::Environment::DEV
+ else SmbCloud::Auth::Environment::PRODUCTION
+ end
+ end
+
+ # Base URL for direct HTTP calls to tenant endpoints the gem doesn't wrap.
+ # Mirrors the gem's Environment hosts (dev: localhost:8088, prod: api.smbcloud.xyz).
+ def self.smbcloud_base_url
+ case resolved_environment_name
+ when :dev then "http://localhost:8088"
+ else "https://api.smbcloud.xyz"
+ end
+ end
+
+ # POSTs a JSON payload to a tenant client endpoint, authenticating with the
+ # app's client_id/client_secret query params (the same shape the gem uses).
+ # Returns the parsed JSON body, or {} when the body is empty.
+ def self.post_client(path, payload)
+ uri = URI.parse("#{smbcloud_base_url}/#{path}")
+ uri.query = URI.encode_www_form(
+ client_id: smbcloud_app_id,
+ client_secret: smbcloud_app_secret
+ )
+
+ request = Net::HTTP::Post.new(uri)
+ request["Content-Type"] = "application/json"
+ request["User-Agent"] = smbcloud_app_id
+ request.body = payload.to_json
+
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
+ http.request(request)
end
+
+ unless response.is_a?(Net::HTTPSuccess)
+ raise AuthenticationError.new("smbCloud request failed (HTTP #{response.code}).")
+ end
+
+ response.body.to_s.empty? ? {} : JSON.parse(response.body)
+ rescue AuthenticationError, KeyError
+ raise
+ rescue StandardError => e
+ raise AuthenticationError.new(e.message)
end
def self.smbcloud_app_id
@@ -146,5 +206,6 @@ class SmbcloudAuthService
end
end
- private_class_method :client, :smbcloud_environment, :smbcloud_app_id, :smbcloud_app_secret
+ private_class_method :client, :resolved_environment_name, :smbcloud_environment,
+ :smbcloud_base_url, :post_client, :smbcloud_app_id, :smbcloud_app_secret
end
app/views/confirmations/new.html.erb
+54
new file mode 100644
index 0000000..47786b4
--- /dev/null
+++ b/app/views/confirmations/new.html.erb
@@ -0,0 +1,54 @@
+<% content_for :title, "Resend confirmation" %>
+
+<div class="min-h-screen bg-surface-900 flex flex-col items-center justify-center px-4 py-16">
+
+ <div class="mb-10 flex flex-col items-center gap-3">
+ <%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
+ <img src="/icon.png" alt="siGit" class="h-9 w-auto">
+ <span class="text-xl font-semibold tracking-tight">Code & Deploy</span>
+ <% end %>
+ <p class="text-sm text-gray-500">Resend confirmation email</p>
+ </div>
+
+ <div class="w-full max-w-sm">
+ <div class="border border-surface-500 rounded bg-surface-700 px-8 py-8">
+
+ <% if flash[:alert].present? %>
+ <div class="mb-5 flex items-start gap-2.5 rounded border border-red-800/40 bg-red-900/20 px-3.5 py-3 text-sm text-red-400" role="alert">
+ <svg class="w-4 h-4 mt-0.5 shrink-0" viewBox="0 0 16 16" fill="currentColor">
+ <path d="M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 3.75a.75.75 0 0 0-.75.75v3.5a.75.75 0 0 0 1.5 0v-3.5A.75.75 0 0 0 8 4.75zm0 7.5a.875.875 0 1 0 0-1.75.875.875 0 0 0 0 1.75z"/>
+ </svg>
+ <span><%= flash[:alert] %></span>
+ </div>
+ <% end %>
+
+ <p class="mb-5 text-sm text-gray-400 leading-relaxed">
+ Enter your email and we'll send a new confirmation link if your account
+ hasn't been verified yet.
+ </p>
+
+ <%= form_tag resend_confirmation_path, method: :post, class: "space-y-5" do %>
+
+ <div>
+ <label for="email" class="form-label">Email</label>
+ <input type="email" id="email" name="email"
+ class="form-input" autocomplete="email"
+ value="<%= params[:email].to_s %>"
+ placeholder="you@example.com"
+ autofocus required>
+ </div>
+
+ <button type="submit" class="btn-primary w-full justify-center py-2.5 mt-2">
+ Resend confirmation email
+ </button>
+
+ <% end %>
+ </div>
+
+ <p class="mt-6 text-center text-xs text-gray-500">
+ Back to
+ <%= link_to "Sign in", signin_path, class: "text-gray-400 hover:text-gray-200 underline underline-offset-2" %>
+ </p>
+ </div>
+
+</div>
app/views/registrations/new.html.erb
+1
-1
index 041ea0c..95bdf0f 100644
--- a/app/views/registrations/new.html.erb
+++ b/app/views/registrations/new.html.erb
@@ -5,7 +5,7 @@
<div class="mb-10 flex flex-col items-center gap-3">
<%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
<img src="/icon.png" alt="siGit" class="h-9 w-auto">
- <span class="text-xl font-semibold tracking-tight">siGit Code & Deploy</span>
+ <span class="text-xl font-semibold tracking-tight">Code & Deploy</span>
<% end %>
<p class="text-sm text-gray-500">Create your account</p>
</div>
app/views/sessions/new.html.erb
+6
-1
index 976b244..33497a0 100644
--- a/app/views/sessions/new.html.erb
+++ b/app/views/sessions/new.html.erb
@@ -5,7 +5,7 @@
<div class="mb-10 flex flex-col items-center gap-3">
<%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
<img src="/icon.png" alt="siGit" class="h-9 w-auto">
- <span class="text-xl font-semibold tracking-tight">siGit Code & Deploy</span>
+ <span class="text-xl font-semibold tracking-tight">Code & Deploy</span>
<% end %>
<p class="text-sm text-gray-500">Sign in to continue</p>
</div>
@@ -51,6 +51,11 @@
No account?
<%= link_to "Create one", signup_path, class: "text-gray-400 hover:text-gray-200 underline underline-offset-2" %>
</p>
+
+ <p class="mt-3 text-center text-xs text-gray-500">
+ Didn't get a confirmation email?
+ <%= link_to "Resend it", resend_confirmation_path, class: "text-gray-400 hover:text-gray-200 underline underline-offset-2" %>
+ </p>
</div>
</div>
app/views/shared/_navbar.html.erb
+1
-1
index 8a77717..ba64dbb 100644
--- a/app/views/shared/_navbar.html.erb
+++ b/app/views/shared/_navbar.html.erb
@@ -4,7 +4,7 @@
<div class="flex items-center gap-6">
<%= link_to root_path, class: "flex items-center gap-2 text-gray-100 hover:text-brand-500 transition-colors" do %>
<img src="/icon.png" alt="siGit" class="h-6 w-auto">
- <span class="font-semibold text-base tracking-tight">siGit Code & Deploy</span>
+ <span class="font-semibold text-base tracking-tight">Code & Deploy</span>
<% end %>
<% if signed_in? %>
config/routes.rb
+4
index bb85104..66b7b9b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,6 +18,10 @@ Rails.application.routes.draw do
get "/auth/signup", to: "registrations#new", as: :signup
post "/auth/signup", to: "registrations#create", as: :signup_create
+ # Resend confirmation email
+ get "/auth/confirmation/resend", to: "confirmations#new", as: :resend_confirmation
+ post "/auth/confirmation/resend", to: "confirmations#create"
+
# Repository creation
get "/new", to: "repositories#new", as: :new_repository
post "/new", to: "repositories#create"