Compare commits
No commits in common. "43aaed31625e06688e449ced10b1ce6d231d5619" and "c3dba29b8fa6dfa92c7b438cb94ed930b3ea2b84" have entirely different histories.
43aaed3162
...
c3dba29b8f
|
@ -1,13 +0,0 @@
|
||||||
class AccountVerificationsController < ApplicationController
|
|
||||||
def show
|
|
||||||
user = User.find_by_token_for(:email_verification, params[:id])
|
|
||||||
unless user
|
|
||||||
flash[:alert] = t(".error")
|
|
||||||
redirect_to :root and return
|
|
||||||
end
|
|
||||||
user.update(verified: true)
|
|
||||||
UserMailer.with(user: user).email_verified.deliver_later
|
|
||||||
flash[:notice] = t(".success")
|
|
||||||
redirect_to :root # TODO: New session path
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,31 +0,0 @@
|
||||||
class UsersController < ApplicationController
|
|
||||||
def new
|
|
||||||
@user = User.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@user = User.new(user_params)
|
|
||||||
if @user.save
|
|
||||||
token = @user.generate_token_for(:email_verification)
|
|
||||||
UserMailer.with(user: @user, token: token).email_verification.deliver_later
|
|
||||||
flash[:notice] = t(".success", name: @user.first_name)
|
|
||||||
redirect_to :root
|
|
||||||
else
|
|
||||||
flash[:alert] = t(".error", error: @user.errors.full_messages.to_sentence)
|
|
||||||
render :new, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def user_params
|
|
||||||
params.require(:user).permit(
|
|
||||||
:username,
|
|
||||||
:password,
|
|
||||||
:password_confirmation,
|
|
||||||
:email,
|
|
||||||
:first_name,
|
|
||||||
:last_name,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +0,0 @@
|
||||||
module MailerHelper
|
|
||||||
def htmlify_email(content)
|
|
||||||
content.gsub!("\n\n", "</p>\n\n<p>")
|
|
||||||
"<p>#{content.chomp}</p>".html_safe
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,16 +0,0 @@
|
||||||
class UserMailer < ApplicationMailer
|
|
||||||
helper(:mailer)
|
|
||||||
|
|
||||||
def email_verification
|
|
||||||
@user = params[:user]
|
|
||||||
@token = params[:token]
|
|
||||||
|
|
||||||
mail(to: @user.email, subject: "[Forg] Verify your email")
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_verified
|
|
||||||
@user = params[:user]
|
|
||||||
|
|
||||||
mail(to: @user.email, subject: "[Forg] Your email has been verified")
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,9 +3,6 @@ class User < ApplicationRecord
|
||||||
generates_token_for :password_reset, expires_in: 4.hours do
|
generates_token_for :password_reset, expires_in: 4.hours do
|
||||||
password_salt.last(10) # Invalidates when password changed
|
password_salt.last(10) # Invalidates when password changed
|
||||||
end
|
end
|
||||||
generates_token_for :email_verification, expires_in: 1.week do
|
|
||||||
verified.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
validates :username,
|
validates :username,
|
||||||
presence: true,
|
presence: true,
|
||||||
|
@ -17,7 +14,7 @@ class User < ApplicationRecord
|
||||||
uniqueness: true,
|
uniqueness: true,
|
||||||
length: { minimum: 5, maximum: 100 },
|
length: { minimum: 5, maximum: 100 },
|
||||||
format: { with: URI::MailTo::EMAIL_REGEXP,
|
format: { with: URI::MailTo::EMAIL_REGEXP,
|
||||||
message: I18n.t("users.validations.email_format"),
|
message: "must be a valid email address",
|
||||||
}
|
}
|
||||||
normalizes :email, with: ->(email) { email.strip.downcase }
|
normalizes :email, with: ->(email) { email.strip.downcase }
|
||||||
validates :first_name,
|
validates :first_name,
|
||||||
|
@ -27,9 +24,6 @@ class User < ApplicationRecord
|
||||||
allow_nil: false,
|
allow_nil: false,
|
||||||
length: { maximum: 50 }
|
length: { maximum: 50 }
|
||||||
|
|
||||||
scope :verified, -> { where(verified: true) }
|
|
||||||
scope :unverified, -> { where(verified: false) }
|
|
||||||
|
|
||||||
def full_name
|
def full_name
|
||||||
return first_name if last_name.blank?
|
return first_name if last_name.blank?
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,16 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title><%= "#{yield(:title)} | " if content_for? :title %><%= t("forg") %></title>
|
<title>Forg</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||||
<%= javascript_importmap_tags %>
|
<%= javascript_importmap_tags %>
|
||||||
<meta name="turbo-refresh-method" content="morph">
|
|
||||||
<meta name="turbo-refresh-scroll" content="preserve">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
|
||||||
<h1><%= link_to t("forg"), root_path %></h1>
|
|
||||||
</header>
|
|
||||||
<%= render partial: "shared/flash_messages" %>
|
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<p><%= t(".greeting", name: @user.first_name) %></p>
|
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<%= t(".sign_off_html") %>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1 @@
|
||||||
<%= t(".greeting") %>
|
|
||||||
|
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
|
|
||||||
<%= t(".sign_off") %>
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<% flash.each do |type, message| %>
|
|
||||||
<aside class="flash <%= type %>">
|
|
||||||
<%= message %>
|
|
||||||
</aside>
|
|
||||||
<% end %>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<%= htmlify_email(t(".content")) %>
|
|
||||||
|
|
||||||
<%= link_to account_verification_url(@token),
|
|
||||||
account_verification_url(@token) %>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<%= t(".content", url: account_verification_url(@token)) %>
|
|
||||||
|
|
||||||
<%= account_verification_url(@token) %>
|
|
|
@ -1 +0,0 @@
|
||||||
<%= htmlify_email(t(".content")) %>
|
|
|
@ -1 +0,0 @@
|
||||||
<%= t(".content") %>
|
|
|
@ -1,23 +0,0 @@
|
||||||
<%# locals: (user:, button_text:) -%>
|
|
||||||
|
|
||||||
<%= form_with model: user do |f| %>
|
|
||||||
<%= f.label :username %>
|
|
||||||
<%= f.text_field :username %>
|
|
||||||
|
|
||||||
<%= f.label :first_name %>
|
|
||||||
<%= f.text_field :first_name %>
|
|
||||||
|
|
||||||
<%= f.label :last_name %>
|
|
||||||
<%= f.text_field :last_name %>
|
|
||||||
|
|
||||||
<%= f.label :email %>
|
|
||||||
<%= f.text_field :email %>
|
|
||||||
|
|
||||||
<%= f.label :password %>
|
|
||||||
<%= f.password_field :password %>
|
|
||||||
|
|
||||||
<%= f.label :password_confirmation %>
|
|
||||||
<%= f.password_field :password_confirmation %>
|
|
||||||
|
|
||||||
<%= f.submit button_text %>
|
|
||||||
<% end %>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<% content_for :title, "Sign up" %>
|
|
||||||
|
|
||||||
<h1><%= t(".sign_up") %></h1>
|
|
||||||
|
|
||||||
<%= render partial: "users/form",
|
|
||||||
locals: { user: @user, button_text: t(".sign_up") } %>
|
|
|
@ -63,7 +63,7 @@ Rails.application.configure do
|
||||||
config.assets.quiet = true
|
config.assets.quiet = true
|
||||||
|
|
||||||
# Raises error for missing translations.
|
# Raises error for missing translations.
|
||||||
config.i18n.raise_on_missing_translations = true
|
# config.i18n.raise_on_missing_translations = true
|
||||||
|
|
||||||
# Annotate rendered view with file names.
|
# Annotate rendered view with file names.
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
|
@ -54,7 +54,7 @@ Rails.application.configure do
|
||||||
config.active_support.disallowed_deprecation_warnings = []
|
config.active_support.disallowed_deprecation_warnings = []
|
||||||
|
|
||||||
# Raises error for missing translations.
|
# Raises error for missing translations.
|
||||||
config.i18n.raise_on_missing_translations = true
|
# config.i18n.raise_on_missing_translations = true
|
||||||
|
|
||||||
# Annotate rendered view with file names.
|
# Annotate rendered view with file names.
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
|
# Files in the config/locales directory are used for internationalization and
|
||||||
|
# are automatically loaded by Rails. If you want to use locales other than
|
||||||
|
# English, add the necessary files in this directory.
|
||||||
|
#
|
||||||
|
# To use the locales, use `I18n.t`:
|
||||||
|
#
|
||||||
|
# I18n.t "hello"
|
||||||
|
#
|
||||||
|
# In views, this is aliased to just `t`:
|
||||||
|
#
|
||||||
|
# <%= t("hello") %>
|
||||||
|
#
|
||||||
|
# To use a different locale, set it with `I18n.locale`:
|
||||||
|
#
|
||||||
|
# I18n.locale = :es
|
||||||
|
#
|
||||||
|
# This would use the information in config/locales/es.yml.
|
||||||
|
#
|
||||||
|
# To learn more about the API, please read the Rails Internationalization guide
|
||||||
|
# at https://guides.rubyonrails.org/i18n.html.
|
||||||
|
#
|
||||||
|
# Be aware that YAML interprets the following case-insensitive strings as
|
||||||
|
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
|
||||||
|
# must be quoted to be interpreted as strings. For example:
|
||||||
|
#
|
||||||
|
# en:
|
||||||
|
# "yes": yup
|
||||||
|
# enabled: "ON"
|
||||||
|
|
||||||
en:
|
en:
|
||||||
forg: Forg
|
hello: "Hello world"
|
||||||
layouts:
|
|
||||||
mailer:
|
|
||||||
greeting: "Hello, %{name}"
|
|
||||||
sign_off: |
|
|
||||||
See you soon,
|
|
||||||
The Forg team
|
|
||||||
sign_off_html: "<p>See you soon,<br>The Forg team</p>"
|
|
||||||
account_verifications:
|
|
||||||
show:
|
|
||||||
success: "Thanks for verifying your email address! You can now log in."
|
|
||||||
error: "Invalid token, could not verify your account."
|
|
||||||
users:
|
|
||||||
validations:
|
|
||||||
email_format: "must be a valid email address"
|
|
||||||
new:
|
|
||||||
sign_up: Sign up
|
|
||||||
create:
|
|
||||||
error: "Could not create account: %{error}"
|
|
||||||
success: "Thanks for joining Forg, %{name}! Please check your email to verify your address."
|
|
||||||
user_mailer:
|
|
||||||
email_verified:
|
|
||||||
content: |-
|
|
||||||
Thanks for verifying your email address, and welcome to Forg!
|
|
||||||
|
|
||||||
You can now go ahead and log in. Enjoy!
|
|
||||||
email_verification:
|
|
||||||
content: |-
|
|
||||||
If you did not sign up for Forg, please ignore this email.
|
|
||||||
|
|
||||||
Otherwise, please visit the link below to verify your email address.
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
default_url_options host: "forg-app.com"
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
root "users#new"
|
|
||||||
resources :users, only: %i[new create]
|
|
||||||
resources :account_verifications, only: %i[show]
|
|
||||||
|
|
||||||
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
|
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||||
get "up" => "rails/health#show", as: :rails_health_check
|
get "up" => "rails/health#show", as: :rails_health_check
|
||||||
|
|
||||||
|
# Defines the root path route ("/")
|
||||||
|
# root "posts#index"
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
class AddVerifiedToUser < ActiveRecord::Migration[7.1]
|
|
||||||
def change
|
|
||||||
add_column :users, :verified, :boolean, null: false, default: false
|
|
||||||
add_index :users, :verified
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_04_14_122652) do
|
ActiveRecord::Schema[7.1].define(version: 2024_04_13_152553) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -22,10 +22,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_14_122652) do
|
||||||
t.string "last_name", limit: 50, default: "", null: false
|
t.string "last_name", limit: 50, default: "", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.boolean "verified", default: false, null: false
|
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["username"], name: "index_users_on_username", unique: true
|
t.index ["username"], name: "index_users_on_username", unique: true
|
||||||
t.index ["verified"], name: "index_users_on_verified"
|
|
||||||
t.check_constraint "length(email::text) >= 5", name: "chk_email_min_length"
|
t.check_constraint "length(email::text) >= 5", name: "chk_email_min_length"
|
||||||
t.check_constraint "length(first_name::text) >= 1", name: "chk_first_name_min_length"
|
t.check_constraint "length(first_name::text) >= 1", name: "chk_first_name_min_length"
|
||||||
t.check_constraint "length(username::text) >= 3", name: "chk_username_min_length"
|
t.check_constraint "length(username::text) >= 3", name: "chk_username_min_length"
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
require "test_helper"
|
|
||||||
|
|
||||||
class AccountVerificationsControllerTest < ActionDispatch::IntegrationTest
|
|
||||||
test "should verify email" do
|
|
||||||
user = users(:unverified)
|
|
||||||
token = user.generate_token_for(:email_verification)
|
|
||||||
assert_emails(+1) do
|
|
||||||
get account_verification_url(token)
|
|
||||||
end
|
|
||||||
user.reload
|
|
||||||
assert user.verified
|
|
||||||
assert_redirected_to root_url
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,40 +0,0 @@
|
||||||
require "test_helper"
|
|
||||||
|
|
||||||
class UsersControllerTest < ActionDispatch::IntegrationTest
|
|
||||||
test "should get new" do
|
|
||||||
get new_user_url
|
|
||||||
assert_response :success
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should create user" do
|
|
||||||
assert_changes("User.count", +1) do
|
|
||||||
post(users_url, params: { user: user_params })
|
|
||||||
end
|
|
||||||
assert_redirected_to :root
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should email user for confirmation" do
|
|
||||||
assert_emails(+1) do
|
|
||||||
post(users_url, params: { user: user_params })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should alert to invalid user" do
|
|
||||||
assert_no_changes("User.count") do
|
|
||||||
post(users_url, params: { user: { username: "new_user" } })
|
|
||||||
end
|
|
||||||
assert_response :unprocessable_entity
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def user_params
|
|
||||||
{
|
|
||||||
username: "new_user",
|
|
||||||
password: "password",
|
|
||||||
email: "new_user@ex.com",
|
|
||||||
first_name: "new",
|
|
||||||
last_name: "user",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,24 +1,15 @@
|
||||||
DEFAULTS: &DEFAULTS
|
|
||||||
password_digest: <%= BCrypt::Password.create('password', cost: 5) %>
|
|
||||||
username: $LABEL
|
|
||||||
verified: true
|
|
||||||
email: $LABEL<%= "@example.com" %>
|
|
||||||
|
|
||||||
trevor:
|
trevor:
|
||||||
<<: *DEFAULTS
|
|
||||||
username: tsv
|
username: tsv
|
||||||
|
password_digest: <%= BCrypt::Password.create('password', cost: 5) %>
|
||||||
|
email: trevor@example.com
|
||||||
first_name: Trevor
|
first_name: Trevor
|
||||||
last_name: Vallender
|
last_name: Vallender
|
||||||
|
|
||||||
unverified:
|
|
||||||
<<: *DEFAULTS
|
|
||||||
first_name: Unverified
|
|
||||||
last_name: User
|
|
||||||
verified: false
|
|
||||||
|
|
||||||
<% 1.upto(10) do |i| %>
|
<% 1.upto(10) do |i| %>
|
||||||
user_<%= i %>:
|
user_<%= i %>:
|
||||||
<<: *DEFAULTS
|
username: <%= "user_#{i}" %>
|
||||||
|
password_digest: <%= BCrypt::Password.create('password', cost: 5) %>
|
||||||
|
email: <%= "user_#{i}@example.com" %>
|
||||||
first_name: <%= "User#{i}" %>
|
first_name: <%= "User#{i}" %>
|
||||||
last_name: <%= "User#{i}" %>
|
last_name: <%= "User#{i}" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
require "test_helper"
|
|
||||||
|
|
||||||
class MailerHelperTest < ActionView::TestCase
|
|
||||||
test "htmlify_email adds correct tags" do
|
|
||||||
input = <<~TEST.chomp
|
|
||||||
One line here.
|
|
||||||
|
|
||||||
Another one here.
|
|
||||||
The same paragraph.
|
|
||||||
|
|
||||||
And another.
|
|
||||||
TEST
|
|
||||||
expected_output = <<~TEST.chomp
|
|
||||||
<p>One line here.</p>
|
|
||||||
|
|
||||||
<p>Another one here.
|
|
||||||
The same paragraph.</p>
|
|
||||||
|
|
||||||
<p>And another.</p>
|
|
||||||
TEST
|
|
||||||
assert_equal expected_output, htmlify_email(input)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
|
|
||||||
class UserMailerPreview < ActionMailer::Preview
|
|
||||||
def email_verification
|
|
||||||
UserMailer.with(user: User.first, token: "token").email_verification
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_verified
|
|
||||||
UserMailer.with(user: User.first).email_verified
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -38,14 +38,6 @@ class UserTest < ActiveSupport::TestCase
|
||||||
assert_must_exist(users(:trevor), "first_name")
|
assert_must_exist(users(:trevor), "first_name")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "email verification token is invalid after email verified" do
|
|
||||||
user = users(:unverified)
|
|
||||||
token = user.generate_token_for(:email_verification)
|
|
||||||
assert_equal user, User.find_by_token_for(:email_verification, token)
|
|
||||||
user.update(verified: true)
|
|
||||||
assert_nil User.find_by_token_for(:email_verification, token)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "password reset token is invalid after password changed" do
|
test "password reset token is invalid after password changed" do
|
||||||
user = users(:trevor)
|
user = users(:trevor)
|
||||||
token = user.generate_token_for(:password_reset)
|
token = user.generate_token_for(:password_reset)
|
||||||
|
|
Loading…
Reference in New Issue