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 &amp; 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 &amp; Deploy</span> + <span class="text-xl font-semibold tracking-tight">Code &amp; 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 &amp; Deploy</span> + <span class="text-xl font-semibold tracking-tight">Code &amp; 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 &amp; Deploy</span> + <span class="font-semibold text-base tracking-tight">Code &amp; 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"