Skip to content
Merged
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 Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ gem 'redis', '>= 4.0.1'
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
gem 'bcrypt', '~> 3.1.7'

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[windows jruby]
Expand Down
56 changes: 29 additions & 27 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,16 @@ GEM
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
base64 (0.3.0)
bcrypt (3.1.22)
benchmark (0.5.0)
bigdecimal (4.1.2)
bindex (0.8.1)
bootsnap (1.24.2)
bootsnap (1.24.6)
msgpack (~> 1.2)
builder (3.3.0)
concurrent-ruby (1.3.6)
concurrent-ruby (1.3.7)
connection_pool (3.0.2)
crass (1.0.6)
crass (1.0.7)
cssbundling-rails (1.4.3)
railties (>= 6.0.0)
csv (3.3.5)
Expand All @@ -95,14 +96,14 @@ GEM
ffi (1.17.4-arm64-darwin)
ffi (1.17.4-x86_64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
globalid (1.3.0)
globalid (1.4.0)
activesupport (>= 6.1)
honeybadger (5.30.0)
logger
ostruct
i18n (1.14.8)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
icalendar (2.12.2)
icalendar (2.12.3)
base64
ice_cube (~> 0.16)
logger
Expand All @@ -117,7 +118,7 @@ GEM
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.14.1)
jbuilder (2.15.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jsbundling-rails (1.3.1)
Expand All @@ -126,20 +127,20 @@ GEM
loofah (2.25.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
mail (2.9.1)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
marcel (1.2.1)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
msgpack (1.8.3)
net-imap (0.6.4.1)
date
net-protocol
Expand All @@ -150,23 +151,20 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.3-arm64-darwin)
nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.3-x86_64-darwin)
nokogiri (1.19.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.3-x86_64-linux-gnu)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
ostruct (0.6.3)
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-darwin)
pg (1.6.3-x86_64-linux)
pp (0.6.3)
pp (0.6.4)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
psych (5.3.1)
date
stringio
puma (8.0.2)
nio4r (~> 2.0)
racc (1.8.1)
Expand Down Expand Up @@ -209,13 +207,18 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rake (13.4.2)
rdoc (7.2.0)
rbs (4.0.3)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
psych (>= 4.0.0)
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.28.0)
redis-client (0.30.0)
connection_pool
reline (0.6.3)
io-console (~> 0.5)
Expand All @@ -233,7 +236,6 @@ GEM
sprockets (>= 3.0.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
thor (1.5.0)
timeout (0.6.1)
tsort (0.2.0)
Expand All @@ -244,16 +246,15 @@ GEM
concurrent-ruby (~> 1.0)
uri (1.1.1)
useragent (0.16.11)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
web-console (4.3.0)
actionview (>= 8.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
websocket-driver (0.8.0)
railties (>= 8.0.0)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.7.5)
zeitwerk (2.8.2)

PLATFORMS
arm64-darwin-23
Expand All @@ -262,6 +263,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
bcrypt (~> 3.1.7)
bootsnap
cssbundling-rails
csv
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ The database and storage configurations are provided in a `.sample` file, make s

This should get the application setup and ready to run.

### Admin authentication
Admin access (creating/editing events) is gated behind a login screen backed by a `User` model with `has_secure_password`. Admin users are seeded from the encrypted credentials under the `admins:` key — add or update entries there and re-run `bin/rails db:seed` to upsert. The credentials file also holds the `active_record_encryption` keys used to encrypt `password_digest` and `session_token` at rest. To rotate or add admins:

```
bin/rails credentials:edit
# then:
bin/rails db:seed
```

The default seeded admin is `trbadmin`.

To run the application locally, in two separate consoles:
1. Run `bin/dev` - watches and rebuilds JS and CSS (runs `Procfile.dev` via foreman)
1. Run `bin/rails s` - starts the Rails server
Expand Down
5 changes: 1 addition & 4 deletions app/controllers/admin/base_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
module Admin
class BaseController < ApplicationController
http_basic_authenticate_with(
name: Rails.env.production? ? Rails.application.credentials.auth.user : 'admin',
password: Rails.env.production? ? Rails.application.credentials.auth.password : 'admin'
)
before_action :authenticate_admin!
end
end
23 changes: 23 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
class ApplicationController < ActionController::Base
helper_method :current_user, :signed_in?

private

def current_user
return @current_user if defined?(@current_user)

token = session[:session_token]
@current_user = token && User.where(session_token: token).sole
rescue ActiveRecord::RecordNotFound
@current_user = nil
end

def signed_in?
current_user.present?
end

def authenticate_admin!
return if signed_in?

session[:return_to] = request.fullpath if request.get?
redirect_to login_path, alert: 'Please sign in to continue.'
end
end
26 changes: 26 additions & 0 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class SessionsController < ApplicationController
rate_limit to: 10, within: 3.minutes, only: :create,
with: -> { redirect_to login_path, alert: 'Too many attempts. Try again in a few minutes.' }

def new; end

def create
user = User.authenticate_by(username: params[:username].to_s, password: params[:password].to_s)
if user
user.regenerate_session_token
return_to = session[:return_to]
reset_session
session[:session_token] = user.session_token
redirect_to(return_to || admin_events_path, notice: 'Signed in.')
else
flash.now[:alert] = 'Invalid username or password.'
render :new, status: :unprocessable_entity
end
end

def destroy
current_user&.update!(session_token: nil)
reset_session
redirect_to login_path, notice: 'Signed out.'
end
end
11 changes: 11 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class User < ApplicationRecord
has_secure_password
has_secure_token :session_token

encrypts :password_digest
encrypts :session_token, deterministic: true

normalizes :username, with: ->(v) { v.to_s.strip.downcase }

validates :username, presence: true, uniqueness: true
end
6 changes: 6 additions & 0 deletions app/views/layouts/common/_navigation.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<% end %>
<%= link_to "Chat", chat_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors" %>
<%= link_to "Past Events", past_events_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors" %>
<% if signed_in? %>
<%= button_to "Sign out", logout_path, method: :delete, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors bg-transparent border-0 p-0 cursor-pointer" %>
<% end %>
</nav>

<!-- Mobile menu button -->
Expand All @@ -47,6 +50,9 @@
<% end %>
<%= link_to "Chat", chat_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors px-3 py-2 rounded-lg hover:bg-ruby-50" %>
<%= link_to "Past Events", past_events_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors px-3 py-2 rounded-lg hover:bg-ruby-50" %>
<% if signed_in? %>
<%= button_to "Sign out", logout_path, method: :delete, class: "text-left text-gray-700 hover:text-ruby-500 font-medium transition-colors px-3 py-2 rounded-lg hover:bg-ruby-50 bg-transparent border-0 cursor-pointer w-full" %>
<% end %>
</div>
</div>
</div>
Expand Down
35 changes: 35 additions & 0 deletions app/views/sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<% content_for :page_title, "Sign in" %>

<div class="max-w-md mx-auto px-4 py-16">
<div class="card p-8">
<h1 class="text-2xl font-semibold mb-6">Sign in</h1>

<%= form_with url: login_path, scope: :session, local: true do |form| %>
<div class="space-y-4">
<div>
<%= form.label :username, class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.text_field :username,
name: :username,
value: params[:username],
autocomplete: "username",
autofocus: true,
required: true,
class: "block w-full px-4 py-3 rounded-lg border border-gray-300 shadow-sm focus:border-ruby-500 focus:ring-1 focus:ring-ruby-500 transition-colors" %>
</div>

<div>
<%= form.label :password, class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.password_field :password,
name: :password,
autocomplete: "current-password",
required: true,
class: "block w-full px-4 py-3 rounded-lg border border-gray-300 shadow-sm focus:border-ruby-500 focus:ring-1 focus:ring-ruby-500 transition-colors" %>
</div>

<div class="pt-2">
<%= form.submit "Sign in", class: "btn-primary w-full" %>
</div>
</div>
<% end %>
</div>
</div>
5 changes: 4 additions & 1 deletion bin/docker-entrypoint
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#!/bin/bash -e

# If running the rails server then create or migrate existing database
# If running the rails server then create or migrate existing database,
# then upsert admins from encrypted credentials. admins:sync is idempotent
# (find_or_initialize_by on username) and does not touch event fixtures.
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
./bin/rails db:prepare
./bin/rails admins:sync
fi

exec "${@}"
2 changes: 1 addition & 1 deletion config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
L18YmnmQ7HVCtRlwFNxwc6o+I59OjZMzUA2KVdQ+TwesMDTOH7amkAqJHrILU9XvCaP+v3WBFdwZb8+MoLE6CS7AUiKh5tP99uNiw8Ri/iWSNIvX6jZorKlOdszaKs/KGr+0Exe3NVvOUeD6t/8CRWKTdQvTwOedpzloIhhlIz0r0Fi/+tT2dzezH91aKKyCr3hKYbkD7Wvai8/36zGC3JOWepTjsRxsHA6PCjwV24g+7oBnsH7KiboW1whGuSz8SOVyPHJZFQvwFi571oAxQLY0qUW7C0qkUA/8TMoIcEtmcLEPbLJE9Nya/pe07xR8MQetKb2q29HD1Aa4/leygO2EyONUMpDPg0Cm/XJTOswvkhGsmp+yXVeyjr8ISmVyKR7X25+a8Yhj9JuTnP+SRb0FIWnjWPD5rRE/VFKA/96JEiWfHp4IaZ9iHwhJ9C5AP7G2n280mwcrWiKTaqc0U2g2dkb6XDxfy+ozAl1T0wSRlCnB9twGSyVAH9eSaIzzeZx6i46uPlP6q/Y4jWp8lfNaKxCoGYixeQzlK6DwKHnmbdmtx/2xgJEmWdDt--VPOiYqTn+QXyR0PA--1bBCdyTlIYOVD/PPlfLLNA==
Ukz+P33RYHWu9L3Y3cTWNa1OT6pp0XBxHF8fTmkOkWsTxtQ1vhcI55K2BuSg7WedwQa/CLaGQXVwn1qs/mO0HSglXYGp3T/x+EfNXjGOp3TDlwgwS+Q3fCqkwXpD4/50FXlEVU5MgsY+yEfwWyy/XpY/byHQacyoj+UIfPMiPE8WEyLA8ImDxEQLH6UxBmY9GqXUTRJiyD6fxiIChuHnyNabwIDklRaYvObwiJd1aZUgj4hh5bNl3YNCtN4mUbV1Vd9HjDQsoY/5mugkK0F4SSnnqL3/40pZQblRUmz9kKF5AEJS/eH4fOPgZPC3es+4gT8PllpF1mNSsl/pM6wuHmgcIgZA3Vp5mpiAfvjec8igUsbnBkYIj5TpcSRZjxWvubGuMMaQ455dN25fV77AD4bdCGtE55AI39v4fPQekOJFJRvhxWrdsSmPk60d0mBmiPNGKwdHKG33diylbfqIZe0KNWwIpawSmoVRK4YkAIYws24ZaRHp3vw7zK48ryx2lQ7Q+2vez7HklSTGOXWdHhKyXKMr3y9+PNxiEEQxqCGwrtcCTMrXXX2zFvpUra3hB27eNVapx2bXEJZKNNS46Copi7gCyziR00KrFIUSO1M=--U9HaVnhO9PdPHU2z--vPBFOb2my1bEWWgRaoWSGw==
7 changes: 7 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,11 @@

# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true

# Static Active Record encryption keys for the test suite. Production keys
# live in encrypted credentials; test data is throwaway and CI doesn't have
# (nor should it need) the master key.
config.active_record.encryption.primary_key = 'test_primary_key_not_for_production_use_1234'
config.active_record.encryption.deterministic_key = 'test_deterministic_key_not_for_production_1234'
config.active_record.encryption.key_derivation_salt = 'test_key_derivation_salt_not_for_production_1234'
end
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
Rails.application.routes.draw do
get 'login', to: 'sessions#new', as: :login
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy', as: :logout

namespace :admin do
root to: redirect('/admin/events')
resources :events
Expand Down
14 changes: 14 additions & 0 deletions db/migrate/20260711022932_create_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :username, null: false
t.string :password_digest, null: false
t.string :session_token

t.timestamps
end

add_index :users, :username, unique: true
add_index :users, :session_token, unique: true
end
end
12 changes: 11 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading