Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .template-infra/app-app-rails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ _src_path: https://github.com/navapbc/template-infra
app_has_dev_env_setup: true
app_local_port: 3100
app_name: app-rails
template: app
template: app
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm confused why this shows up as a diff, looks the same

1 change: 1 addition & 0 deletions app-rails/Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
source "https://rubygems.org"

ruby "3.4.2"
gem "cgi", ">= 0.4.2"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.2.0", ">= 7.2.2.1"
Expand Down
2 changes: 2 additions & 0 deletions app-rails/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
cgi (0.4.2)
childprocess (5.1.0)
logger (~> 1.5)
choice (0.2.0)
Expand Down Expand Up @@ -458,6 +459,7 @@ DEPENDENCIES
aws-sdk-s3
bootsnap
capybara
cgi (>= 0.4.2)
cssbundling-rails (~> 1.4)
debug
devise
Expand Down
23 changes: 23 additions & 0 deletions app-rails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,29 @@ To run natively:
1. `make start-native`
1. Then visit http://localhost:3100

### Enabling Local Login for Development

For local development (without AWS Cognito credentials), you can use local login via Devise. To enable local login:

1. **Set Rails environment to development**:

```bash
RAILS_ENV=development
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already set by default?


2. **Enable Devise in development by setting the environment variable:**:
```bash
USE_DEVISE=true
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? When would we ever not want to use devise?


3. **Set up the database (run migrations):**:
```bash
rails db:migrate

4. **Follow the instructions for starting a container then start it:**:
```bash
make start-container
Comment on lines +112 to +118
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two instructions seem redundant with instructions for just running the app locally


5. **Visit the sign-up page: Go to http://localhost:3100/users/sign_up and register with any email and password.:**:

#### IDE tips

<details>
Expand Down
2 changes: 1 addition & 1 deletion app-rails/app/controllers/users/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def update_email

private
def auth_service
AuthService.new
AuthServiceFactory.instance.auth_service
end

def user_email_params
Expand Down
2 changes: 1 addition & 1 deletion app-rails/app/controllers/users/mfa_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ def destroy
private

def auth_service
AuthService.new
AuthServiceFactory.instance.auth_service
end
end
2 changes: 1 addition & 1 deletion app-rails/app/controllers/users/passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def confirm_reset
private

def auth_service
AuthService.new
AuthServiceFactory.instance.auth_service
end

def reset_password_params
Expand Down
82 changes: 56 additions & 26 deletions app-rails/app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,68 @@ class Users::SessionsController < Devise::SessionsController
layout "users"
skip_after_action :verify_authorized

# Disable CSRF for API requests
protect_from_forgery with: :null_session, if: -> { request.format.json? }
respond_to :html, :json

def new
@form = Users::NewSessionForm.new
end

def create
@form = Users::NewSessionForm.new(new_session_params)

if @form.invalid?
flash.now[:errors] = @form.errors.full_messages
return render :new, status: :unprocessable_entity
if Rails.application.config.auth_provider == :devise
permitted_params = params.require(:users_new_session_form).permit(:email, :password)
@form = Users::NewSessionForm.new(permitted_params)

if @form.invalid?
flash[:alert] = "Invalid request. Please try again."
return render :new, status: :unprocessable_entity
end

user_params = { "email" => @form.email, "password" => @form.password }
user = User.find_by(email: user_params["email"])

if user.nil? || !user.valid_password?(user_params["password"])
Rails.logger.debug "Login failed: invalid credentials for #{user_params['email']}"
flash[:alert] = "Invalid email or password"
return render :new, status: :unauthorized
end

# Authenticate user via Warden
warden = request.env["warden"]
warden.set_user(user, scope: :user)

redirect_to root_path, notice: "Signed in successfully!"
else

@form = Users::NewSessionForm.new(new_session_params)

if @form.invalid?
flash.now[:errors] = @form.errors.full_messages
return render :new, status: :unprocessable_entity
end

begin
response = auth_service.initiate_auth(
@form.email,
@form.password
)
rescue Auth::Errors::UserNotConfirmed => e
return redirect_to users_verify_account_path
rescue Auth::Errors::BaseAuthError => e
flash.now[:errors] = [ e.message ]
return render :new, status: :unprocessable_entity
end

unless response[:user].present?
puts response.inspect
session[:challenge_session] = response[:session]
session[:challenge_email] = @form.email
return redirect_to session_challenge_path
end

auth_user(response[:user], response[:access_token])
end

begin
response = auth_service.initiate_auth(
@form.email,
@form.password
)
rescue Auth::Errors::UserNotConfirmed => e
return redirect_to users_verify_account_path
rescue Auth::Errors::BaseAuthError => e
flash.now[:errors] = [ e.message ]
return render :new, status: :unprocessable_entity
end

unless response[:user].present?
puts response.inspect
session[:challenge_session] = response[:session]
session[:challenge_email] = @form.email
return redirect_to session_challenge_path
end

auth_user(response[:user], response[:access_token])
end

# Show MFA
Expand Down
6 changes: 5 additions & 1 deletion app-rails/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
class User < ApplicationRecord
devise :cognito_authenticatable, :timeoutable
if Rails.env.development? && !ENV["USE_DEVISE"].blank?
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
else
devise :cognito_authenticatable, :timeoutable
end
attr_accessor :access_token

# == Enums ========================================================
Expand Down
35 changes: 25 additions & 10 deletions app-rails/app/services/auth_service.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# frozen_string_literal: true

class AuthService
def initialize(auth_adapter = Auth::CognitoAdapter.new)
def initialize(auth_adapter = AuthServiceFactory.instance.auth_service)
@auth_adapter = auth_adapter
end

def create_account(email, password)
@auth_adapter.create_account(email, password)
end

# Send a confirmation code that's required to change the user's password
def forgot_password(email)
@auth_adapter.forgot_password(email)
Expand Down Expand Up @@ -34,12 +38,12 @@ def respond_to_auth_challenge(code, challenge = {})
handle_auth_result(response, challenge[:email])
end

def register(email, password)
def register(email, password, role = "applicant")
# @TODO: Handle errors from the auth service, like when the email is already taken
# See https://github.com/navapbc/template-application-rails/issues/15
account = @auth_adapter.create_account(email, password)

create_db_user(account[:uid], email, account[:provider])
create_db_user(account[:uid], email, account[:provider], password, role)
end

# Verify the code sent to the user as part of their initial sign up process.
Expand Down Expand Up @@ -72,14 +76,25 @@ def disable_software_token(user)

private

def create_db_user(uid, email, provider)
Rails.logger.info "Creating User uid: #{uid}"
def create_db_user(uid, email, provider, password = nil, role = "applicant")
Rails.logger.info "Creating User uid: #{uid}, and UserRole: #{role}"

user = nil
if Rails.env.development? && ENV["USE_DEVISE"].present?
user = User.create!(
uid: uid,
email: email,
password: password,
provider: provider,
)
else
user = User.create!(
uid: uid,
email: email,
provider: provider,
)
end

user = User.create!(
uid: uid,
email: email,
provider: provider,
)
user
end

Expand Down
16 changes: 16 additions & 0 deletions app-rails/app/services/auth_service_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require "singleton"

class AuthServiceFactory
include Singleton

def initialize
@auth_service =
if ENV["AUTH_ADAPTER"] == "mock"
AuthService.new(Auth::MockAdapter.new)
else
AuthService.new(Auth::CognitoAdapter.new)
end
end

attr_reader :auth_service
end
2 changes: 1 addition & 1 deletion app-rails/app/views/users/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<%= t(".title") %>
</h1>

<%= us_form_with model: @form, url: new_user_session_path, local: true do |f| %>
<%= us_form_with model: @form, url: new_user_session_path, data: { turbo: false }, local: true do |f| %>
<%= f.honeypot_field %>
<%= f.email_field :email, { autocomplete: "username" } %>

Expand Down
3 changes: 3 additions & 0 deletions app-rails/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

module TemplateApplicationRails
class Application < Rails::Application
config.eager_load = true if Rails.env.development?
# Internationalization
I18n.available_locales = [ :"en", :"es-US" ]
I18n.default_locale = :"en"
Expand Down Expand Up @@ -49,5 +50,7 @@ class Application < Rails::Application

# Show a 403 Forbidden error page when Pundit raises a NotAuthorizedError
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden

config.auth_provider = ENV["USE_DEVISE"] == "true" ? :devise : :cognito
end
end
2 changes: 2 additions & 0 deletions app-rails/config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
# Enable server timing.
config.server_timing = true

config.auth_provider = :devise

# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
Expand Down
4 changes: 4 additions & 0 deletions app-rails/config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
# Custom setting: set the default url.
Rails.application.default_url_options[:host] = "localhost"

Rails.application.configure do
config.auth_provider = :cognito
end

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.

Expand Down
25 changes: 22 additions & 3 deletions app-rails/config/initializers/devise.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Aws.config.update(
region: "us-east-1", # Change this if needed
credentials: Aws::Credentials.new("fake_access_key", "fake_secret_key")
)


# frozen_string_literal: true

# Use this hook to configure devise mailer, warden hooks and so forth.
Expand All @@ -11,6 +17,19 @@
# time the user will be asked for credentials again. Default is 30 minutes.
config.timeout_in = 15.minutes

config.warden do |manager|
manager.failure_app = lambda do |env|
Devise::FailureApp.call(env)
end
end

Rails.application.config.to_prepare do
if Rails.env.development?
User.connection
Devise.mappings[:user]
end
end

# ==> Navigation configuration
# Lists the formats that should be treated as navigational. Formats like
# :html should redirect to the sign in page when the user does not have
Expand All @@ -20,7 +39,7 @@
# should add them to the navigational formats lists.
#
# The "*/*" below is required to match Internet Explorer requests.
# config.navigational_formats = ['*/*', :html, :turbo_stream]
config.navigational_formats = [ "*/*", :html, :turbo_stream ]

# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete
Expand Down Expand Up @@ -125,10 +144,10 @@
# enable this with :database unless you are using a custom strategy.
# The supported strategies are:
# :database = Support basic authentication with authentication key + password
# config.http_authenticatable = false
config.http_authenticatable = true

# If 401 status code should be returned for AJAX requests. True by default.
# config.http_authenticatable_on_xhr = true
config.http_authenticatable_on_xhr = true

# The realm used in Http Basic Authentication. 'Application' by default.
# config.http_authentication_realm = 'Application'
Expand Down
5 changes: 4 additions & 1 deletion app-rails/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
root "home#index"

# Session management
devise_for :users, controllers: { sessions: "users/sessions" }
devise_for :users, controllers: {
sessions: "users/sessions",
registrations: "registrations"
}
devise_scope :user do
get "sessions/challenge" => "users/sessions#challenge", as: :session_challenge
post "sessions/challenge" => "users/sessions#respond_to_challenge", as: :respond_to_session_challenge
Expand Down
Loading
Loading