+ StaffPlan does not use passwords. You will receive an email with a link to sign in.
+
+
+
+
+
diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb
new file mode 100644
index 00000000..a446e9a2
--- /dev/null
+++ b/app/views/shared/_flash.html.erb
@@ -0,0 +1,30 @@
+<% if flash[:error].present? %>
+
+ Holy smokes!
+ <%= flash[:error] %>
+
+
+
+<% end %>
+
+<% if flash[:notice] %>
+
+
+
<%= flash[:notice] %>
+
+<% end %>
+
+<% if flash[:info].present? %>
+
+<% end %>
+
+<% if flash[:success].present? %>
+
+
+
+
+
<%= flash[:success] %>
+
+
+
+<% end %>
\ No newline at end of file
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 2e7fb486..dc9114de 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,6 +1,15 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.alert = true
+ Bullet.bullet_logger = true
+ Bullet.console = true
+ Bullet.rails_logger = true
+ Bullet.add_footer = true
+ end
+
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
@@ -38,7 +47,8 @@
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
-
+ config.action_mailer.delivery_method = :letter_opener
+ config.action_mailer.perform_deliveries = true
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
@@ -73,4 +83,7 @@
# Raise error when a before_action's only/except options reference missing actions
config.action_controller.raise_on_missing_callback_actions = true
+
+ Rails.application.routes.default_url_options[:host] = 'localhost'
+ Rails.application.routes.default_url_options[:port] = 3000
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index d2cf6202..7a9df46a 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -94,4 +94,7 @@
# ]
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
+
+ Rails.application.routes.default_url_options[:host] = 'staffplan.com'
+ Rails.application.routes.default_url_options[:protocol] = 'https'
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 0dda9f9f..b30ccdb2 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -6,6 +6,12 @@
# and recreated between test runs. Don't rely on the data there!
Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.bullet_logger = true
+ Bullet.raise = true # raise an error if n+1 query occurs
+ end
+
# Settings specified here will take precedence over those in config/application.rb.
# While tests run files are not watched, reloading is not necessary.
@@ -61,4 +67,7 @@
# Raise error when a before_action's only/except options reference missing actions
config.action_controller.raise_on_missing_callback_actions = true
+
+ Rails.application.routes.default_url_options[:host] = 'localhost'
+ Rails.application.routes.default_url_options[:port] = 3000
end
diff --git a/config/initializers/passwordless.rb b/config/initializers/passwordless.rb
new file mode 100644
index 00000000..43abed78
--- /dev/null
+++ b/config/initializers/passwordless.rb
@@ -0,0 +1,15 @@
+Passwordless.configure do |config|
+ config.default_from_address = "noreply@staffplan.com"
+ config.parent_mailer = "ActionMailer::Base"
+ config.restrict_token_reuse = true # A token/link can only be used once
+ config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.
+
+ config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
+ config.timeout_at = lambda { 10.minutes.from_now } # How long until a token/magic link times out.
+
+ config.redirect_back_after_sign_in = true # When enabled the user will be redirected to their previous page, or a page specified by the `destination_path` query parameter, if available.
+ config.redirect_to_response_options = {} # Additional options for redirects.
+ config.success_redirect_path = '/' # After a user successfully signs in
+ config.failure_redirect_path = '/' # After a sign in fails
+ config.sign_out_redirect_path = '/' # After a user signs out
+end
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index a125ef08..5bd9e12b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,10 +1,11 @@
Rails.application.routes.draw do
- # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
-
# 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
- # Defines the root path route ("/")
- # root "posts#index"
+ passwordless_for :users, at: '/', as: :auth
+
+ resource :dashboard, only: [:show], controller: "dashboard"
+
+ root "dashboard#show"
end
diff --git a/db/migrate/20231119204318_create_passwordless_sessions.passwordless_engine.rb b/db/migrate/20231119204318_create_passwordless_sessions.passwordless_engine.rb
new file mode 100644
index 00000000..cb1de6d5
--- /dev/null
+++ b/db/migrate/20231119204318_create_passwordless_sessions.passwordless_engine.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# This migration comes from passwordless_engine (originally 20171104221735)
+class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
+ def change
+ create_table(:passwordless_sessions) do |t|
+ t.belongs_to(
+ :authenticatable,
+ polymorphic: true,
+ index: {name: "authenticatable"}
+ )
+
+ t.datetime(:timeout_at, null: false)
+ t.datetime(:expires_at, null: false)
+ t.datetime(:claimed_at)
+ t.string(:token_digest, null: false)
+ t.string(:identifier, null: false, index: {unique: true}, length: 36)
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20231119204431_create_users.rb b/db/migrate/20231119204431_create_users.rb
new file mode 100644
index 00000000..531cf23e
--- /dev/null
+++ b/db/migrate/20231119204431_create_users.rb
@@ -0,0 +1,11 @@
+class CreateUsers < ActiveRecord::Migration[7.1]
+ def change
+ create_table :users do |t|
+ t.string :name
+ t.string :email
+ t.timestamps
+
+ t.index :email, unique: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f9139984..9b0948bb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,8 +10,30 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 0) do
+ActiveRecord::Schema[7.1].define(version: 2023_11_19_204431) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ create_table "passwordless_sessions", force: :cascade do |t|
+ t.string "authenticatable_type"
+ t.bigint "authenticatable_id"
+ t.datetime "timeout_at", precision: nil, null: false
+ t.datetime "expires_at", precision: nil, null: false
+ t.datetime "claimed_at", precision: nil
+ t.string "token_digest", null: false
+ t.string "identifier", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.index ["authenticatable_type", "authenticatable_id"], name: "authenticatable"
+ t.index ["identifier"], name: "index_passwordless_sessions_on_identifier", unique: true
+ end
+
+ create_table "users", force: :cascade do |t|
+ t.string "name"
+ t.string "email"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["email"], name: "index_users_on_email", unique: true
+ end
+
end
diff --git a/spec/examples.txt b/spec/examples.txt
new file mode 100644
index 00000000..4658731b
--- /dev/null
+++ b/spec/examples.txt
@@ -0,0 +1,10 @@
+example_id | status | run_time |
+-------------------------------------------- | ------ | --------------- |
+./spec/system/authentications_spec.rb[1:1:1] | passed | 0.27175 seconds |
+./spec/system/authentications_spec.rb[1:2:1] | passed | 0.01112 seconds |
+./spec/system/authentications_spec.rb[1:3:1] | passed | 0.02471 seconds |
+./spec/system/authentications_spec.rb[1:3:2] | passed | 0.02664 seconds |
+./spec/system/authentications_spec.rb[1:3:3] | passed | 0.34036 seconds |
+./spec/system/authentications_spec.rb[1:3:4] | passed | 0.29556 seconds |
+./spec/system/authentications_spec.rb[1:4:1] | passed | 0.37869 seconds |
+./spec/system/authentications_spec.rb[1:5:1] | passed | 0.29928 seconds |
diff --git a/spec/factories.rb b/spec/factories.rb
new file mode 100644
index 00000000..3af0c59b
--- /dev/null
+++ b/spec/factories.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :user do
+ name { Faker::Name.name }
+ email { Faker::Internet.email }
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index b1ebbd84..9d57b225 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -5,7 +5,7 @@
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
-# Add additional requires below this line. Rails is not loaded until this point!
+require "passwordless/test_helpers"
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
diff --git a/spec/system/authentications_spec.rb b/spec/system/authentications_spec.rb
new file mode 100644
index 00000000..79734e4b
--- /dev/null
+++ b/spec/system/authentications_spec.rb
@@ -0,0 +1,107 @@
+require 'rails_helper'
+
+RSpec.describe "Authentication", type: :system do
+ before do
+ driven_by(:rack_test)
+ end
+
+ context "when user is not signed in" do
+ it "redirects to sign in page" do
+ visit root_path
+ expect(page).to have_current_path(auth_sign_in_path)
+ end
+ end
+
+ context "when an email does not exist" do
+ it "shows the user an error that their email does not exist" do
+ visit root_path
+ fill_in "passwordless[email]", with: Faker::Internet.email
+ click_button "Sign in"
+ expect(page).to have_content("We couldn't find a user with that email address")
+ end
+ end
+
+ context "when an email exists" do
+ it "sends the user an email with a sign in link" do
+ user = create(:user)
+ visit root_path
+ fill_in "passwordless[email]", with: user.email
+
+
+ expect {
+ click_button "Sign in"
+ }.to change { ActionMailer::Base.deliveries.count }.by(1)
+
+ expect(page).to have_content("We've sent you an email with a secret token")
+ end
+
+ it "redirects the user to the token entry page" do
+ user = create(:user)
+ visit root_path
+ fill_in "passwordless[email]", with: user.email
+ click_button "Sign in"
+ expect(page).to have_current_path(
+ verify_auth_sign_in_path(user.passwordless_sessions.last.identifier)
+ )
+ end
+
+ it "signs them in after successfully entering the token" do
+ token = "123456"
+ expect(Passwordless.config.token_generator).to receive(:call).and_return(token)
+ user = create(:user)
+ visit root_path
+ fill_in "passwordless[email]", with: user.email
+ click_button "Sign in"
+ fill_in "passwordless[token]", with: token
+ click_button "Confirm"
+ expect(page).to have_current_path(root_path)
+ end
+
+ it "signs them in after clicking the link in the email" do
+ token = "123456"
+ expect(Passwordless.config.token_generator).to receive(:call).and_return(token)
+ user = create(:user)
+ visit root_path
+ fill_in "passwordless[email]", with: user.email
+ click_button "Sign in"
+
+ # click the link in the email
+ visit confirm_auth_sign_in_path(user.passwordless_sessions.last.identifier, token)
+ expect(page).to have_current_path(root_path)
+ end
+ end
+
+ context "when the user enters an invalid token" do
+ it "shows the user an error that their token is invalid" do
+ user = create(:user)
+ visit root_path
+ fill_in "passwordless[email]", with: user.email
+ click_button "Sign in"
+ fill_in "passwordless[token]", with: "bad token"
+ click_button "Confirm"
+ expect(page).to have_content("Token is invalid")
+ end
+ end
+
+ context "when a user signs out" do
+ it "redirects the user to the sign in page" do
+ token = "123456"
+ expect(Passwordless.config.token_generator).to receive(:call).and_return(token)
+
+ user = create(:user)
+ visit root_path
+ fill_in "passwordless[email]", with: user.email
+ click_button "Sign in"
+ fill_in "passwordless[token]", with: token
+ click_button "Confirm"
+ expect(page).to have_current_path(root_path)
+
+ # sign yourself out
+ visit auth_sign_out_path
+
+ # now visit the dashboard, see the login page
+ visit dashboard_path
+ expect(page).to have_content("Please sign in")
+ end
+ end
+end
diff --git a/test/controllers/dashboard_controller_test.rb b/test/controllers/dashboard_controller_test.rb
new file mode 100644
index 00000000..dc4c0a6a
--- /dev/null
+++ b/test/controllers/dashboard_controller_test.rb
@@ -0,0 +1,8 @@
+require "test_helper"
+
+class DashboardControllerTest < ActionDispatch::IntegrationTest
+ test "should get show" do
+ get dashboard_show_url
+ assert_response :success
+ end
+end
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
new file mode 100644
index 00000000..6c868efa
--- /dev/null
+++ b/test/fixtures/users.yml
@@ -0,0 +1,7 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ email: MyString
+
+two:
+ email: MyString
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
new file mode 100644
index 00000000..5c07f490
--- /dev/null
+++ b/test/models/user_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class UserTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end