Add resend confirmation
Seto Elkahfi committed
Jun 17, 2026 at 07:30 UTC
c78b320d9a9610039bf3a4ce03fe5f5d956c33b4
7 files changed
+174
-8
app/controllers/confirmations_controller.rb
new
+42
@@ -0,0 +1,42 @@
1
+# frozen_string_literal: true
2
+
3
+class ConfirmationsController < ApplicationController
4
+ before_action :redirect_if_signed_in
5
+
6
+ # GET /auth/confirmation/resend
7
+ def new
8
+ end
9
+
10
+ # POST /auth/confirmation/resend
11
+ def create
12
+ email = params[:email].to_s.strip.downcase
13
+
14
+ if email.blank?
15
+ flash.now[:alert] = "Email is required."
16
+ return render :new, status: :unprocessable_entity
17
+ end
18
+
19
+ SmbcloudAuthService.resend_confirmation(email: email)
20
+
21
+ # The endpoint never reveals whether the account exists or is already
22
+ # confirmed, so we always show the same neutral message.
23
+ redirect_to signin_path,
24
+ notice: "If that email has an unconfirmed account, we've sent a new confirmation link. Please check your inbox."
25
+
26
+ rescue KeyError => e
27
+ Rails.logger.error("smbCloud configuration error during resend confirmation: #{e.message}")
28
+ flash.now[:alert] = "Authentication service is not configured. Please contact support."
29
+ render :new, status: :internal_server_error
30
+
31
+ rescue SmbcloudAuthService::AuthenticationError => e
32
+ Rails.logger.warn("resend confirmation failed for #{email.inspect}: #{e.message}")
33
+ flash.now[:alert] = "Something went wrong sending the confirmation email. Please try again."
34
+ render :new, status: :internal_server_error
35
+ end
36
+
37
+ private
38
+
39
+ def redirect_if_signed_in
40
+ redirect_to root_path, notice: "You are already signed in." if signed_in?
41
+ end
42
+end
app/services/smbcloud_auth_service.rb
+66
-5
@@ -24,6 +24,8 @@
24
# SIGITSI_SMBCLOUD_ENVIRONMENT – "production" (default) or "dev"
25
#
26
require "auth"
27
+require "net/http"
28
+require "json"
29
30
class SmbcloudAuthService
31
class AuthenticationError < StandardError
@@ -113,11 +115,23 @@ class SmbcloudAuthService
115
raise AuthenticationError.new(e.message, error_code: e.error_code)
116
end
117
118
+ # Asks smbCloud to resend the confirmation email for an unconfirmed account.
119
+ #
120
+ # The gem does not wrap this endpoint, so we call it directly. The endpoint
121
+ # intentionally always returns 200 with a neutral message (no account
122
+ # enumeration), so this returns the parsed body and never reveals whether the
123
+ # email belongs to a real or still-unconfirmed account.
124
+ def self.resend_confirmation(email:)
125
+ post_client("v1/client/users/resend_confirmation", user: { email: email })
126
+ end
127
+
128
# ---------------------------------------------------------------------------
129
# Private helpers
130
# ---------------------------------------------------------------------------
131
120
- def self.smbcloud_environment
132
+ # Resolves the configured environment to :dev or :production.
133
+ # In development the dev panel can override via Rails.cache.
134
+ def self.resolved_environment_name
135
raw = if Rails.env.development?
136
Rails.cache.read("dev:smbcloud_environment") ||
137
ENV.fetch("SIGITSI_SMBCLOUD_ENVIRONMENT", "dev")
@@ -126,12 +140,58 @@ class SmbcloudAuthService
140
end.strip.downcase
141
142
case raw
129
- when "dev", "development" then SmbCloud::Auth::Environment::DEV
130
- when "production" then SmbCloud::Auth::Environment::PRODUCTION
143
+ when "dev", "development" then :dev
144
+ when "production" then :production
145
else
146
Rails.logger.warn("Unknown SIGITSI_SMBCLOUD_ENVIRONMENT '#{raw}', defaulting to production.")
133
- SmbCloud::Auth::Environment::PRODUCTION
147
+ :production
148
+ end
149
+ end
150
+
151
+ def self.smbcloud_environment
152
+ case resolved_environment_name
153
+ when :dev then SmbCloud::Auth::Environment::DEV
154
+ else SmbCloud::Auth::Environment::PRODUCTION
155
+ end
156
+ end
157
+
158
+ # Base URL for direct HTTP calls to tenant endpoints the gem doesn't wrap.
159
+ # Mirrors the gem's Environment hosts (dev: localhost:8088, prod: api.smbcloud.xyz).
160
+ def self.smbcloud_base_url
161
+ case resolved_environment_name
162
+ when :dev then "http://localhost:8088"
163
+ else "https://api.smbcloud.xyz"
164
+ end
165
+ end
166
+
167
+ # POSTs a JSON payload to a tenant client endpoint, authenticating with the
168
+ # app's client_id/client_secret query params (the same shape the gem uses).
169
+ # Returns the parsed JSON body, or {} when the body is empty.
170
+ def self.post_client(path, payload)
171
+ uri = URI.parse("#{smbcloud_base_url}/#{path}")
172
+ uri.query = URI.encode_www_form(
173
+ client_id: smbcloud_app_id,
174
+ client_secret: smbcloud_app_secret
175
+ )
176
+
177
+ request = Net::HTTP::Post.new(uri)
178
+ request["Content-Type"] = "application/json"
179
+ request["User-Agent"] = smbcloud_app_id
180
+ request.body = payload.to_json
181
+
182
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
183
+ http.request(request)
184
end
185
+
186
+ unless response.is_a?(Net::HTTPSuccess)
187
+ raise AuthenticationError.new("smbCloud request failed (HTTP #{response.code}).")
188
+ end
189
+
190
+ response.body.to_s.empty? ? {} : JSON.parse(response.body)
191
+ rescue AuthenticationError, KeyError
192
+ raise
193
+ rescue StandardError => e
194
+ raise AuthenticationError.new(e.message)
195
end
196
197
def self.smbcloud_app_id
@@ -146,5 +206,6 @@ class SmbcloudAuthService
206
end
207
end
208
149
- private_class_method :client, :smbcloud_environment, :smbcloud_app_id, :smbcloud_app_secret
209
+ private_class_method :client, :resolved_environment_name, :smbcloud_environment,
210
+ :smbcloud_base_url, :post_client, :smbcloud_app_id, :smbcloud_app_secret
211
end
app/views/confirmations/new.html.erb
new
+54
@@ -0,0 +1,54 @@
1
+<% content_for :title, "Resend confirmation" %>
2
+
3
+<div class="min-h-screen bg-surface-900 flex flex-col items-center justify-center px-4 py-16">
4
+
5
+ <div class="mb-10 flex flex-col items-center gap-3">
6
+ <%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
7
+ <img src="/icon.png" alt="siGit" class="h-9 w-auto">
8
+ <span class="text-xl font-semibold tracking-tight">Code & Deploy</span>
9
+ <% end %>
10
+ <p class="text-sm text-gray-500">Resend confirmation email</p>
11
+ </div>
12
+
13
+ <div class="w-full max-w-sm">
14
+ <div class="border border-surface-500 rounded bg-surface-700 px-8 py-8">
15
+
16
+ <% if flash[:alert].present? %>
17
+ <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">
18
+ <svg class="w-4 h-4 mt-0.5 shrink-0" viewBox="0 0 16 16" fill="currentColor">
19
+ <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"/>
20
+ </svg>
21
+ <span><%= flash[:alert] %></span>
22
+ </div>
23
+ <% end %>
24
+
25
+ <p class="mb-5 text-sm text-gray-400 leading-relaxed">
26
+ Enter your email and we'll send a new confirmation link if your account
27
+ hasn't been verified yet.
28
+ </p>
29
+
30
+ <%= form_tag resend_confirmation_path, method: :post, class: "space-y-5" do %>
31
+
32
+ <div>
33
+ <label for="email" class="form-label">Email</label>
34
+ <input type="email" id="email" name="email"
35
+ class="form-input" autocomplete="email"
36
+ value="<%= params[:email].to_s %>"
37
+ placeholder="you@example.com"
38
+ autofocus required>
39
+ </div>
40
+
41
+ <button type="submit" class="btn-primary w-full justify-center py-2.5 mt-2">
42
+ Resend confirmation email
43
+ </button>
44
+
45
+ <% end %>
46
+ </div>
47
+
48
+ <p class="mt-6 text-center text-xs text-gray-500">
49
+ Back to
50
+ <%= link_to "Sign in", signin_path, class: "text-gray-400 hover:text-gray-200 underline underline-offset-2" %>
51
+ </p>
52
+ </div>
53
+
54
+</div>
app/views/registrations/new.html.erb
+1
-1
@@ -5,7 +5,7 @@
5
<div class="mb-10 flex flex-col items-center gap-3">
6
<%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
7
<img src="/icon.png" alt="siGit" class="h-9 w-auto">
8
- <span class="text-xl font-semibold tracking-tight">siGit Code & Deploy</span>
8
+ <span class="text-xl font-semibold tracking-tight">Code & Deploy</span>
9
<% end %>
10
<p class="text-sm text-gray-500">Create your account</p>
11
</div>
app/views/sessions/new.html.erb
+6
-1
@@ -5,7 +5,7 @@
5
<div class="mb-10 flex flex-col items-center gap-3">
6
<%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
7
<img src="/icon.png" alt="siGit" class="h-9 w-auto">
8
- <span class="text-xl font-semibold tracking-tight">siGit Code & Deploy</span>
8
+ <span class="text-xl font-semibold tracking-tight">Code & Deploy</span>
9
<% end %>
10
<p class="text-sm text-gray-500">Sign in to continue</p>
11
</div>
@@ -51,6 +51,11 @@
51
No account?
52
<%= link_to "Create one", signup_path, class: "text-gray-400 hover:text-gray-200 underline underline-offset-2" %>
53
</p>
54
+
55
+ <p class="mt-3 text-center text-xs text-gray-500">
56
+ Didn't get a confirmation email?
57
+ <%= link_to "Resend it", resend_confirmation_path, class: "text-gray-400 hover:text-gray-200 underline underline-offset-2" %>
58
+ </p>
59
</div>
60
61
</div>
app/views/shared/_navbar.html.erb
+1
-1
@@ -4,7 +4,7 @@
4
<div class="flex items-center gap-6">
5
<%= link_to root_path, class: "flex items-center gap-2 text-gray-100 hover:text-brand-500 transition-colors" do %>
6
<img src="/icon.png" alt="siGit" class="h-6 w-auto">
7
- <span class="font-semibold text-base tracking-tight">siGit Code & Deploy</span>
7
+ <span class="font-semibold text-base tracking-tight">Code & Deploy</span>
8
<% end %>
9
10
<% if signed_in? %>
config/routes.rb
+4
@@ -18,6 +18,10 @@ Rails.application.routes.draw do
18
get "/auth/signup", to: "registrations#new", as: :signup
19
post "/auth/signup", to: "registrations#create", as: :signup_create
20
21
+ # Resend confirmation email
22
+ get "/auth/confirmation/resend", to: "confirmations#new", as: :resend_confirmation
23
+ post "/auth/confirmation/resend", to: "confirmations#create"
24
+
25
# Repository creation
26
get "/new", to: "repositories#new", as: :new_repository
27
post "/new", to: "repositories#create"