Much work.
This commit is contained in:
7
Gemfile
7
Gemfile
@@ -20,7 +20,12 @@ gem "tailwindcss-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"
|
||||
|
||||
# OpenID Connect authentication support
|
||||
gem "openid_connect", "~> 2.2"
|
||||
gem "omniauth", "~> 2.1"
|
||||
gem "omniauth_openid_connect", "~> 0.8"
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
||||
|
||||
72
Gemfile.lock
72
Gemfile.lock
@@ -77,10 +77,14 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
bigdecimal (3.3.1)
|
||||
bindata (2.5.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
@@ -110,10 +114,20 @@ GEM
|
||||
dotenv (3.1.8)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (5.1.3)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
faraday (2.14.0)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.4.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-net_http (3.4.1)
|
||||
net-http (>= 0.5.0)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-aarch64-linux-musl)
|
||||
ffi (1.17.2-arm-linux-gnu)
|
||||
@@ -126,6 +140,7 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
hashie (5.0.0)
|
||||
httparty (0.23.2)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
@@ -148,6 +163,13 @@ GEM
|
||||
actionview (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
json (2.15.2)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
base64
|
||||
bindata
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
kamal (2.8.2)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -181,6 +203,8 @@ GEM
|
||||
msgpack (1.8.0)
|
||||
multi_xml (0.7.2)
|
||||
bigdecimal (~> 3.1)
|
||||
net-http (0.7.0)
|
||||
uri
|
||||
net-imap (0.5.12)
|
||||
date
|
||||
net-protocol
|
||||
@@ -210,6 +234,27 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
omniauth (2.1.4)
|
||||
hashie (>= 3.4.6)
|
||||
logger
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth_openid_connect (0.8.0)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 2.2)
|
||||
openid_connect (2.3.1)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
email_validator
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.16)
|
||||
mail
|
||||
rack-oauth2 (~> 2.2)
|
||||
swd (~> 2.0)
|
||||
tzinfo
|
||||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
ostruct (0.6.3)
|
||||
pagy (9.4.0)
|
||||
parallel (1.27.0)
|
||||
@@ -233,6 +278,17 @@ GEM
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.3)
|
||||
rack-oauth2 (2.2.1)
|
||||
activesupport
|
||||
attr_required
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (4.2.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
@@ -353,6 +409,11 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.7)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
tailwindcss-rails (4.4.0)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-ruby (~> 4.0)
|
||||
@@ -379,11 +440,18 @@ GEM
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.1.0)
|
||||
useragent (0.16.11)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webfinger (2.1.3)
|
||||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
@@ -405,6 +473,7 @@ PLATFORMS
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
brakeman
|
||||
bundler-audit
|
||||
@@ -416,6 +485,9 @@ DEPENDENCIES
|
||||
jbuilder
|
||||
kamal
|
||||
maxmind-db
|
||||
omniauth (~> 2.1)
|
||||
omniauth_openid_connect (~> 0.8)
|
||||
openid_connect (~> 2.2)
|
||||
pagy
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
|
||||
16
app/channels/application_cable/connection.rb
Normal file
16
app/channels/application_cable/connection.rb
Normal file
@@ -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
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
module Api
|
||||
class RulesController < ApplicationController
|
||||
# NOTE: This controller is now SECONDARY/UNUSED for primary agent synchronization
|
||||
# Agents get rule updates via event responses (see Api::EventsController)
|
||||
# These endpoints are kept for administrative/debugging purposes only
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
before_action :authenticate_project!
|
||||
before_action :check_project_enabled
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,4 +7,40 @@ class ApplicationController < ActionController::Base
|
||||
stale_when_importmap_changes
|
||||
|
||||
include Pagy::Backend
|
||||
|
||||
helper_method :current_user, :user_signed_in?, :current_user_admin?, :current_user_viewer?
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
Current.session&.user
|
||||
end
|
||||
|
||||
def user_signed_in?
|
||||
current_user.present?
|
||||
end
|
||||
|
||||
def current_user_admin?
|
||||
current_user&.admin?
|
||||
end
|
||||
|
||||
def current_user_viewer?
|
||||
current_user&.viewer?
|
||||
end
|
||||
|
||||
def require_admin
|
||||
unless current_user_admin?
|
||||
redirect_to root_path, alert: "Admin access required"
|
||||
end
|
||||
end
|
||||
|
||||
def require_write_access
|
||||
if current_user_viewer?
|
||||
redirect_to root_path, alert: "Viewer access - cannot make changes"
|
||||
end
|
||||
end
|
||||
|
||||
def after_authentication_url
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
end
|
||||
end
|
||||
|
||||
52
app/controllers/concerns/authentication.rb
Normal file
52
app/controllers/concerns/authentication.rb
Normal file
@@ -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
|
||||
26
app/controllers/omniauth_callbacks_controller.rb
Normal file
26
app/controllers/omniauth_callbacks_controller.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class OmniauthCallbacksController < ApplicationController
|
||||
allow_unauthenticated_access only: [:oidc, :failure]
|
||||
|
||||
def oidc
|
||||
auth_hash = request.env['omniauth.auth']
|
||||
|
||||
user = User.from_oidc(auth_hash)
|
||||
|
||||
if user
|
||||
start_new_session_for(user)
|
||||
redirect_to after_login_path, notice: "Successfully signed in via OIDC"
|
||||
else
|
||||
redirect_to new_session_path, alert: "Failed to sign in via OIDC - email not found"
|
||||
end
|
||||
end
|
||||
|
||||
def failure
|
||||
redirect_to new_session_path, alert: "Authentication failed: #{params[:message]}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_login_path
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
end
|
||||
end
|
||||
35
app/controllers/passwords_controller.rb
Normal file
35
app/controllers/passwords_controller.rb
Normal file
@@ -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
|
||||
31
app/controllers/registrations_controller.rb
Normal file
31
app/controllers/registrations_controller.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class RegistrationsController < ApplicationController
|
||||
allow_unauthenticated_access only: [:new, :create]
|
||||
before_action :ensure_no_users_exist, only: [:new, :create]
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
|
||||
if @user.save
|
||||
start_new_session_for(@user)
|
||||
redirect_to root_path, notice: "Welcome! Your admin account has been created successfully."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email_address, :password, :password_confirmation)
|
||||
end
|
||||
|
||||
def ensure_no_users_exist
|
||||
if User.exists?
|
||||
redirect_to new_session_path, alert: "Registration is not allowed. Users already exist."
|
||||
end
|
||||
end
|
||||
end
|
||||
32
app/controllers/sessions_controller.rb
Normal file
32
app/controllers/sessions_controller.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
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
|
||||
@show_oidc_login = oidc_configured?
|
||||
@oidc_provider_name = ENV['OIDC_PROVIDER_NAME'] || 'OpenID Connect'
|
||||
@show_registration = User.none?
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def oidc_configured?
|
||||
ENV['OIDC_DISCOVERY_URL'].present? &&
|
||||
ENV['OIDC_CLIENT_ID'].present? &&
|
||||
ENV['OIDC_CLIENT_SECRET'].present?
|
||||
end
|
||||
end
|
||||
32
app/controllers/users_controller.rb
Normal file
32
app/controllers/users_controller.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class UsersController < ApplicationController
|
||||
before_action :require_admin
|
||||
before_action :set_user, only: [:show, :edit, :update]
|
||||
|
||||
def index
|
||||
@users = User.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
redirect_to @user, notice: "User was successfully updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:role)
|
||||
end
|
||||
end
|
||||
2
app/helpers/registrations_helper.rb
Normal file
2
app/helpers/registrations_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module RegistrationsHelper
|
||||
end
|
||||
2
app/helpers/users_helper.rb
Normal file
2
app/helpers/users_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module UsersHelper
|
||||
end
|
||||
6
app/mailers/passwords_mailer.rb
Normal file
6
app/mailers/passwords_mailer.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class PasswordsMailer < ApplicationMailer
|
||||
def reset(user)
|
||||
@user = user
|
||||
mail subject: "Reset your password", to: user.email_address
|
||||
end
|
||||
end
|
||||
@@ -1,16 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :baffle_host
|
||||
attribute :baffle_internal_host
|
||||
attribute :project
|
||||
attribute :ip
|
||||
|
||||
def baffle_host
|
||||
@baffle_host || ENV.fetch("BAFFLE_HOST", "localhost:3000")
|
||||
end
|
||||
|
||||
def baffle_internal_host
|
||||
@baffle_internal_host || ENV.fetch("BAFFLE_INTERNAL_HOST", nil)
|
||||
end
|
||||
attribute :session
|
||||
delegate :user, to: :session, allow_nil: true
|
||||
end
|
||||
|
||||
3
app/models/session.rb
Normal file
3
app/models/session.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Session < ApplicationRecord
|
||||
belongs_to :user
|
||||
end
|
||||
64
app/models/user.rb
Normal file
64
app/models/user.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
class User < ApplicationRecord
|
||||
has_secure_password
|
||||
has_many :sessions, dependent: :destroy
|
||||
|
||||
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
||||
|
||||
enum :role, { admin: 0, user: 1, viewer: 2 }, default: :user
|
||||
|
||||
validates :email_address, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :role, presence: true
|
||||
|
||||
before_validation :set_first_user_as_admin, on: :create
|
||||
|
||||
def self.from_oidc(auth_hash)
|
||||
# Extract user info from OIDC auth hash
|
||||
email = auth_hash.dig('info', 'email')
|
||||
return nil unless email
|
||||
|
||||
user = find_or_initialize_by(email_address: email)
|
||||
|
||||
# Map OIDC groups to role
|
||||
if auth_hash.dig('extra', 'raw_info', 'groups')
|
||||
user.role = map_oidc_groups_to_role(auth_hash.dig('extra', 'raw_info', 'groups'))
|
||||
end
|
||||
|
||||
# Don't override password for OIDC users
|
||||
user.save!(validate: false) if user.new_record?
|
||||
user
|
||||
end
|
||||
|
||||
def admin?
|
||||
role == 'admin'
|
||||
end
|
||||
|
||||
def viewer?
|
||||
role == 'viewer'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_first_user_as_admin
|
||||
return if User.any?
|
||||
self.role = 'admin'
|
||||
end
|
||||
|
||||
def self.map_oidc_groups_to_role(groups)
|
||||
groups = Array(groups)
|
||||
|
||||
# Check admin groups first
|
||||
admin_groups = ENV['OIDC_ADMIN_GROUPS']&.split(',')&.map(&:strip)
|
||||
return 'admin' if admin_groups && (admin_groups & groups).any?
|
||||
|
||||
# Check user groups
|
||||
user_groups = ENV['OIDC_USER_GROUPS']&.split(',')&.map(&:strip)
|
||||
return 'user' if user_groups && (user_groups & groups).any?
|
||||
|
||||
# Check viewer groups
|
||||
viewer_groups = ENV['OIDC_VIEWER_GROUPS']&.split(',')&.map(&:strip)
|
||||
return 'viewer' if viewer_groups && (viewer_groups & groups).any?
|
||||
|
||||
# Default to user if no group matches
|
||||
'user'
|
||||
end
|
||||
end
|
||||
@@ -54,8 +54,41 @@
|
||||
<%= link_to "Projects", projects_path, class: "nav-link" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Rule Sets", rule_sets_path, class: "nav-link" %>
|
||||
<%= link_to "Rules", rules_path, class: "nav-link" %>
|
||||
</li>
|
||||
<% if user_signed_in? && current_user_admin? %>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Users", users_path, class: "nav-link" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<% if user_signed_in? %>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<%= current_user.email_address %>
|
||||
<span class="badge bg-secondary ms-1"><%= current_user.role %></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<% if current_user_admin? %>
|
||||
<li><%= link_to "Manage Users", users_path, class: "dropdown-item" %></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<% end %>
|
||||
<li><%= link_to "Sign Out", session_path, data: { turbo_method: :delete }, class: "dropdown-item" %></li>
|
||||
</ul>
|
||||
</li>
|
||||
<% else %>
|
||||
<% if User.none? %>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Create Admin Account", new_registration_path, class: "nav-link btn btn-success text-white" %>
|
||||
</li>
|
||||
<% else %>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Sign In", new_session_path, class: "nav-link" %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
21
app/views/passwords/edit.html.erb
Normal file
21
app/views/passwords/edit.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<% if alert = flash[:alert] %>
|
||||
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
|
||||
<% end %>
|
||||
|
||||
<h1 class="font-bold text-4xl">Update your password</h1>
|
||||
|
||||
<%= form_with url: password_path(params[:token]), method: :put, class: "contents" do |form| %>
|
||||
<div class="my-5">
|
||||
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<%= form.submit "Save", class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
17
app/views/passwords/new.html.erb
Normal file
17
app/views/passwords/new.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<% if alert = flash[:alert] %>
|
||||
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
|
||||
<% end %>
|
||||
|
||||
<h1 class="font-bold text-4xl">Forgot your password?</h1>
|
||||
|
||||
<%= form_with url: passwords_path, class: "contents" do |form| %>
|
||||
<div class="my-5">
|
||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<%= form.submit "Email reset instructions", class: "w-full sm:w-auto text-center rounded-lg px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
6
app/views/passwords_mailer/reset.html.erb
Normal file
6
app/views/passwords_mailer/reset.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<p>
|
||||
You can reset your password on
|
||||
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
|
||||
|
||||
This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
|
||||
</p>
|
||||
4
app/views/passwords_mailer/reset.text.erb
Normal file
4
app/views/passwords_mailer/reset.text.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
You can reset your password on
|
||||
<%= edit_password_url(@user.password_reset_token) %>
|
||||
|
||||
This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
|
||||
4
app/views/registrations/create.html.erb
Normal file
4
app/views/registrations/create.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<h1 class="font-bold text-4xl">Registrations#create</h1>
|
||||
<p>Find me in app/views/registrations/create.html.erb</p>
|
||||
</div>
|
||||
61
app/views/registrations/new.html.erb
Normal file
61
app/views/registrations/new.html.erb
Normal file
@@ -0,0 +1,61 @@
|
||||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<% if alert = flash[:alert] %>
|
||||
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
|
||||
<% end %>
|
||||
|
||||
<% if notice = flash[:notice] %>
|
||||
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
|
||||
<% end %>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-blue-900 mb-2">🎉 Welcome to Baffle Hub!</h2>
|
||||
<p class="text-blue-700">
|
||||
This is the first time Baffle Hub is being set up. You'll create the initial administrator account
|
||||
that will have full access to manage users, projects, and system settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h1 class="font-bold text-3xl mb-6">Create Administrator Account</h1>
|
||||
|
||||
<%= form_with(model: @user, url: registration_path, class: "contents") do |form| %>
|
||||
<% if @user.errors.any? %>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||
<h2 class="text-red-800 font-medium mb-2">
|
||||
<%= pluralize(@user.errors.count, "error") %> prohibited this account from being saved:
|
||||
</h2>
|
||||
<ul class="list-disc list-inside text-red-700">
|
||||
<% @user.errors.each do |error| %>
|
||||
<li><%= error.full_message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "email", placeholder: "Enter your email address", class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Create a password", minlength: 8, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
<p class="mt-1 text-sm text-gray-600">Minimum 8 characters</p>
|
||||
</div>
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Confirm your password", minlength: 8, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<%= form.submit "Create Administrator Account", class: "rounded-md px-3.5 py-2.5 bg-green-600 hover:bg-green-500 text-white font-medium cursor-pointer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-8 text-sm text-gray-600">
|
||||
<p class="mb-2">This administrator account will have:</p>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Full system access and control</li>
|
||||
<li>Ability to manage other users</li>
|
||||
<li>Permission to create and manage projects</li>
|
||||
<li>Access to system configuration and analytics</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
57
app/views/sessions/new.html.erb
Normal file
57
app/views/sessions/new.html.erb
Normal file
@@ -0,0 +1,57 @@
|
||||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<% if alert = flash[:alert] %>
|
||||
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
|
||||
<% end %>
|
||||
|
||||
<% if notice = flash[:notice] %>
|
||||
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
|
||||
<% end %>
|
||||
|
||||
<% if @show_registration %>
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-green-900 mb-2">🚀 First Time Setup</h2>
|
||||
<p class="text-green-700 mb-4">
|
||||
No administrator account exists yet. Create the first administrator account to get started with Baffle Hub.
|
||||
</p>
|
||||
<%= link_to "Create Administrator Account", new_registration_path,
|
||||
class: "inline-block rounded-md px-4 py-2 bg-green-600 hover:bg-green-500 text-white font-medium" %>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<span class="text-gray-500">or</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h1 class="font-bold text-4xl">Sign in</h1>
|
||||
|
||||
<% if @show_oidc_login && !@show_registration %>
|
||||
<div class="my-6">
|
||||
<%= link_to "Sign in with #{@oidc_provider_name}", "/auth/oidc",
|
||||
class: "w-full block text-center rounded-md px-3.5 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium cursor-pointer" %>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<span class="text-gray-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_with url: session_url, class: "contents" do |form| %>
|
||||
<div class="my-5">
|
||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:flex sm:items-center sm:gap-4">
|
||||
<div class="inline">
|
||||
<%= form.submit "Sign in", class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-500 sm:mt-0">
|
||||
<%= link_to "Forgot password?", new_password_path, class: "text-gray-700 underline hover:no-underline" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
63
app/views/users/edit.html.erb
Normal file
63
app/views/users/edit.html.erb
Normal file
@@ -0,0 +1,63 @@
|
||||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<div class="flex items-center mb-6">
|
||||
<%= link_to "← Back to Users", users_path, class: "text-blue-600 hover:text-blue-800" %>
|
||||
</div>
|
||||
|
||||
<h1 class="font-bold text-3xl mb-6">Edit User</h1>
|
||||
|
||||
<% if notice = flash[:notice] %>
|
||||
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
|
||||
<% end %>
|
||||
|
||||
<% if alert = flash[:alert] %>
|
||||
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
|
||||
<% end %>
|
||||
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">User Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Email Address</label>
|
||||
<div class="mt-1 text-sm text-gray-900"><%= @user.email_address %></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Created</label>
|
||||
<div class="mt-1 text-sm text-gray-900"><%= @user.created_at.strftime("%B %d, %Y at %I:%M %p") %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with(model: @user, class: "contents") do |form| %>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">Role Assignment</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center">
|
||||
<%= form.radio_button :role, "admin", class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300" %>
|
||||
<%= form.label :role_admin, "Admin", class: "ml-3 block text-sm font-medium text-gray-700" %>
|
||||
<span class="ml-2 text-sm text-gray-500">- Full system access, user management, project creation</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.radio_button :role, "user", class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" %>
|
||||
<%= form.label :role_user, "User", class: "ml-3 block text-sm font-medium text-gray-700" %>
|
||||
<span class="ml-2 text-sm text-gray-500">- Read/write access to projects</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.radio_button :role, "viewer", class: "h-4 w-4 text-gray-600 focus:ring-gray-500 border-gray-300" %>
|
||||
<%= form.label :role_viewer, "Viewer", class: "ml-3 block text-sm font-medium text-gray-700" %>
|
||||
<span class="ml-2 text-sm text-gray-500">- Read-only access to all projects</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<%= form.submit "Update User", class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-medium cursor-pointer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
62
app/views/users/index.html.erb
Normal file
62
app/views/users/index.html.erb
Normal file
@@ -0,0 +1,62 @@
|
||||
<div class="mx-auto md:w-4/5 w-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="font-bold text-3xl">User Management</h1>
|
||||
<div class="text-sm text-gray-600">
|
||||
Total users: <%= @users.count %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if notice = flash[:notice] %>
|
||||
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
|
||||
<% end %>
|
||||
|
||||
<% if alert = flash[:alert] %>
|
||||
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
|
||||
<% end %>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<% @users.each do |user| %>
|
||||
<li>
|
||||
<div class="px-4 py-4 sm:px-6 hover:bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
<%= user.email_address.first.upcase %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
<%= user.email_address %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
Joined <%= user.created_at.strftime("%B %d, %Y") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
<% if user.admin? %>bg-purple-100 text-purple-800
|
||||
<% elsif user.viewer? %>bg-gray-100 text-gray-800
|
||||
<% else %>bg-blue-100 text-blue-800<% end %>">
|
||||
<%= user.role.capitalize %>
|
||||
</span>
|
||||
<%= link_to "Edit", edit_user_path(user),
|
||||
class: "inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% if @users.empty? %>
|
||||
<div class="text-center py-12">
|
||||
<p class="text-gray-500">No users found.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
4
app/views/users/show.html.erb
Normal file
4
app/views/users/show.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<h1 class="font-bold text-4xl">Users#show</h1>
|
||||
<p>Find me in app/views/users/show.html.erb</p>
|
||||
</div>
|
||||
4
app/views/users/update.html.erb
Normal file
4
app/views/users/update.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<h1 class="font-bold text-4xl">Users#update</h1>
|
||||
<p>Find me in app/views/users/update.html.erb</p>
|
||||
</div>
|
||||
@@ -6,6 +6,7 @@ default: &default
|
||||
namespace: <%= Rails.env %>
|
||||
|
||||
development:
|
||||
database: cache
|
||||
<<: *default
|
||||
|
||||
test:
|
||||
|
||||
@@ -10,8 +10,21 @@ default: &default
|
||||
timeout: 5000
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
database: storage/development.sqlite3
|
||||
primary:
|
||||
<<: *default
|
||||
database: storage/development.sqlite3
|
||||
cache:
|
||||
<<: *default
|
||||
database: storage/development_cache.sqlite3
|
||||
migrations_paths: db/cache_migrate
|
||||
queue:
|
||||
<<: *default
|
||||
database: storage/development_queue.sqlite3
|
||||
migrations_paths: db/queue_migrate
|
||||
cable:
|
||||
<<: *default
|
||||
database: storage/development_cable.sqlite3
|
||||
migrations_paths: db/cable_migrate
|
||||
|
||||
# Warning: The database defined as "test" will be erased and
|
||||
# re-generated from your development database when you run "rake".
|
||||
|
||||
@@ -87,4 +87,18 @@ Rails.application.configure do
|
||||
#
|
||||
# Skip DNS rebinding protection for the default health check endpoint.
|
||||
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
||||
|
||||
# Docker Compose friendly settings
|
||||
config.log_level = :info
|
||||
config.log_tags = [ :request_id ]
|
||||
|
||||
# Log to stdout for Docker container logging
|
||||
if ENV["RAILS_LOG_TO_STDOUT"].present?
|
||||
logger = ActiveSupport::Logger.new(STDOUT)
|
||||
logger.formatter = config.log_formatter
|
||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||
end
|
||||
|
||||
# Serve static files (Docker Compose deployments typically don't have a separate web server)
|
||||
config.public_file_server.enabled = true
|
||||
end
|
||||
|
||||
29
config/initializers/omniauth.rb
Normal file
29
config/initializers/omniauth.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
Rails.application.config.middleware.use OmniAuth::Builder do
|
||||
# Only configure OIDC if environment variables are present
|
||||
if ENV['OIDC_DISCOVERY_URL'].present? && ENV['OIDC_CLIENT_ID'].present? && ENV['OIDC_CLIENT_SECRET'].present?
|
||||
provider :openid_connect, {
|
||||
name: :oidc,
|
||||
scope: [:openid, :email, :groups],
|
||||
response_type: :code,
|
||||
client_options: {
|
||||
identifier: ENV['OIDC_CLIENT_ID'],
|
||||
secret: ENV['OIDC_CLIENT_SECRET'],
|
||||
redirect_uri: ENV['OIDC_REDIRECT_URI'] || "#{Rails.application.routes.url_helpers.root_url}auth/oidc/callback",
|
||||
discovery: true,
|
||||
authorization_endpoint: nil,
|
||||
token_endpoint: nil,
|
||||
userinfo_endpoint: nil,
|
||||
jwks_uri: nil
|
||||
},
|
||||
discovery_document: {
|
||||
issuer: ENV['OIDC_ISSUER'] # Optional, defaults to discovery URL issuer
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Disable OmniAuth logging in production
|
||||
OmniAuth.config.logger = Rails.logger if Rails.env.production?
|
||||
|
||||
# Set OmniAuth failure mode
|
||||
OmniAuth.config.failure_raise_out_environments = %w[development test]
|
||||
@@ -1,4 +1,16 @@
|
||||
Rails.application.routes.draw do
|
||||
# Registration only allowed when no users exist
|
||||
resource :registration, only: [:new, :create]
|
||||
resource :session
|
||||
resources :passwords, param: :token
|
||||
|
||||
# OIDC authentication routes
|
||||
get "/auth/failure", to: "omniauth_callbacks#failure"
|
||||
get "/auth/:provider/callback", to: "omniauth_callbacks#oidc"
|
||||
|
||||
# Admin user management (admin only)
|
||||
resources :users, only: [:index, :show, :edit, :update]
|
||||
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
@@ -7,10 +19,11 @@ Rails.application.routes.draw do
|
||||
|
||||
# WAF API
|
||||
namespace :api, defaults: { format: :json } do
|
||||
# Event ingestion
|
||||
# Event ingestion (PRIMARY method - includes rule updates in response)
|
||||
post ":project_id/events", to: "events#create"
|
||||
|
||||
# Rule synchronization
|
||||
# Rule synchronization (SECONDARY - for admin/debugging only)
|
||||
# Note: Agents should use event responses for rule synchronization
|
||||
get ":public_key/rules/version", to: "rules#version"
|
||||
get ":public_key/rules", to: "rules#index"
|
||||
end
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
ActiveRecord::Schema[7.1].define(version: 1) do
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 1) do
|
||||
create_table "solid_cable_messages", force: :cascade do |t|
|
||||
t.binary "channel", limit: 1024, null: false
|
||||
t.binary "payload", limit: 536870912, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "channel_hash", limit: 8, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.binary "payload", limit: 536870912, null: false
|
||||
t.index ["channel"], name: "index_solid_cable_messages_on_channel"
|
||||
t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
|
||||
t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 1) do
|
||||
ActiveRecord::Schema[8.1].define(version: 1) do
|
||||
create_table "solid_cache_entries", force: :cascade do |t|
|
||||
t.binary "key", limit: 1024, null: false
|
||||
t.binary "value", limit: 536870912, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "key_hash", limit: 8, null: false
|
||||
t.integer "byte_size", limit: 4, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.binary "key", limit: 1024, null: false
|
||||
t.integer "key_hash", limit: 8, null: false
|
||||
t.binary "value", limit: 536870912, null: false
|
||||
t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
|
||||
t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
|
||||
t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
|
||||
|
||||
BIN
db/geoip/GeoLite2-Country.mmdb
Normal file
BIN
db/geoip/GeoLite2-Country.mmdb
Normal file
Binary file not shown.
11
db/migrate/20251103225239_create_users.rb
Normal file
11
db/migrate/20251103225239_create_users.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class CreateUsers < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :users do |t|
|
||||
t.string :email_address, null: false
|
||||
t.string :password_digest, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :users, :email_address, unique: true
|
||||
end
|
||||
end
|
||||
11
db/migrate/20251103225240_create_sessions.rb
Normal file
11
db/migrate/20251103225240_create_sessions.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class CreateSessions < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :sessions do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.string :ip_address
|
||||
t.string :user_agent
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
5
db/migrate/20251103225251_add_role_to_users.rb
Normal file
5
db/migrate/20251103225251_add_role_to_users.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddRoleToUsers < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :users, :role, :integer, default: 1, null: false
|
||||
end
|
||||
end
|
||||
@@ -1,123 +1,135 @@
|
||||
ActiveRecord::Schema[7.1].define(version: 1) do
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 1) do
|
||||
create_table "solid_queue_blocked_executions", force: :cascade do |t|
|
||||
t.bigint "job_id", null: false
|
||||
t.string "queue_name", null: false
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.string "concurrency_key", null: false
|
||||
t.datetime "expires_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
|
||||
t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
|
||||
t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
|
||||
t.datetime "expires_at", null: false
|
||||
t.bigint "job_id", null: false
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.string "queue_name", null: false
|
||||
t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
|
||||
t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
|
||||
t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
|
||||
end
|
||||
|
||||
create_table "solid_queue_claimed_executions", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "job_id", null: false
|
||||
t.bigint "process_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
|
||||
t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
|
||||
t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
|
||||
t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
|
||||
end
|
||||
|
||||
create_table "solid_queue_failed_executions", force: :cascade do |t|
|
||||
t.bigint "job_id", null: false
|
||||
t.text "error"
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
|
||||
t.text "error"
|
||||
t.bigint "job_id", null: false
|
||||
t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
|
||||
end
|
||||
|
||||
create_table "solid_queue_jobs", force: :cascade do |t|
|
||||
t.string "queue_name", null: false
|
||||
t.string "class_name", null: false
|
||||
t.text "arguments"
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.string "active_job_id"
|
||||
t.datetime "scheduled_at"
|
||||
t.datetime "finished_at"
|
||||
t.text "arguments"
|
||||
t.string "class_name", null: false
|
||||
t.string "concurrency_key"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "finished_at"
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.string "queue_name", null: false
|
||||
t.datetime "scheduled_at"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
|
||||
t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
|
||||
t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
|
||||
t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
|
||||
t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
|
||||
t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
|
||||
t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
|
||||
t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
|
||||
t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
|
||||
t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
|
||||
end
|
||||
|
||||
create_table "solid_queue_pauses", force: :cascade do |t|
|
||||
t.string "queue_name", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
|
||||
t.string "queue_name", null: false
|
||||
t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
|
||||
end
|
||||
|
||||
create_table "solid_queue_processes", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "hostname"
|
||||
t.string "kind", null: false
|
||||
t.datetime "last_heartbeat_at", null: false
|
||||
t.bigint "supervisor_id"
|
||||
t.integer "pid", null: false
|
||||
t.string "hostname"
|
||||
t.text "metadata"
|
||||
t.datetime "created_at", null: false
|
||||
t.string "name", null: false
|
||||
t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
|
||||
t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
|
||||
t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
|
||||
t.integer "pid", null: false
|
||||
t.bigint "supervisor_id"
|
||||
t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
|
||||
t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
|
||||
t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
|
||||
end
|
||||
|
||||
create_table "solid_queue_ready_executions", force: :cascade do |t|
|
||||
t.bigint "job_id", null: false
|
||||
t.string "queue_name", null: false
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
|
||||
t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
|
||||
t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
|
||||
t.bigint "job_id", null: false
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.string "queue_name", null: false
|
||||
t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
|
||||
t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
|
||||
t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
|
||||
end
|
||||
|
||||
create_table "solid_queue_recurring_executions", force: :cascade do |t|
|
||||
t.bigint "job_id", null: false
|
||||
t.string "task_key", null: false
|
||||
t.datetime "run_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
|
||||
t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
|
||||
t.bigint "job_id", null: false
|
||||
t.datetime "run_at", null: false
|
||||
t.string "task_key", null: false
|
||||
t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
|
||||
t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
|
||||
end
|
||||
|
||||
create_table "solid_queue_recurring_tasks", force: :cascade do |t|
|
||||
t.string "key", null: false
|
||||
t.string "schedule", null: false
|
||||
t.string "command", limit: 2048
|
||||
t.string "class_name"
|
||||
t.text "arguments"
|
||||
t.string "queue_name"
|
||||
t.integer "priority", default: 0
|
||||
t.boolean "static", default: true, null: false
|
||||
t.text "description"
|
||||
t.string "class_name"
|
||||
t.string "command", limit: 2048
|
||||
t.datetime "created_at", null: false
|
||||
t.text "description"
|
||||
t.string "key", null: false
|
||||
t.integer "priority", default: 0
|
||||
t.string "queue_name"
|
||||
t.string "schedule", null: false
|
||||
t.boolean "static", default: true, null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
|
||||
t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
|
||||
t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
|
||||
t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
|
||||
end
|
||||
|
||||
create_table "solid_queue_scheduled_executions", force: :cascade do |t|
|
||||
t.bigint "job_id", null: false
|
||||
t.string "queue_name", null: false
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.datetime "scheduled_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
|
||||
t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
|
||||
t.bigint "job_id", null: false
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.string "queue_name", null: false
|
||||
t.datetime "scheduled_at", null: false
|
||||
t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
|
||||
t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
|
||||
end
|
||||
|
||||
create_table "solid_queue_semaphores", force: :cascade do |t|
|
||||
t.string "key", null: false
|
||||
t.integer "value", default: 1, null: false
|
||||
t.datetime "expires_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "expires_at", null: false
|
||||
t.string "key", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
|
||||
t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
|
||||
t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
|
||||
t.integer "value", default: 1, null: false
|
||||
t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
|
||||
t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
|
||||
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
||||
|
||||
21
db/schema.rb
21
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_11_03_130430) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_11_03_225251) do
|
||||
create_table "events", force: :cascade do |t|
|
||||
t.string "agent_name"
|
||||
t.string "agent_version"
|
||||
@@ -190,6 +190,25 @@ ActiveRecord::Schema[8.1].define(version: 2025_11_03_130430) do
|
||||
t.index ["updated_at", "id"], name: "idx_rules_sync"
|
||||
end
|
||||
|
||||
create_table "sessions", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "ip_address"
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "user_agent"
|
||||
t.integer "user_id", null: false
|
||||
t.index ["user_id"], name: "index_sessions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "email_address", null: false
|
||||
t.string "password_digest", null: false
|
||||
t.integer "role", default: 1, null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["email_address"], name: "index_users_on_email_address", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "events", "projects"
|
||||
add_foreign_key "events", "request_hosts"
|
||||
add_foreign_key "sessions", "users"
|
||||
end
|
||||
|
||||
67
test/controllers/passwords_controller_test.rb
Normal file
67
test/controllers/passwords_controller_test.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
require "test_helper"
|
||||
|
||||
class PasswordsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup { @user = User.take }
|
||||
|
||||
test "new" do
|
||||
get new_password_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create" do
|
||||
post passwords_path, params: { email_address: @user.email_address }
|
||||
assert_enqueued_email_with PasswordsMailer, :reset, args: [ @user ]
|
||||
assert_redirected_to new_session_path
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "reset instructions sent"
|
||||
end
|
||||
|
||||
test "create for an unknown user redirects but sends no mail" do
|
||||
post passwords_path, params: { email_address: "missing-user@example.com" }
|
||||
assert_enqueued_emails 0
|
||||
assert_redirected_to new_session_path
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "reset instructions sent"
|
||||
end
|
||||
|
||||
test "edit" do
|
||||
get edit_password_path(@user.password_reset_token)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "edit with invalid password reset token" do
|
||||
get edit_password_path("invalid token")
|
||||
assert_redirected_to new_password_path
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "reset link is invalid"
|
||||
end
|
||||
|
||||
test "update" do
|
||||
assert_changes -> { @user.reload.password_digest } do
|
||||
put password_path(@user.password_reset_token), params: { password: "new", password_confirmation: "new" }
|
||||
assert_redirected_to new_session_path
|
||||
end
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "Password has been reset"
|
||||
end
|
||||
|
||||
test "update with non matching passwords" do
|
||||
token = @user.password_reset_token
|
||||
assert_no_changes -> { @user.reload.password_digest } do
|
||||
put password_path(token), params: { password: "no", password_confirmation: "match" }
|
||||
assert_redirected_to edit_password_path(token)
|
||||
end
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "Passwords did not match"
|
||||
end
|
||||
|
||||
private
|
||||
def assert_notice(text)
|
||||
assert_select "div", /#{text}/
|
||||
end
|
||||
end
|
||||
13
test/controllers/registrations_controller_test.rb
Normal file
13
test/controllers/registrations_controller_test.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require "test_helper"
|
||||
|
||||
class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should get new" do
|
||||
get registrations_new_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get create" do
|
||||
get registrations_create_url
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
33
test/controllers/sessions_controller_test.rb
Normal file
33
test/controllers/sessions_controller_test.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require "test_helper"
|
||||
|
||||
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup { @user = User.take }
|
||||
|
||||
test "new" do
|
||||
get new_session_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create with valid credentials" do
|
||||
post session_path, params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
assert_redirected_to root_path
|
||||
assert cookies[:session_id]
|
||||
end
|
||||
|
||||
test "create with invalid credentials" do
|
||||
post session_path, params: { email_address: @user.email_address, password: "wrong" }
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
assert_nil cookies[:session_id]
|
||||
end
|
||||
|
||||
test "destroy" do
|
||||
sign_in_as(User.take)
|
||||
|
||||
delete session_path
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
assert_empty cookies[:session_id]
|
||||
end
|
||||
end
|
||||
23
test/controllers/users_controller_test.rb
Normal file
23
test/controllers/users_controller_test.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require "test_helper"
|
||||
|
||||
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should get index" do
|
||||
get users_index_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get show" do
|
||||
get users_show_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get edit" do
|
||||
get users_edit_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get update" do
|
||||
get users_update_url
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
9
test/fixtures/users.yml
vendored
Normal file
9
test/fixtures/users.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<% password_digest = BCrypt::Password.create("password") %>
|
||||
|
||||
one:
|
||||
email_address: one@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
|
||||
two:
|
||||
email_address: two@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
7
test/mailers/previews/passwords_mailer_preview.rb
Normal file
7
test/mailers/previews/passwords_mailer_preview.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer
|
||||
class PasswordsMailerPreview < ActionMailer::Preview
|
||||
# Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset
|
||||
def reset
|
||||
PasswordsMailer.reset(User.take)
|
||||
end
|
||||
end
|
||||
8
test/models/user_test.rb
Normal file
8
test/models/user_test.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
require "test_helper"
|
||||
|
||||
class UserTest < ActiveSupport::TestCase
|
||||
test "downcases and strips email_address" do
|
||||
user = User.new(email_address: " DOWNCASED@EXAMPLE.COM ")
|
||||
assert_equal("downcased@example.com", user.email_address)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
ENV["RAILS_ENV"] ||= "test"
|
||||
require_relative "../config/environment"
|
||||
require "rails/test_help"
|
||||
require_relative "test_helpers/session_test_helper"
|
||||
|
||||
module ActiveSupport
|
||||
class TestCase
|
||||
|
||||
19
test/test_helpers/session_test_helper.rb
Normal file
19
test/test_helpers/session_test_helper.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module SessionTestHelper
|
||||
def sign_in_as(user)
|
||||
Current.session = user.sessions.create!
|
||||
|
||||
ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
|
||||
cookie_jar.signed[:session_id] = Current.session.id
|
||||
cookies["session_id"] = cookie_jar[:session_id]
|
||||
end
|
||||
end
|
||||
|
||||
def sign_out
|
||||
Current.session&.destroy!
|
||||
cookies.delete("session_id")
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
||||
include SessionTestHelper
|
||||
end
|
||||
Reference in New Issue
Block a user