From 20b62e7312d69fc912d6d4006a44d57ac5b5097c Mon Sep 17 00:00:00 2001 From: Some Date: Tue, 19 Aug 2025 07:24:18 +0200 Subject: [PATCH] Added users support --- README.md | 118 +++++++- app/controllers/application_controller.rb | 41 ++- app/controllers/reservations_controller.rb | 5 + app/controllers/sessions_controller.rb | 25 ++ app/models/company.rb | 1 + app/models/user.rb | 23 ++ app/views/layouts/application.html.erb | 9 + app/views/reservations/_form.html.erb | 2 +- app/views/reservations/index.html.erb | 10 +- app/views/sessions/new.html.erb | 24 ++ config/locales/bs.yml | 13 + config/locales/en.yml | 13 + config/routes.rb | 6 +- db/migrate/20250731113948_create_users.rb | 14 + .../20250731114403_add_email_to_users.rb | 6 + db/schema.rb | 15 +- db/seeds.rb | 31 ++ lib/tasks/users.rake | 271 ++++++++++++++++++ 18 files changed, 592 insertions(+), 35 deletions(-) create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/models/user.rb create mode 100644 app/views/sessions/new.html.erb create mode 100644 db/migrate/20250731113948_create_users.rb create mode 100644 db/migrate/20250731114403_add_email_to_users.rb create mode 100644 lib/tasks/users.rake diff --git a/README.md b/README.md index 7db80e4..3fda671 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,116 @@ -# README +# ZSTerminator - -This README would normally document whatever steps are necessary to get the -application up and running. -Things you may want to cover: +## Setup -* Ruby version +* Ruby version: 3.2.4 +* Rails version: 7.1.5+ +* Database: SQLite3 -* System dependencies +## Getting Started -* Configuration +1. Install dependencies: + ```bash + bundle install + ``` -* Database creation +2. Setup database: + ```bash + rails db:create + rails db:migrate + rails db:seed + ``` -* Database initialization +3. Start the server: + ```bash + rails server + ``` -* How to run the test suite +4. Visit http://localhost:3000 and login with: + - Username: `admin` + - Password: `password123` -* Services (job queues, cache servers, search engines, etc.) +## User Management (Rake Tasks) -* Deployment instructions +This project includes Rake tasks for managing users via command line: -* ... +### Available Commands + +```bash +# List all users +rails users:list + +# Show user details +rails users:show[username] + +# Create a new user +rails users:create[username,email,password,company_id] + +# Change user password +rails users:change_password[username,new_password] + +# Delete a user +rails users:delete[username] + +# Clean up test users (users with 'test' in username/email) +rails users:cleanup_test_users +``` + +### Usage Examples + +```bash +# Create a new user (company_id is optional, uses first company if blank) +rails "users:create[john,john@example.com,securepass123]" + +# Change password +rails "users:change_password[john,newpassword456]" + +# Show user information +rails users:show[john] + +# Delete user (with confirmation) +rails users:delete[john] + +# List all users in a formatted table +rails users:list +``` + +### Automated Testing + +**Run complete CRUD test suite:** +```bash +# Single command to test all user operations automatically +rails users:test_crud +``` + +This automated test will: +1. **Create** test users (dodavanje) +2. **List** created users +3. **Change** password (mijenjanje sifre) +4. **Show** user details +5. **Delete** user (brisanje) +6. **Clean up** all test data + +### Manual Testing Workflow + +1. **Create test users:** + ```bash + rails "users:create[testuser1,test1@example.com,testpass123]" + rails "users:create[testuser2,test2@example.com,testpass456]" + ``` + +2. **Test your application** with the created users + +3. **Clean up test data:** + ```bash + # This will find and delete all users with 'test' in username or email + rails users:cleanup_test_users + ``` + +### Notes + +- All user passwords are encrypted using bcrypt +- Users must belong to a company +- Username and email must be unique +- Minimum password length is 6 characters +- The cleanup task only removes users with 'test' in their username or email for safety diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7d02033..7a6b9e6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base before_action :set_locale + before_action :require_login private @@ -8,27 +9,37 @@ class ApplicationController < ActionController::Base session[:locale] = I18n.locale end - # Optional: Make locale persist across requests via URL helpers def default_url_options { locale: I18n.locale } end - def set_company - # This should be handled by your authentication system - # But for now, we'll use a placeholder - company_id = session[:company_id] - - unless company_id && Company.exists?(company_id) - # If no company in session or it doesn't exist, use the first company - company_id = Company.first&.id - session[:company_id] = company_id - end - - @company = Company.find(company_id) if company_id + def current_user + @current_user ||= User.find(session[:user_id]) if session[:user_id] end - + + def logged_in? + !!current_user + end + + def require_login + return if logged_in? + + flash[:alert] = t('sessions.login_required') + redirect_to login_path + end + + def set_company + return unless logged_in? + + @company = current_user.company + + return if @company + + redirect_to companies_path, alert: 'No company found. Please create a company first.' + end + def current_company @company end - helper_method :current_company + helper_method :current_user, :logged_in?, :current_company end diff --git a/app/controllers/reservations_controller.rb b/app/controllers/reservations_controller.rb index ba47cdf..9751a1a 100644 --- a/app/controllers/reservations_controller.rb +++ b/app/controllers/reservations_controller.rb @@ -219,5 +219,10 @@ class ReservationsController < ApplicationController end @company = Company.includes(:teams).find(company_id) if company_id + + unless @company + redirect_to companies_path, alert: 'No company found. Please create a company first.' + return + end end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..d5c03a0 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,25 @@ +class SessionsController < ApplicationController + skip_before_action :require_login, only: %i[new create] + + def new + end + + def create + user = User.find_by_login(params[:login]) + + if user&.authenticate(params[:password]) + session[:user_id] = user.id + session[:company_id] = user.company_id + redirect_to root_path, notice: t('sessions.login_successful') + else + flash.now[:alert] = t('sessions.invalid_credentials') + render :new, status: :unprocessable_entity + end + end + + def destroy + session[:user_id] = nil + session[:company_id] = nil + redirect_to login_path, notice: t('sessions.logout_successful') + end +end diff --git a/app/models/company.rb b/app/models/company.rb index 2cc0110..dfbb100 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -2,4 +2,5 @@ class Company < ApplicationRecord has_many :customers, dependent: :destroy has_many :reservations, dependent: :destroy has_many :teams, dependent: :destroy + has_many :users, dependent: :destroy end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..6df3b84 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,23 @@ +class User < ApplicationRecord + has_secure_password + + belongs_to :company + + validates :username, presence: true, uniqueness: { case_sensitive: false } + validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :password, length: { minimum: 6 }, if: -> { new_record? || password.present? } + validates :company_id, presence: true + + before_save :downcase_username_and_email + + def self.find_by_login(login) + find_by(username: login.downcase) || find_by(email: login.downcase) + end + + private + + def downcase_username_and_email + self.username = username.downcase.strip if username.present? + self.email = email.downcase.strip if email.present? + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 3ebfd04..72a0e24 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -18,6 +18,15 @@ + <% if logged_in? %> +
+ <%= current_user.username %> + | + <%= link_to t('sessions.logout_button'), logout_path, method: :delete, + class: "text-blue-600 hover:text-blue-800" %> +
+ <% end %> +
<%= yield %>
diff --git a/app/views/reservations/_form.html.erb b/app/views/reservations/_form.html.erb index ee62d77..1e5ce6c 100644 --- a/app/views/reservations/_form.html.erb +++ b/app/views/reservations/_form.html.erb @@ -62,7 +62,7 @@
<%= form.label :team_id, t('reservations.form.team') %> <%= form.collection_select :team_id, - @company.teams, + @company&.teams || [], :id, :name, { prompt: t('reservations.form.select_team') }, diff --git a/app/views/reservations/index.html.erb b/app/views/reservations/index.html.erb index b89d592..142c2cc 100644 --- a/app/views/reservations/index.html.erb +++ b/app/views/reservations/index.html.erb @@ -28,10 +28,12 @@
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..dfb3f35 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,24 @@ +
+

<%= t('sessions.login') %>

+ + <%= form_with url: login_path, method: :post, local: true, class: "space-y-4" do |form| %> +
+ <%= form.label :login, t('sessions.username_or_email'), class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.text_field :login, required: true, + class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent", + placeholder: t('sessions.username_or_email_placeholder') %> +
+ +
+ <%= form.label :password, t('sessions.password'), class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.password_field :password, required: true, + class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent", + placeholder: t('sessions.password_placeholder') %> +
+ +
+ <%= form.submit t('sessions.login_button'), + class: "w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-200" %> +
+ <% end %> +
\ No newline at end of file diff --git a/config/locales/bs.yml b/config/locales/bs.yml index 5ea51a5..cf7f187 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -131,4 +131,17 @@ bs: city: "Grad:" entity: "Entitet:" country: "Država:" + + sessions: + login: "Prijava" + username_or_email: "Korisničko ime ili email" + username_or_email_placeholder: "Unesite korisničko ime ili email" + password: "Lozinka" + password_placeholder: "Unesite lozinku" + login_button: "Prijaviť se" + logout_button: "Odjavi se" + login_successful: "Uspješno ste se prijavili!" + logout_successful: "Uspješno ste se odjavili!" + invalid_credentials: "Neispravno korisničko ime/email ili lozinka" + login_required: "Molimo prijavite se da biste pristupili ovoj stranici" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index a521798..998b46c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -167,3 +167,16 @@ en: city: "City:" entity: "Entity:" country: "Country:" + + sessions: + login: "Login" + username_or_email: "Username or Email" + username_or_email_placeholder: "Enter username or email" + password: "Password" + password_placeholder: "Enter password" + login_button: "Log In" + logout_button: "Log Out" + login_successful: "Successfully logged in!" + logout_successful: "Successfully logged out!" + invalid_credentials: "Invalid username/email or password" + login_required: "Please log in to access this page" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 69f7424..20d6f57 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,8 @@ Rails.application.routes.draw do + get '/login', to: 'sessions#new' + post '/login', to: 'sessions#create' + delete '/logout', to: 'sessions#destroy' + root "reservations#index" resources :customers, param: :composite_key do get :search, on: :collection @@ -12,7 +16,7 @@ Rails.application.routes.draw do # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check - # config/routes.rb + # config/routes.rb get "/service-worker.js" => "service_worker#service_worker" get "/manifest.json" => "service_worker#manifest" # Defines the root path route ("/") diff --git a/db/migrate/20250731113948_create_users.rb b/db/migrate/20250731113948_create_users.rb new file mode 100644 index 0000000..f765e1b --- /dev/null +++ b/db/migrate/20250731113948_create_users.rb @@ -0,0 +1,14 @@ +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.string :username + t.string :email + t.string :password_digest + t.references :company, null: false, foreign_key: true + + t.timestamps + end + add_index :users, :username, unique: true + add_index :users, :email, unique: true + end +end diff --git a/db/migrate/20250731114403_add_email_to_users.rb b/db/migrate/20250731114403_add_email_to_users.rb new file mode 100644 index 0000000..3b26e34 --- /dev/null +++ b/db/migrate/20250731114403_add_email_to_users.rb @@ -0,0 +1,6 @@ +class AddEmailToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :email, :string + add_index :users, :email, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 58fb5bc..296a5eb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_07_04_092438) do +ActiveRecord::Schema[7.1].define(version: 2025_07_31_114403) do create_table "companies", force: :cascade do |t| t.string "name" t.string "id_number" @@ -66,8 +66,21 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_04_092438) do t.index ["company_id"], name: "index_teams_on_company_id" end + create_table "users", force: :cascade do |t| + t.string "username" + t.string "password_digest" + t.integer "company_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "email" + t.index ["company_id"], name: "index_users_on_company_id" + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["username"], name: "index_users_on_username", unique: true + end + add_foreign_key "reservations", "companies" add_foreign_key "reservations", "customers", column: ["customer_first_name", "customer_surname", "customer_original_phone"], primary_key: ["first_name", "surname", "original_phone"] add_foreign_key "reservations", "teams" add_foreign_key "teams", "companies" + add_foreign_key "users", "companies" end diff --git a/db/seeds.rb b/db/seeds.rb index 4fbd6ed..f9a764f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,3 +7,34 @@ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| # MovieGenre.find_or_create_by!(name: genre_name) # end +# Create a default company if none exists +default_company = Company.find_or_create_by!(name: 'Default Company') do |company| + company.id_number = 'COMP001' + company.vat_number = 'VAT001' + company.address_line_one = '123 Main Street' + company.city = 'Default City' + company.country = 'Default Country' +end + +# Create default teams for the company +teams_data = [ + { name: 'Team Alpha' }, + { name: 'Team Beta' }, + { name: 'Team Gamma' } +] + +teams_data.each do |team_attrs| + Team.find_or_create_by!(name: team_attrs[:name], company: default_company) +end + +puts "Seeded default company: #{default_company.name}" +puts "Seeded #{default_company.teams.count} teams" + +# Create default user for the company +default_user = User.find_or_create_by!(username: 'admin', company: default_company) do |user| + user.email = 'admin@company.ba' + user.password = 'password123' + user.password_confirmation = 'password123' +end + +puts "Seeded default user: #{default_user.username} (#{default_user.email})" diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake new file mode 100644 index 0000000..c319359 --- /dev/null +++ b/lib/tasks/users.rake @@ -0,0 +1,271 @@ +namespace :users do + desc "Create a new user" + task :create, %i[username email password company_id] => :environment do |_t, args| + username = args[:username] || ask("Username: ") + email = args[:email] || ask("Email: ") + password = args[:password] || ask("Password: ") { |q| q.echo = "*" } + company_id = find_or_validate_company(args[:company_id]) + + user = User.new( + username: username, + email: email, + password: password, + password_confirmation: password, + company_id: company_id + ) + + if user.save + puts "User created successfully!" + puts " Username: #{user.username}" + puts " Email: #{user.email}" + puts " Company: #{user.company.name}" + else + puts "Failed to create user:" + user.errors.full_messages.each { |msg| puts " - #{msg}" } + exit 1 + end + end + + desc "Change user password" + task :change_password, %i[username new_password] => :environment do |_t, args| + username = args[:username] || ask("Username: ") + new_password = args[:new_password] || ask("New password: ") { |q| q.echo = "*" } + + user = User.find_by(username: username) + unless user + puts "User '#{username}' not found." + exit 1 + end + + user.password = new_password + user.password_confirmation = new_password + + if user.save + puts "Password changed successfully for user '#{username}'" + else + puts "Failed to change password:" + user.errors.full_messages.each { |msg| puts " - #{msg}" } + exit 1 + end + end + + desc "Delete a user" + task :delete, %i[username] => :environment do |_t, args| + username = args[:username] || ask("Username to delete: ") + + user = User.find_by(username: username) + unless user + puts "User '#{username}' not found." + exit 1 + end + + puts "User details:" + puts " Username: #{user.username}" + puts " Email: #{user.email}" + puts " Company: #{user.company.name}" + + confirm = ask("Are you sure you want to delete this user? (yes/no): ") + unless confirm.downcase == 'yes' + puts "User deletion cancelled." + exit 0 + end + + if user.destroy + puts "User '#{username}' deleted successfully." + else + puts "Failed to delete user:" + user.errors.full_messages.each { |msg| puts " - #{msg}" } + exit 1 + end + end + + desc "List all users" + task list: :environment do + users = User.includes(:company).order(:username) + + if users.empty? + puts "No users found." + exit 0 + end + + puts "Users List:" + puts "=" * 80 + printf "%-20s %-30s %-20s %s\n", "USERNAME", "EMAIL", "COMPANY", "CREATED" + puts "-" * 80 + + users.each do |user| + printf "%-20s %-30s %-20s %s\n", + user.username, + user.email, + user.company.name, + user.created_at.strftime("%Y-%m-%d") + end + + puts "-" * 80 + puts "Total: #{users.count} users" + end + + desc "Run automated CRUD tests for user management" + task test_crud: :environment do + puts "=" * 60 + puts "AUTOMATED USER CRUD TEST" + puts "=" * 60 + + # Test 1: Create users + puts "\n1. TESTING USER CREATION (dodavanje):" + puts "-" * 40 + + test_users = [ + { username: 'testuser1', email: 'test1@example.com', password: 'testpass123' }, + { username: 'testuser2', email: 'test2@example.com', password: 'testpass456' } + ] + + test_users.each do |user_data| + company_id = find_or_validate_company(nil) + user = User.new( + username: user_data[:username], + email: user_data[:email], + password: user_data[:password], + password_confirmation: user_data[:password], + company_id: company_id + ) + + if user.save + puts "✓ Created user: #{user.username} (#{user.email})" + else + puts "✗ Failed to create #{user_data[:username]}: #{user.errors.full_messages.join(', ')}" + end + end + + # Test 2: List users + puts "\n2. TESTING USER LISTING:" + puts "-" * 40 + users = User.where("username LIKE ?", "%test%").order(:username) + users.each do |user| + puts "✓ Found user: #{user.username} (#{user.email}) - Company: #{user.company.name}" + end + + # Test 3: Change password (mijenjanje sifre) + puts "\n3. TESTING PASSWORD CHANGE (mijenjanje sifre):" + puts "-" * 40 + test_user = User.find_by(username: 'testuser1') + if test_user + test_user.password = 'newpassword789' + test_user.password_confirmation = 'newpassword789' + if test_user.save + puts "✓ Password changed for: #{test_user.username}" + else + puts "✗ Failed to change password: #{test_user.errors.full_messages.join(', ')}" + end + end + + # Test 4: Show user details + puts "\n4. TESTING USER DETAILS:" + puts "-" * 40 + if test_user + puts "✓ User: #{test_user.username}" + puts " Email: #{test_user.email}" + puts " Company: #{test_user.company.name}" + puts " Created: #{test_user.created_at.strftime('%Y-%m-%d %H:%M:%S')}" + end + + # Test 5: Delete user (brisanje) + puts "\n5. TESTING USER DELETION (brisanje):" + puts "-" * 40 + delete_user = User.find_by(username: 'testuser2') + if delete_user + username = delete_user.username + if delete_user.destroy + puts "✓ Deleted user: #{username}" + else + puts "✗ Failed to delete user: #{delete_user.errors.full_messages.join(', ')}" + end + end + + # Test 6: Final cleanup + puts "\n6. CLEANUP:" + puts "-" * 40 + remaining_test_users = User.where("username LIKE ? OR email LIKE ?", "%test%", "%test%") + deleted_count = remaining_test_users.count + remaining_test_users.destroy_all + puts "✓ Cleaned up #{deleted_count} remaining test users" + + puts "\n" + ("=" * 60) + puts "CRUD TEST COMPLETED SUCCESSFULLY!" + puts "All operations (dodavanje, mijenjanje sifre, brisanje) tested." + puts "=" * 60 + end + + desc "Clean up test users (users with 'test' in username or email)" + task cleanup_test_users: :environment do + test_users = User.where("username LIKE ? OR email LIKE ?", "%test%", "%test%") + + if test_users.empty? + puts "No test users found to clean up." + return + end + + puts "Found #{test_users.count} test users to delete:" + test_users.each do |user| + puts " - #{user.username} (#{user.email})" + end + + confirm = ask("Delete all test users? (yes/no): ") + if confirm.downcase == 'yes' + deleted_count = test_users.count + test_users.destroy_all + puts "Deleted #{deleted_count} test users successfully." + else + puts "Cleanup cancelled." + end + end + + desc "Show user details" + task :show, %i[username] => :environment do |_t, args| + username = args[:username] || ask("Username: ") + + user = User.find_by(username: username) + unless user + puts "User '#{username}' not found." + exit 1 + end + + puts "User Details:" + puts " Username: #{user.username}" + puts " Email: #{user.email}" + puts " Company: #{user.company.name} (ID: #{user.company.id})" + puts " Created: #{user.created_at.strftime('%Y-%m-%d %H:%M:%S')}" + puts " Updated: #{user.updated_at.strftime('%Y-%m-%d %H:%M:%S')}" + end + + private + + def find_or_validate_company(company_id) + if company_id.blank? + company = Company.first + unless company + puts "No companies found. Please create a company first." + exit 1 + end + puts "Using company: #{company.name} (ID: #{company.id})" + company.id + else + company = Company.find_by(id: company_id) + unless company + puts "Company with ID #{company_id} not found." + exit 1 + end + company_id + end + end + + def ask(prompt, &_block) + require 'io/console' + print prompt + if block_given? + yield.call + else + $stdin.gets.chomp + end + end +end