Compare commits
2 Commits
b943b4c8bd
...
a69be52b72
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a69be52b72 | ||
|
|
35f10c4bda |
@@ -0,0 +1,2 @@
|
||||
[tools]
|
||||
ruby = "4"
|
||||
@@ -3,7 +3,7 @@ source "https://rubygems.org"
|
||||
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
|
||||
gem "rails", "~> 8.1.2"
|
||||
# Excel import for seeds
|
||||
gem "roo", "~> 2.10"
|
||||
gem "roo", "~> 3.0"
|
||||
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
|
||||
gem "propshaft"
|
||||
# Use sqlite3 as the database for Active Record
|
||||
|
||||
+11
-6
@@ -111,6 +111,7 @@ GEM
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
crass (1.0.6)
|
||||
csv (3.3.5)
|
||||
daemons (1.4.1)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
@@ -286,9 +287,12 @@ GEM
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.4)
|
||||
roo (2.10.1)
|
||||
roo (3.0.0)
|
||||
base64 (~> 0.2)
|
||||
csv (~> 3)
|
||||
logger (~> 1)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (>= 1.3.0, < 3.0.0)
|
||||
rubyzip (>= 3.0.0, < 4.0.0)
|
||||
rubocop (1.82.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
@@ -322,7 +326,7 @@ GEM
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.40.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -424,7 +428,7 @@ DEPENDENCIES
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.1.2)
|
||||
roo (~> 2.10)
|
||||
roo (~> 3.0)
|
||||
rubocop-rails-omakase
|
||||
selenium-webdriver
|
||||
solid_cable
|
||||
@@ -467,6 +471,7 @@ CHECKSUMS
|
||||
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
||||
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
||||
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
|
||||
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
|
||||
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
||||
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
|
||||
@@ -539,7 +544,7 @@ CHECKSUMS
|
||||
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
||||
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||
roo (2.10.1) sha256=cbb43bc955f9c110e74b721c835fb9bd3515b63af88ec709ac87fbf30f8be70e
|
||||
roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
|
||||
rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273
|
||||
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
|
||||
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
|
||||
@@ -548,7 +553,7 @@ CHECKSUMS
|
||||
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
|
||||
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
||||
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
|
||||
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
||||
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
|
||||
sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
|
||||
|
||||
@@ -44,6 +44,12 @@ class Admin::InvitationsController < Admin::BaseController
|
||||
private
|
||||
|
||||
def invitation_params
|
||||
params.require(:user).permit(:email, :name, :role, :primary_language)
|
||||
permitted = params.require(:user).permit(:email, :name, :primary_language)
|
||||
|
||||
if params[:user][:role].present? && User.roles.key?(params[:user][:role])
|
||||
permitted[:role] = params[:user][:role]
|
||||
end
|
||||
|
||||
permitted
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,14 +3,20 @@ class Admin::UsersController < Admin::BaseController
|
||||
|
||||
def index
|
||||
@users = User.order(created_at: :desc)
|
||||
@users = @users.where(role: params[:role]) if params[:role].present?
|
||||
@users = @users.where("email LIKE ?", "%#{params[:q]}%") if params[:q].present?
|
||||
.by_role(params[:role])
|
||||
.search_email(params[:q])
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
# Prevent users from modifying their own role
|
||||
if @user == current_user && user_params[:role].present?
|
||||
redirect_to admin_users_path, alert: "You cannot modify your own role."
|
||||
return
|
||||
end
|
||||
|
||||
if @user.update(user_params)
|
||||
redirect_to admin_users_path, notice: "User updated successfully."
|
||||
else
|
||||
@@ -40,6 +46,13 @@ class Admin::UsersController < Admin::BaseController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:name, :email, :role, :primary_language)
|
||||
permitted = params.require(:user).permit(:name, :email, :primary_language)
|
||||
|
||||
# Only allow role if it's a valid role enum value
|
||||
if params[:user][:role].present? && User.roles.key?(params[:user][:role])
|
||||
permitted[:role] = params[:user][:role]
|
||||
end
|
||||
|
||||
permitted
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class EntriesController < ApplicationController
|
||||
before_action :set_entry, only: [ :show, :edit, :update ]
|
||||
|
||||
def index
|
||||
@language_code = params[:language].presence
|
||||
@language_code = validate_language_code(params[:language].presence)
|
||||
@category = params[:category].presence
|
||||
@query = params[:q].to_s.strip
|
||||
@starts_with = params[:starts_with].presence
|
||||
@@ -79,4 +79,10 @@ class EntriesController < ApplicationController
|
||||
def entry_params
|
||||
params.require(:entry).permit(:category)
|
||||
end
|
||||
|
||||
def validate_language_code(code)
|
||||
return nil if code.blank?
|
||||
|
||||
SupportedLanguage.valid_codes.include?(code) ? code : nil
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Entry < ApplicationRecord
|
||||
return none unless valid_lang?(language_code)
|
||||
|
||||
where.not(language_code => [ nil, "" ])
|
||||
.order(Arel.sql("#{language_code} ASC"))
|
||||
.order(arel_table[language_code].asc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -21,6 +21,9 @@ class User < ApplicationRecord
|
||||
validates :email, presence: true, uniqueness: true
|
||||
validates :password, length: { minimum: 12 }, if: -> { password.present? }
|
||||
|
||||
scope :by_role, ->(role) { where(role: role) if role.present? }
|
||||
scope :search_email, ->(q) { where("email LIKE ?", "%#{sanitize_sql_like(q)}%") if q.present? }
|
||||
|
||||
# Invitation token expires after 14 days
|
||||
INVITATION_TOKEN_EXPIRY = 14.days
|
||||
|
||||
|
||||
+20
-11
@@ -22,6 +22,24 @@
|
||||
- [ ] Contributor permissions enforcement (for entry editing)
|
||||
- [ ] Reviewer permissions enforcement (for review queue)
|
||||
|
||||
## Security & Vulnerabilities
|
||||
|
||||
- [x] **Fixed user-controlled method execution** (HIGH)
|
||||
- Added language code validation in EntriesController
|
||||
- Prevents arbitrary method execution via `public_send()`
|
||||
- [x] **Fixed SQL injection in Entry model** (MEDIUM)
|
||||
- Replaced string interpolation with Arel safe column references
|
||||
- Changed `Arel.sql("#{language_code} ASC")` to `arel_table[language_code].asc`
|
||||
- [x] **Fixed mass assignment vulnerabilities** (MEDIUM)
|
||||
- Added role validation in admin invitations and user management
|
||||
- Only allows valid enum role values
|
||||
- Prevents users from modifying their own role
|
||||
- [x] **Fixed SQL LIKE injection** (MEDIUM)
|
||||
- Added `sanitize_sql_like()` for email search in UsersController
|
||||
- Prevents wildcard injection attacks
|
||||
|
||||
**Status:** All Brakeman security warnings resolved ✓
|
||||
|
||||
## Core Features
|
||||
|
||||
### Search & Browse
|
||||
@@ -119,24 +137,15 @@
|
||||
|
||||
---
|
||||
|
||||
## Completed
|
||||
## Completed (Not Tracked Above)
|
||||
|
||||
- [x] **Invitation system** (complete flow with email, acceptance, and expiry validation)
|
||||
- [x] **Invitation acceptance flow** (users can accept invitations and set passwords)
|
||||
- [x] **Invitation mailer** (HTML and text email templates with styled design)
|
||||
- [x] **Token expiry validation** (14-day expiration for invitation links)
|
||||
- [x] **Controller tests** (40 tests with 160+ assertions for authentication)
|
||||
- [x] **Authentication system** (login/logout with session management)
|
||||
- [x] **Admin layout design** updated to match entries page style
|
||||
- [x] **Dynamic navigation** (Admin button for logged-in admins, Sign In for guests)
|
||||
- [x] **Authorization middleware** (Admin::BaseController with role checks)
|
||||
- [x] **Invitation token generation** (secure token creation for new users)
|
||||
- [x] **Controller tests** (40 tests with 160+ assertions for authentication)
|
||||
- [x] **Search input loses focus on filter change**
|
||||
- [x] **Mismatched enum syntax** in models
|
||||
- [x] **Replace hardcoded LANGUAGE_COLUMNS** with dynamic query
|
||||
- [x] **Improve fixture quality** (resolved foreign key violations)
|
||||
- [x] **XLSX download** button for entries
|
||||
- [x] **FTS5 integration** (migration added)
|
||||
- [x] **Database schema** implementation (all models and migrations)
|
||||
- [x] **Supported languages** table with seed data
|
||||
- [x] **Filters do not update with new search results**
|
||||
|
||||
Reference in New Issue
Block a user