Skip to content

Commit ff23e6b

Browse files
committed
Fixing manual registration problems
* Properly register names during registration time, instead of verification * Added SMTP setup
1 parent 6dab1e1 commit ff23e6b

File tree

9 files changed

+453
-29
lines changed

9 files changed

+453
-29
lines changed

app/controllers/registrations_controller.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ def create
1414
return redirect_to registration_path, alert: "Username and password are required."
1515
end
1616

17-
if User.exists?(username: username)
18-
return redirect_to registration_path, alert: "Username is already taken."
17+
if password != password_confirmation
18+
return redirect_to registration_path, alert: "Password confirmation does not match."
1919
end
2020

21+
password_digest = BCrypt::Password.create(password)
22+
2123
purpose = 'register'
2224
ttl = 1.hour
2325
token, raw = UserToken.issue!(
@@ -27,11 +29,17 @@ def create
2729
metadata: {
2830
name: name,
2931
username: username,
30-
password: password,
31-
password_confirmation: password_confirmation
32+
password_digest: password_digest
3233
}.to_json
3334
)
3435

36+
begin
37+
NameReservation.reserve!(name: username, owner: token)
38+
rescue ActiveRecord::RecordInvalid
39+
token.destroy
40+
return redirect_to registration_path, alert: "Username is already taken."
41+
end
42+
3543
UserMailer.verification_email(token, raw).deliver_later
3644
redirect_to root_path, notice: 'Verification email sent. Please check your inbox.'
3745
end

app/controllers/verifications_controller.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ def show
2121

2222
def handle_register(token)
2323
existing_aliases = Alias.by_email(token.email)
24-
# If email already linked to another user, block registration
2524
if existing_aliases.where.not(user_id: nil).exists?
2625
return redirect_to new_session_path, alert: 'This email is already claimed. Please sign in.'
2726
end
@@ -30,27 +29,34 @@ def handle_register(token)
3029
metadata = JSON.parse(token.metadata || '{}') rescue {}
3130
desired_username = metadata['username']
3231
user.username = desired_username
33-
if metadata['password'].present?
34-
user.password = metadata['password']
35-
user.password_confirmation = metadata['password_confirmation']
32+
if metadata['password_digest'].present?
33+
user.password_digest = metadata['password_digest']
3634
end
37-
begin
35+
36+
ActiveRecord::Base.transaction do
3837
user.save!(context: :registration)
39-
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
40-
if e.message =~ /username/i
41-
return redirect_to new_registration_path, alert: "Username is already taken."
38+
39+
reservation = NameReservation.find_by(
40+
owner_type: 'UserToken',
41+
owner_id: token.id,
42+
name: NameReservation.normalize(desired_username)
43+
)
44+
if reservation
45+
reservation.update!(owner_type: 'User', owner_id: user.id)
4246
else
43-
raise
47+
begin
48+
NameReservation.reserve!(name: desired_username, owner: user)
49+
rescue ActiveRecord::RecordInvalid
50+
raise ActiveRecord::RecordInvalid.new(user), "Username is already taken."
51+
end
4452
end
4553
end
4654

4755
if existing_aliases.exists?
4856
existing_aliases.update_all(user_id: user.id, verified_at: Time.current)
49-
# Ensure one primary alias
5057
primary = existing_aliases.find_by(primary_alias: true) || existing_aliases.first
5158
primary.update!(primary_alias: true)
5259
else
53-
# Use provided name if any
5460
name = metadata['name'] || token.email
5561
Alias.create!(user: user, name: name, email: token.email, primary_alias: true, verified_at: Time.current)
5662
end
@@ -63,7 +69,6 @@ def handle_register(token)
6369
def handle_add_alias(token)
6470
require_authentication
6571
email = token.email
66-
# Block if email belongs to another active user
6772
if Alias.by_email(email).where.not(user_id: [nil, current_user.id]).exists?
6873
return redirect_to settings_path, alert: 'Email is linked to another account. Delete that account first to release it.'
6974
end

app/models/user_token.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ class UserToken < ApplicationRecord
99

1010
scope :unconsumed, -> { where(consumed_at: nil) }
1111

12+
after_destroy :release_name_reservation
13+
1214
def consumed?
1315
consumed_at.present?
1416
end
@@ -47,4 +49,14 @@ def self.consume!(raw, purpose: nil)
4749
def self.digest(raw)
4850
OpenSSL::Digest::SHA256.hexdigest(raw)
4951
end
52+
53+
def self.cleanup_expired!(older_than: 1.day)
54+
where('expires_at < ? OR consumed_at < ?', older_than.ago, older_than.ago).destroy_all
55+
end
56+
57+
private
58+
59+
def release_name_reservation
60+
NameReservation.release_for(self)
61+
end
5062
end

config/environments/production.rb

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,31 @@
5555
config.active_job.queue_adapter = :solid_queue
5656
config.solid_queue.connects_to = { database: { writing: :queue } }
5757

58-
# Ignore bad email addresses and do not raise email delivery errors.
59-
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
60-
# config.action_mailer.raise_delivery_errors = false
58+
# Raise delivery errors in production
59+
config.action_mailer.raise_delivery_errors = true
60+
config.action_mailer.delivery_method = :smtp
6161

6262
# Set host to be used by links generated in mailer templates.
63-
config.action_mailer.default_url_options = { host: "example.com" }
64-
65-
# Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit.
66-
# config.action_mailer.smtp_settings = {
67-
# user_name: Rails.application.credentials.dig(:smtp, :user_name),
68-
# password: Rails.application.credentials.dig(:smtp, :password),
69-
# address: "smtp.example.com",
70-
# port: 587,
71-
# authentication: :plain
72-
# }
63+
config.action_mailer.default_url_options = {
64+
host: ENV.fetch("APP_HOST", "example.com"),
65+
protocol: "https"
66+
}
67+
68+
# Default from address for all mailers
69+
config.action_mailer.default_options = {
70+
from: ENV.fetch("MAIL_FROM", "noreply@example.com")
71+
}
72+
73+
# SMTP server configuration
74+
config.action_mailer.smtp_settings = {
75+
address: ENV.fetch("SMTP_ADDRESS", "mail"),
76+
port: ENV.fetch("SMTP_PORT", "587").to_i,
77+
domain: ENV.fetch("SMTP_DOMAIN", "example.com"),
78+
# No authentication needed for internal mail server
79+
authentication: nil,
80+
enable_starttls_auto: false,
81+
openssl_verify_mode: "none"
82+
}
7383

7484
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
7585
# the I18n.default_locale when a translation cannot be found).

deploy/.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,20 @@ GOOGLE_REDIRECT_URI=https://hackorum.example.com/auth/google_oauth2/callback
2828

2929
# Hostname for Caddy (update deploy/Caddyfile too)
3030
APP_HOST=hackorum.example.com
31+
32+
# Mail configuration
33+
MAIL_DOMAIN=example.com
34+
MAIL_HOSTNAME=mail.example.com
35+
MAIL_FROM=noreply@example.com
36+
DKIM_SELECTOR=mail
37+
38+
# Optional: Use a relay host (e.g., SendGrid, Mailgun) instead of direct sending
39+
# This is recommended for better deliverability
40+
# MAIL_RELAYHOST=[smtp.sendgrid.net]:587
41+
# MAIL_RELAYHOST_USERNAME=apikey
42+
# MAIL_RELAYHOST_PASSWORD=your-sendgrid-api-key
43+
44+
# SMTP settings for Rails
45+
SMTP_ADDRESS=mail
46+
SMTP_PORT=587
47+
SMTP_DOMAIN=example.com

deploy/docker-compose.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,54 @@ services:
102102
- /var/run/docker.sock:/var/run/docker.sock
103103
restart: unless-stopped
104104

105+
mail:
106+
image: boky/postfix:latest
107+
hostname: ${MAIL_HOSTNAME:-mail.example.com}
108+
environment:
109+
# Domain from which emails will be sent
110+
ALLOWED_SENDER_DOMAINS: ${MAIL_DOMAIN:-example.com}
111+
112+
# Relay host (leave empty for direct sending, or use a relay like SendGrid/Mailgun)
113+
# RELAYHOST: ${MAIL_RELAYHOST:-}
114+
# RELAYHOST_USERNAME: ${MAIL_RELAYHOST_USERNAME:-}
115+
# RELAYHOST_PASSWORD: ${MAIL_RELAYHOST_PASSWORD:-}
116+
117+
# DKIM signing
118+
DKIM_SELECTOR: ${DKIM_SELECTOR:-mail}
119+
DKIM_AUTOGENERATE: "true"
120+
121+
# Logging
122+
LOG_LEVEL: ${MAIL_LOG_LEVEL:-info}
123+
124+
# Override from address (optional, for testing)
125+
# OVERWRITE_FROM: ${MAIL_OVERWRITE_FROM:-}
126+
127+
# TLS configuration
128+
TLS: "true"
129+
130+
# Allow localhost relay (for the web container)
131+
ALLOW_EMPTY_SENDER_DOMAINS: "false"
132+
133+
# Message size limit (default 10MB)
134+
MESSAGE_SIZE_LIMIT: ${MAIL_MESSAGE_SIZE_LIMIT:-10240000}
135+
volumes:
136+
# Persist DKIM keys
137+
- mail_dkim:/etc/opendkim/keys
138+
# Persist mail queue
139+
- mail_spool:/var/spool/postfix
140+
expose:
141+
- "587" # Submission port (for authenticated clients)
142+
ports:
143+
- "25:25" # SMTP port (for receiving mail)
144+
healthcheck:
145+
test: ["CMD-SHELL", "postfix status || exit 1"]
146+
interval: 30s
147+
timeout: 10s
148+
retries: 3
149+
restart: unless-stopped
150+
labels:
151+
autoheal: "true"
152+
105153
psql:
106154
image: postgres:18
107155
entrypoint: ["psql", "postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-hackorum}"]
@@ -117,3 +165,5 @@ volumes:
117165
pgbackups:
118166
caddy_data:
119167
caddy_config:
168+
mail_dkim:
169+
mail_spool:

deploy/get-dkim-key.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
# Script to extract DKIM public key from the mail container
3+
# Run this after starting the mail service for the first time
4+
5+
set -e
6+
7+
echo "==================================================================="
8+
echo "DKIM Public Key for DNS Configuration"
9+
echo "==================================================================="
10+
echo ""
11+
echo "Retrieving DKIM key from mail container..."
12+
echo ""
13+
14+
# Try to get the DKIM key
15+
KEY=$(docker compose exec -T mail cat /etc/opendkim/keys/mail.txt 2>/dev/null || \
16+
docker compose exec -T mail sh -c 'cat /etc/opendkim/keys/*.txt' 2>/dev/null || \
17+
echo "ERROR: Could not retrieve DKIM key")
18+
19+
if [[ "$KEY" == "ERROR:"* ]]; then
20+
echo "❌ Failed to retrieve DKIM key!"
21+
echo ""
22+
echo "Make sure the mail container is running:"
23+
echo " docker compose ps mail"
24+
echo ""
25+
echo "Check mail container logs:"
26+
echo " docker compose logs mail"
27+
exit 1
28+
fi
29+
30+
echo "Raw key file content:"
31+
echo "-------------------------------------------------------------------"
32+
echo "$KEY"
33+
echo "-------------------------------------------------------------------"
34+
echo ""
35+
36+
# Extract the DNS record value (remove line breaks, quotes, and extra spaces)
37+
DNS_VALUE=$(echo "$KEY" | grep -v "^;" | tr -d '\n' | sed 's/.*TXT[[:space:]]*(//' | sed 's/[[:space:]]*)[[:space:]]*;.*//' | tr -d '"' | sed 's/[[:space:]]\+/ /g' | xargs)
38+
39+
echo "DNS Record to add:"
40+
echo "-------------------------------------------------------------------"
41+
echo "Type: TXT"
42+
echo "Name: mail._domainkey"
43+
echo "Value: $DNS_VALUE"
44+
echo "TTL: 3600"
45+
echo "-------------------------------------------------------------------"
46+
echo ""
47+
echo "✅ Copy the 'Value' line above and add it to your DNS provider"
48+
echo ""
49+
echo "Note: The selector 'mail' must match your DKIM_SELECTOR in .env"
50+
echo " If using a different selector, update the Name field accordingly"
51+
echo ""

0 commit comments

Comments
 (0)