User registation working. Sidebar built. Dashboard built. TOTP enable works - TOTP login works
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-10-23 18:07:27 +11:00
parent 56f7dd7b3c
commit 256cbe3a48
26 changed files with 1278 additions and 119 deletions

View File

@@ -0,0 +1,12 @@
class DashboardController < ApplicationController
def index
# First run: redirect to signup
if User.count.zero?
redirect_to signup_path
return
end
# User must be authenticated
@user = Current.session.user
end
end

View File

@@ -0,0 +1,45 @@
class ProfilesController < ApplicationController
def show
@user = Current.session.user
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
end
def update
@user = Current.session.user
if params[:user][:password].present?
# Updating password - requires current password
unless @user.authenticate(params[:user][:current_password])
@user.errors.add(:current_password, "is incorrect")
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
render :show, status: :unprocessable_entity
return
end
if @user.update(password_params)
redirect_to profile_path, notice: "Password updated successfully."
else
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
render :show, status: :unprocessable_entity
end
else
# Updating email
if @user.update(email_params)
redirect_to profile_path, notice: "Email updated successfully."
else
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
render :show, status: :unprocessable_entity
end
end
end
private
def email_params
params.require(:user).permit(:email_address)
end
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
end

View File

@@ -1,21 +1,47 @@
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." }
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." }
def new
# Redirect to signup if this is first run
redirect_to signup_path if User.count.zero?
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."
user = User.authenticate_by(params.permit(:email_address, :password))
if user.nil?
redirect_to signin_path, alert: "Invalid email address or password."
return
end
# Check if user is active
unless user.status == "active"
redirect_to signin_path, alert: "Your account is not active. Please contact an administrator."
return
end
# Check if TOTP is required
if user.totp_enabled?
# TODO: Implement TOTP verification flow
# For now, reject login if TOTP is enabled
redirect_to signin_path, alert: "Two-factor authentication is enabled but not yet implemented. Please contact an administrator."
return
end
# Sign in successful
start_new_session_for user
redirect_to after_authentication_url, notice: "Signed in successfully."
end
def destroy
terminate_session
redirect_to new_session_path, status: :see_other
redirect_to signin_path, status: :see_other, notice: "Signed out successfully."
end
def destroy_other
session = Current.session.user.sessions.find(params[:id])
session.destroy
redirect_to profile_path, notice: "Session revoked successfully."
end
end

View File

@@ -0,0 +1,84 @@
class TotpController < ApplicationController
before_action :set_user
before_action :redirect_if_totp_enabled, only: [:new, :create]
before_action :require_totp_enabled, only: [:backup_codes, :verify_password, :destroy]
# GET /totp/new - Show QR code to set up TOTP
def new
# Generate TOTP secret but don't save yet
@totp_secret = ROTP::Base32.random
@provisioning_uri = ROTP::TOTP.new(@totp_secret, issuer: "Clinch").provisioning_uri(@user.email_address)
# Generate QR code
require "rqrcode"
@qr_code = RQRCode::QRCode.new(@provisioning_uri)
end
# POST /totp - Verify TOTP code and enable 2FA
def create
totp_secret = params[:totp_secret]
code = params[:code]
# Verify the code works
totp = ROTP::TOTP.new(totp_secret)
if totp.verify(code, drift_behind: 30, drift_ahead: 30)
# Save the secret and generate backup codes
@user.totp_secret = totp_secret
@user.backup_codes = generate_backup_codes
@user.save!
# Redirect to backup codes page with success message
redirect_to backup_codes_totp_path, notice: "Two-factor authentication has been enabled successfully! Save these backup codes now."
else
redirect_to new_totp_path, alert: "Invalid verification code. Please try again."
end
end
# GET /totp/backup_codes - Show backup codes (requires password)
def backup_codes
# This will be shown after password verification
@backup_codes = @user.parsed_backup_codes
end
# POST /totp/verify_password - Verify password before showing backup codes
def verify_password
if @user.authenticate(params[:password])
redirect_to backup_codes_totp_path
else
redirect_to profile_path, alert: "Incorrect password."
end
end
# DELETE /totp - Disable TOTP (requires password)
def destroy
unless @user.authenticate(params[:password])
redirect_to profile_path, alert: "Incorrect password. Could not disable 2FA."
return
end
@user.disable_totp!
redirect_to profile_path, notice: "Two-factor authentication has been disabled."
end
private
def set_user
@user = Current.session.user
end
def redirect_if_totp_enabled
if @user.totp_enabled?
redirect_to profile_path, alert: "Two-factor authentication is already enabled."
end
end
def require_totp_enabled
unless @user.totp_enabled?
redirect_to profile_path, alert: "Two-factor authentication is not enabled."
end
end
def generate_backup_codes
Array.new(10) { SecureRandom.alphanumeric(8).upcase }.to_json
end
end

View File

@@ -0,0 +1,36 @@
class UsersController < ApplicationController
allow_unauthenticated_access only: %i[ new create ]
before_action :ensure_first_run, only: %i[ new create ]
def new
@user = User.new
end
def create
@user = User.new(user_params)
# First user becomes admin automatically
@user.admin = true if User.count.zero?
@user.status = "active"
if @user.save
start_new_session_for @user
redirect_to root_path, notice: "Welcome to Clinch! Your account has been created."
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email_address, :password, :password_confirmation)
end
def ensure_first_run
# Only allow signup if there are no users (first-run scenario)
if User.exists?
redirect_to signin_path, alert: "Registration is closed. Please sign in."
end
end
end