Much work.
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user