Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
17db68a
add: passwords_controller, user.rb
Mar 24, 2025
174bd98
add: setup mailer functionality
Mar 24, 2025
53c71b0
update: routes.rb to accomodate password_resets
Mar 24, 2025
34c89bd
Updated backend to work with frontend
galav12 Mar 24, 2025
e10708a
update: fixed mailer not sending email issue by adding smtp user and …
Mar 24, 2025
4fc00e2
fix: mailer adjusted to expertiza mailer
Mar 25, 2025
27fa103
add: spec test for passwords_controller and user model
Mar 25, 2025
07438d8
update: README.md with testing information
Mar 25, 2025
7e5eb99
Merge remote-tracking branch 'upstream/main' into E2604-password-reset
johnmweisz Mar 3, 2026
6a6bf06
fix routing and namespace
johnmweisz Mar 3, 2026
830f01c
use modern token method
johnmweisz Mar 3, 2026
00dcb47
add passwordless fallback
johnmweisz Mar 3, 2026
3b7354e
Merge pull request #2 from johnmweisz/E2604-password-reset
JaredM2028 Mar 6, 2026
7df5da1
make baseurl configurable, align email and template
johnmweisz Mar 8, 2026
24cff9c
The unit tests weren't working, so made updates to get them working
josev814 Mar 9, 2026
d1ad85a
added a test for token expiration
josev814 Mar 9, 2026
4eb405d
Merge pull request #17 from johnmweisz/7-update-reset-email-make-conf…
johnmweisz Mar 9, 2026
122a4d3
Merge branch 'development' into jvargas6/functioning_unit_tests
johnmweisz Mar 9, 2026
40d31fc
Merge pull request #18 from johnmweisz/jvargas6/functioning_unit_tests
johnmweisz Mar 9, 2026
5fa7eb8
remove mailer credentials
johnmweisz Mar 9, 2026
6d8e72c
needed to add time_helpers to th rails_helper
josev814 Mar 9, 2026
51206f0
remove mailer credentials, fix subject typo
johnmweisz Mar 10, 2026
8f4af85
wrap mailer in conditional
johnmweisz Mar 10, 2026
527b058
remove misleading config
johnmweisz Mar 10, 2026
383af48
Merge pull request #20 from johnmweisz/jweisz/remove-mailer-credentials
JaredM2028 Mar 10, 2026
43c6d8f
Merge pull request #19 from johnmweisz/jvargas6/travel_to_issue
josev814 Mar 11, 2026
0e088af
Cleaning up user tests and adding mailer tests
josev814 Mar 12, 2026
ca472dd
removing extra code from testing
josev814 Mar 12, 2026
eb28d32
Merge pull request #21 from johnmweisz/jvargas6/fixing_user_password_…
josev814 Mar 12, 2026
e2ce30a
Merge branch 'main' into development
johnmweisz Mar 16, 2026
c61e8fc
Minimally cleaned up dead code.
JaredM2028 Mar 25, 2026
91b1d26
Merge pull request #23 from johnmweisz/dead_code_cleanup
JaredM2028 Mar 26, 2026
db84640
add mailer config
johnmweisz Mar 28, 2026
e0eca05
fix syntax
johnmweisz Mar 28, 2026
9c90171
fix configs
johnmweisz Mar 28, 2026
a7d4010
refactor
johnmweisz Mar 28, 2026
aecc20b
making pr updates to namings of user_mailer methods
josev814 Mar 28, 2026
7f656f9
Merge pull request #30 from johnmweisz/jvargas6/user_mailer_review_ad…
josev814 Mar 28, 2026
af5d4c3
Merge branch 'development' into jweisz/move-config
johnmweisz Mar 28, 2026
b567023
updating user_mailer_spec to prevent accessing private send method
josev814 Mar 28, 2026
39a4a36
Address PR review comments: rename env example, fix deliveries, spaci…
Copilot Mar 29, 2026
1d31888
add comments/refine config
johnmweisz Mar 29, 2026
272f91d
more comments
johnmweisz Mar 29, 2026
b13ca39
cleanup
johnmweisz Mar 29, 2026
679b550
Update sample.env
johnmweisz Mar 29, 2026
9eb1fe8
treating blank as unset
johnmweisz Mar 29, 2026
b955c01
treating blank as unset 2
johnmweisz Mar 29, 2026
25e77d6
Add :admin alias trait in users factory for backward compatibility
Copilot Mar 29, 2026
8c111ae
update spec refs to admin
johnmweisz Mar 29, 2026
f425627
Merge pull request #33 from johnmweisz/jvargas6/user_mailer_spec_revi…
josev814 Mar 29, 2026
27a7e1f
Merge pull request #31 from johnmweisz/jweisz/move-config
johnmweisz Mar 29, 2026
6f25406
Addressed R1 feedback.
JaredM2028 Mar 28, 2026
6be1a99
Normalize email getter and controller lookup.
JaredM2028 Mar 28, 2026
2cf2187
Split lookup logic and render invalid response.
JaredM2028 Mar 28, 2026
fc8d5ad
Normalize all emails.
JaredM2028 Mar 28, 2026
016b69a
Refactored for DRY purposes.
JaredM2028 Mar 28, 2026
c31e71b
Preserve nil if passed in by user.
JaredM2028 Mar 28, 2026
823abb0
Updated tests.
JaredM2028 Mar 28, 2026
dd8d876
Centralize email normalization across User model and email-based look…
JaredM2028 Mar 29, 2026
3c56639
Condensed email normalization.
JaredM2028 Mar 29, 2026
b5cfca5
Test coverage for email normalization.
JaredM2028 Mar 29, 2026
3b3ab31
Explicitly splitting token lookup for users. All tests still pass and…
JaredM2028 Mar 29, 2026
84eb3c5
Tests updated to be more robust for token searching. Tests to come.
JaredM2028 Mar 29, 2026
745c8fb
Added email normalization to user model and reverted lookups. Tests t…
JaredM2028 Mar 29, 2026
fe274e0
Removed empty lines.
JaredM2028 Mar 29, 2026
5f6d5ca
Merge pull request #34 from johnmweisz/refine_pw_reset_TESTS
johnmweisz Mar 29, 2026
fd09ef1
Merge remote-tracking branch 'origin/development' into refine_pw_cont…
johnmweisz Mar 29, 2026
db37764
Merge pull request #32 from johnmweisz/refine_pw_controller
johnmweisz Mar 29, 2026
eb382f5
Making FRONTEND_URL use environment settings as requested
josev814 Mar 29, 2026
1501654
Merge branch 'development' into jvargas6/frontend_url_to_envconfig
josev814 Mar 29, 2026
70240bf
cleaning up the readme.md file a little
josev814 Mar 29, 2026
0900776
Apply suggestions from code review
josev814 Mar 29, 2026
9a25afb
adding to the rspec user_mailer test
josev814 Mar 29, 2026
3639eef
adding additional test for the mailer
josev814 Mar 29, 2026
e8661f9
Apply suggestions from code review
josev814 Mar 29, 2026
ec413e5
Apply suggestions from code review
josev814 Mar 29, 2026
792d9ce
using uti generic to build the frontend url
josev814 Mar 29, 2026
6950bc8
with uri generation we should not include :// in the scheme
josev814 Mar 30, 2026
595c39a
Merge pull request #35 from johnmweisz/jvargas6/frontend_url_to_envco…
josev814 Mar 30, 2026
f064482
readme update was messed up
josev814 Mar 30, 2026
6ad90d7
Merge pull request #36 from johnmweisz/jvargas6/readme_fix
josev814 Mar 30, 2026
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@
coverage/
rsa_keys.yml
pg_data/

# Ignore .env files
# .env files should never be committed to prevent sensitive information from being exposed.
# Each environment can have its own .env file, and .env.* covers all of them.
# .env.* covers .env.development .env.test, etc
# ** ensures that .env files are ignored regardless of their location in the directory structure.
**/.env
**/.env.*
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,71 @@ Things you may want to cover:

* Ruby version - 3.4.5

---

## Environment Configuration

Before running the application, you need to configure environment variables:

1. **Copy the sample environment file:**
```bash
cp sample.env .env
```

2. **Configure Frontend URL Settings:**

Edit the `.env` file and set the following frontend variables based on your environment. These settings will override the defaults defined in `config/environments/(development|test|production).rb`:

- **`FRONTEND_SCHEME`**: Should be `http` for development/test or `https` for production
- **`FRONTEND_DOMAIN`**: The domain where your frontend is hosted
- **Development/Test**: Defaults to `localhost` if not set (configured in `config/environments/development.rb` and `config/environments/test.rb`)
- **Staging**: Requires explicit configuration via `.env`
- **Production**: Defaults to `expertiza.ncsu.com` if not set (configured in `config/environments/production.rb`), can be overridden via `.env`
- **`FRONTEND_PORT`**: Optional port number
- **Development/Test**: Defaults to `3000` if not set (configured in `config/environments/development.rb` and `config/environments/test.rb`)
- Leave blank for standard ports (80 for HTTP, 443 for HTTPS)
- Set to custom port if needed (e.g., `8443` for custom HTTPS)

**Example for local development** (using defaults, no .env needed):
```bash
# No need to set anything - defaults will be used
# FRONTEND_DOMAIN defaults to localhost
# FRONTEND_PORT defaults to 3000
```

**Example for local development** (overriding defaults via .env):
```env
FRONTEND_SCHEME=http
FRONTEND_DOMAIN=localhost
FRONTEND_PORT=3000
```

**Note:** `FRONTEND_DOMAIN` must be explicitly configured via `.env` in staging environments. In production, it defaults to `expertiza.ncsu.com` but can be overridden if needed. The application will fail to start if `FRONTEND_DOMAIN` is not set in staging.

3. **Load environment variables** (optional, only needed if running outside of Docker):
> **Note:**
>
> If you're using Docker Compose, the `.env` file is automatically loaded, so you don't need to source it manually.


When running the Rails server outside of Docker (e.g., `rails s`), you may need to source the `.env` file to load environment variables:

**Linux/macOS:**
```
set -a # automatically export all variables
source .env
set +a # stop automatically exporting
rails s
```

**Windows (PowerShell):**
```powershell
Get-Content .env | ForEach-Object { if ($_ -and !$_.StartsWith("#")) { $key, $value = $_ -split '=', 2; [Environment]::SetEnvironmentVariable($key, $value) } }
rails s
```

---

## Development Environment

### Prerequisites
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/invitations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def inviter_team
end

def invitee_participant
invitee_user = User.find_by(name: params[:username])|| User.find_by(email: params[:username])
invitee_user = User.find_by(name: params[:username]) || User.find_by(email: params[:username])
unless invitee_user
render json: { error: "Participant with username #{params[:username]} not found" }, status: :not_found
return
Expand Down
48 changes: 48 additions & 0 deletions app/controllers/password_resets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class PasswordResetsController < ApplicationController
before_action :find_user_by_email, only: [:create]
before_action :load_user_by_token, only: [:update]
before_action :require_valid_token!, only: [:update]
skip_before_action :authenticate_request!, only: [:create, :update]

# POST /password_resets
def create
if @user
token = @user.generate_token_for(:password_reset)
UserMailer.password_reset_email(@user, token).deliver_later
end

# Always return a 200 OK to prevent email enumeration attacks
render json: { message: I18n.t('password_reset.email_sent') }, status: :ok
end

# PATCH/PUT /password_resets/:token
def update
if @user.update(password_params)
render json: { message: I18n.t('password_reset.updated') }, status: :ok
else
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end

private

def find_user_by_email
Comment thread
johnmweisz marked this conversation as resolved.
@user = User.find_by(email: params[:email])
end

def load_user_by_token
@user = User.find_by_token_for(:password_reset, params[:token])
end

def require_valid_token!
render_invalid_token_response unless @user
end

def render_invalid_token_response
render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity
end

def password_params
params.require(:user).permit(:password, :password_confirmation)
end
end
15 changes: 15 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class UserMailer < ApplicationMailer
default from: "expertizamailer@gmail.com"

def password_reset_email(user, token)
@user = user
@reset_url = password_reset_url(token)
mail(to: @user.email, subject: I18n.t('password_reset.email_subject'))
end

private

def password_reset_url(token)
"#{FRONTEND_URL}/password_edit/check_reset_url?token=#{token}"
end
end
14 changes: 7 additions & 7 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class User < ApplicationRecord
has_secure_password
normalizes :email, with: ->(email) { email.strip.downcase }
after_initialize :set_defaults

# name must be lowercase and unique
Expand Down Expand Up @@ -48,6 +49,12 @@ def self.instantiate(record)
end
end

# Built-in Rails 7.1+ token generator. Token invalidates if password_salt or updated_at changes.
# https://api.rubyonrails.org/classes/ActiveRecord/TokenFor/ClassMethods.html
generates_token_for :password_reset, expires_in: 15.minutes do
Comment thread
johnmweisz marked this conversation as resolved.
password_salt&.last(10) || updated_at.to_s
end

# Welcome email to be sent to the user after they sign up
def welcome_email; end

Expand All @@ -62,13 +69,6 @@ def self.login_user(login)
user
end

# Reset the password for the user
def reset_password
random_password = SecureRandom.alphanumeric(10)
user.password_digest = BCrypt::Password.create(random_password)
user.save
end

# Get instructor_id of the user, if the user is TA,
# return the id of the instructor else return the id of the user for superior roles
def instructor_id
Expand Down
14 changes: 14 additions & 0 deletions app/views/user_mailer/password_reset_email.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html>
<head>
<title>Expertiza password reset</title>
</head>

<body>
<p>Hi <%= @user.full_name %>,</p>
<p>Reset your password, and we'll get you on your way.</p>
<p>To change your password, click or paste the following link into your browser:</p>
<p><a href="<%= @reset_url %>"><%= @reset_url %></a></p>
<p>The link will expire in 15 minutes, so be sure to use it right away.</p>
</body>
</html>

23 changes: 23 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,28 @@ class Application < Rails::Application
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.cache_store = :redis_store, ENV['CACHE_STORE'], { expires_in: 3.days, raise_errors: false }

# ── Action Mailer SMTP configuration ──
# All values are pulled from environment variables so the block is safe
# to load even when mailer env vars are absent (e.g. in dev/test).
# Delivery will simply fail at send-time if credentials are missing,
# rather than blowing up on boot.
config.action_mailer.smtp_settings = {
address: ENV['MAILER_SERVER'].presence || 'localhost',
port: (ENV['MAILER_SERVER_PORT'].presence || '587').to_i,
domain: ENV['MAILER_DOMAIN'].presence || 'localhost',

# Only include credentials when they're actually provided.
user_name: ENV['MAILER_USER'].presence,
password: ENV['MAILER_PASSWORD'].presence,

# :plain sends base64-encoded credentials — fine over TLS,
# but skip authentication entirely when no user is configured.
authentication: ENV['MAILER_USER'].present? ? :plain : nil,

enable_starttls_auto: ActiveModel::Type::Boolean.new.cast(
ENV['MAILER_ENABLE_STARTTLS'].presence || 'true'
)
}
end
end
12 changes: 12 additions & 0 deletions config/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@

# Initialize the Rails application.
Rails.application.initialize!

# ── Frontend URL Configuration ──
raise "FRONTEND_DOMAIN must be configured via environment variables or config/environments/*.rb files" if Rails.configuration.x.frontend_domain.blank?

# This runs after all environment files are loaded, so environment-specific defaults are available
# URI::Generic.build expects nil for no port, but ENV vars are strings, so convert to int and back to handle blank/zero cases
uri = URI::Generic.build(
scheme: Rails.configuration.x.frontend_scheme,
host: Rails.configuration.x.frontend_domain,
port: Rails.configuration.x.frontend_port.to_i.nonzero?
)
FRONTEND_URL = uri.to_s
14 changes: 13 additions & 1 deletion config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@

# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false

config.action_mailer.perform_caching = false
config.action_mailer.delivery_method = :smtp

# Prevent actual emails from being sent in development.
# Emails will still be processed and logged, but never leave the server.
# Set to true (and configure SMTP env vars) if you need to test real delivery locally.
config.action_mailer.perform_deliveries = false

# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
Expand All @@ -65,4 +70,11 @@
# config.action_cable.disable_request_forgery_protection = true
config.hosts << 'localhost'
config.hosts << "www.example.com"

# ── Frontend URL Configuration ──
# Set defaults for local development (can be overridden via environment variables)
# Rails convention recommends config.x.* for custom settings
config.x.frontend_scheme = ENV.fetch('FRONTEND_SCHEME', 'http')
config.x.frontend_domain = ENV.fetch('FRONTEND_DOMAIN', 'localhost')
config.x.frontend_port = ENV.fetch('FRONTEND_PORT', '3000')
end
9 changes: 9 additions & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
# config.active_job.queue_name_prefix = "reimplementation_production"

config.action_mailer.perform_caching = false
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true

# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
Expand Down Expand Up @@ -85,4 +87,11 @@

# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false

# ── Frontend URL Configuration ──
# Set defaults for production (can be overridden via environment variables)
# Rails convention recommends config.x.* for custom settings
config.x.frontend_scheme = ENV.fetch('FRONTEND_SCHEME', 'https')
config.x.frontend_domain = ENV.fetch('FRONTEND_DOMAIN', 'expertiza.ncsu.com')
config.x.frontend_port = ENV.fetch('FRONTEND_PORT', nil)
end
9 changes: 9 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.action_mailer.perform_deliveries = true

# This line let us to use expect(...).to have_enqueued_job.on_queue('mailers')
config.active_job.queue_adapter = :test
# Print deprecation notices to the stderr.
Expand All @@ -62,4 +64,11 @@
# config.action_view.annotate_rendered_view_with_filenames = true
config.hosts << 'localhost'
config.hosts << "www.example.com"

# ── Frontend URL Configuration ──
# Set defaults for tests (can be overridden via environment variables)
# Rails convention recommends config.x.* for custom settings
config.x.frontend_scheme = ENV.fetch('FRONTEND_SCHEME', 'http')
config.x.frontend_domain = ENV.fetch('FRONTEND_DOMAIN', 'localhost')
config.x.frontend_port = ENV.fetch('FRONTEND_PORT', '3000')
end
10 changes: 9 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@
# available at https://guides.rubyonrails.org/i18n.html.

en:
hello: "Hello world"
user:
errors:
password_short: "Password is too short (minimum is 6 characters)"
password_reset:
email_subject: "Expertiza password reset"
email_sent: "If the email exists, a reset link has been sent."
updated: "Password successfully updated."
errors:
token_expired: "The token has expired or is invalid."
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,5 @@
resources :assignments do
resources :duties, controller: 'assignments_duties', only: [:index, :create, :destroy]
end
resources :password_resets, only: [:create, :update], param: :token
end
36 changes: 36 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ── Frontend Configuration ──
# Scheme should be either 'http' or 'https'
# Defaults: 'http' in development/test, 'https' in production
FRONTEND_SCHEME=
# The domain of the front-end
# Defaults: 'localhost' in development/test, 'expertiza.ncsu.com' in production
# Should be explicitly set in non-development environments (e.g. staging, production)
# Examples: 'localhost' (dev/test), 'staging.expertiza.ncsu.edu' (staging), 'expertiza.ncsu.com' (production)
FRONTEND_DOMAIN=
# Optional: Port number for the frontend service
# Defaults: 3000 in development/test, omitted (standard port) in production
# Omit this (or leave blank) to use standard ports (80 for http, 443 for https)
# Examples: '3000' (dev/test), '8443' (custom HTTPS port)
FRONTEND_PORT=

# ── Mailer Configuration ──
# All values are optional in development; see config/application.rb for defaults.
# In production, set at minimum MAILER_SERVER, MAILER_USER, and MAILER_PASSWORD.

# MAILER_USER: the email address used to authenticate with the SMTP server (e.g. no-reply@example.com)
# When blank, authentication is skipped entirely.
MAILER_USER=
# MAILER_PASSWORD: the password or app-specific password for the MAILER_USER account
MAILER_PASSWORD=
# MAILER_SERVER: the hostname of your outgoing SMTP server (e.g. smtp.gmail.com)
# Defaults to "localhost" if unset.
MAILER_SERVER=
# MAILER_SERVER_PORT: the port used by the SMTP server (commonly 587 for STARTTLS, 25 for unencrypted)
# Defaults to 587 if unset.
MAILER_SERVER_PORT=
# MAILER_DOMAIN: the HELO/EHLO domain sent to the SMTP server (e.g. example.com)
# Defaults to "localhost" if unset.
MAILER_DOMAIN=
# MAILER_ENABLE_STARTTLS: set to "false" to disable STARTTLS (e.g. for local relays that don't support TLS)
# Defaults to "true" if unset.
MAILER_ENABLE_STARTTLS=
Loading
Loading