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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@
# Ignore key files for decrypting credentials and more.
/config/*.key


/app/assets/builds/*
!/app/assets/builds/.keep
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gem "stimulus-rails"
gem "jbuilder"

# 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 Expand Up @@ -64,3 +64,6 @@ group :test do
gem "capybara"
gem "selenium-webdriver"
end

gem "tailwindcss-rails", "~> 4.4"
gem "faker", "~> 3.8", group: :development
24 changes: 24 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ GEM
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.22)
bcrypt_pbkdf (1.1.2)
bigdecimal (4.1.2)
bindex (0.8.1)
Expand Down Expand Up @@ -113,6 +114,8 @@ GEM
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
faker (3.8.0)
i18n (>= 1.8.11, < 2)
ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.4-arm-linux-gnu)
Expand Down Expand Up @@ -347,6 +350,15 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
tailwindcss-rails (4.4.0)
railties (>= 7.0.0)
tailwindcss-ruby (~> 4.0)
tailwindcss-ruby (4.2.2)
tailwindcss-ruby (4.2.2-aarch64-linux-gnu)
tailwindcss-ruby (4.2.2-aarch64-linux-musl)
tailwindcss-ruby (4.2.2-arm64-darwin)
tailwindcss-ruby (4.2.2-x86_64-linux-gnu)
tailwindcss-ruby (4.2.2-x86_64-linux-musl)
thor (1.5.0)
thruster (0.1.20)
thruster (0.1.20-aarch64-linux)
Expand Down Expand Up @@ -389,11 +401,13 @@ PLATFORMS
x86_64-linux-musl

DEPENDENCIES
bcrypt (~> 3.1.7)
bootsnap
brakeman
bundler-audit
capybara
debug
faker (~> 3.8)
image_processing (~> 1.2)
importmap-rails
jbuilder
Expand All @@ -408,6 +422,7 @@ DEPENDENCIES
solid_queue
sqlite3 (>= 2.1)
stimulus-rails
tailwindcss-rails (~> 4.4)
thruster
turbo-rails
tzinfo-data
Expand All @@ -429,6 +444,7 @@ CHECKSUMS
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
Expand All @@ -448,6 +464,7 @@ CHECKSUMS
erb (6.0.3) sha256=e43685a8a0a0ea6a924871b2162e8953ef73147ce46b75b36d1f6774fd286e91
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc
faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
Expand Down Expand Up @@ -540,6 +557,13 @@ CHECKSUMS
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
tailwindcss-rails (4.4.0) sha256=efa2961351a52acebe616e645a81a30bb4f27fde46cc06ce7688d1cd1131e916
tailwindcss-ruby (4.2.2) sha256=ce66da7b01fb6ef1ad6485b4b8c3476fac959f3324894fd26ec7c67ab3996d30
tailwindcss-ruby (4.2.2-aarch64-linux-gnu) sha256=8656621046bb54c9c368cd1d2f03f7bfaf6046a4fe7060c574b9958043f1deeb
tailwindcss-ruby (4.2.2-aarch64-linux-musl) sha256=3dbaa653a5e9cddbb6bc73598a566d7172a91724463000cd594624dfe5b0eaec
tailwindcss-ruby (4.2.2-arm64-darwin) sha256=2d66feba0c1ffca5b79246bd881bfb9a6b2298d57c4bc83ee3a8c3233df79d41
tailwindcss-ruby (4.2.2-x86_64-linux-gnu) sha256=7f5e7cdd697ff25600d684cedb4df4a56736633c231ee03c7148992c62fd228f
tailwindcss-ruby (4.2.2-x86_64-linux-musl) sha256=676b802dafc677983d471f3acf2dddbddea4e978ea0300bfa21ebd6ab167d6a8
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
thruster (0.1.20) sha256=c05f2fbcae527bbe093a6e6d84fb12d9d680617e7c162325d9b97e8e9d4b5201
thruster (0.1.20-aarch64-linux) sha256=754f1701061235235165dde31e7a3bc87ec88066a02981ff4241fcda0d76d397
Expand Down
2 changes: 2 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/rails server
css: bin/rails tailwindcss:watch
Empty file added app/assets/builds/.keep
Empty file.
17 changes: 17 additions & 0 deletions app/assets/tailwind/application.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import "tailwindcss";

@source not "./daisyui{,*}.mjs";

@plugin "./daisyui.mjs" {
themes: light --default;
}

@plugin "./daisyui-theme.mjs" {
name: "light";
default: true;
prefersdark: false;
color-scheme: light;
--color-primary: rgb(113, 190, 183);
--color-primary-content: white;
--color-secondary: #B5E56D;
}
93 changes: 93 additions & 0 deletions app/assets/tailwind/daisyui-theme.mjs

Large diffs are not rendered by default.

1,026 changes: 1,026 additions & 0 deletions app/assets/tailwind/daisyui.mjs

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
set_current_user || reject_unauthorized_connection
end

private
def set_current_user
if session = Session.find_by(id: cookies.signed[:session_id])
self.current_user = session.user
end
end
end
end
1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class ApplicationController < ActionController::Base
include Authentication
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern

Expand Down
52 changes: 52 additions & 0 deletions app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Authentication
extend ActiveSupport::Concern

included do
before_action :require_authentication
helper_method :authenticated?
end

class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end

private
def authenticated?
resume_session
end

def require_authentication
resume_session || request_authentication
end

def resume_session
Current.session ||= find_session_by_cookie
end

def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
end

def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path
end

def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end

def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end

def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end
35 changes: 35 additions & 0 deletions app/controllers/passwords_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class PasswordsController < ApplicationController
allow_unauthenticated_access
before_action :set_user_by_token, only: %i[ edit update ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

def new
end

def create
if user = User.find_by(email_address: params[:email_address])
PasswordsMailer.reset(user).deliver_later
end

redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
end

def edit
end

def update
if @user.update(params.permit(:password, :password_confirmation))
@user.sessions.destroy_all
redirect_to new_session_path, notice: "Password has been reset."
else
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
end
end

private
def set_user_by_token
@user = User.find_by_password_reset_token!(params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
end
end
71 changes: 71 additions & 0 deletions app/controllers/schools_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
class SchoolsController < ApplicationController
before_action :set_school, only: %i[ show edit update destroy ]

# GET /schools or /schools.json
def index
@schools = School.all
end

# GET /schools/1 or /schools/1.json
def show
end

# GET /schools/new
def new
@school = School.new
end

# GET /schools/1/edit
def edit
end

# POST /schools or /schools.json
def create
@school = School.new(school_params)

respond_to do |format|
if @school.save
format.html { redirect_to @school, notice: "School was successfully created." }
format.json { render :show, status: :created, location: @school }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @school.errors, status: :unprocessable_entity }
end
end
end

# PATCH/PUT /schools/1 or /schools/1.json
def update
respond_to do |format|
if @school.update(school_params)
format.html { redirect_to @school, notice: "School was successfully updated.", status: :see_other }
format.json { render :show, status: :ok, location: @school }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @school.errors, status: :unprocessable_entity }
end
end
end

# DELETE /schools/1 or /schools/1.json
def destroy
@school.destroy!

respond_to do |format|
format.html { redirect_to schools_path, notice: "School was successfully destroyed.", status: :see_other }
format.json { head :no_content }
end
end

private

# Use callbacks to share common setup or constraints between actions.
def set_school
@school = School.find(params.expect(:id))
end

# Only allow a list of trusted parameters through.
def school_params
params.expect(school: [ :name ])
end
end
21 changes: 21 additions & 0 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class SessionsController < ApplicationController
allow_unauthenticated_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

def new
end

def create
if user = User.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to after_authentication_url
else
redirect_to new_session_path, alert: "Try another email address or password."
end
end

def destroy
terminate_session
redirect_to new_session_path, status: :see_other
end
end
Loading