diff --git a/Gemfile b/Gemfile index a97eac7..7e34274 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,7 @@ gem "stimulus-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" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] @@ -50,6 +50,11 @@ gem "thruster", require: false group :development do # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console" + + gem "rubocop-rails", require: false + gem "rubocop-performance", require: false + gem "rubocop-rspec", require: false + gem "rubocop-rspec_rails", require: false end group :development, :test do @@ -68,12 +73,3 @@ group :development, :test do gem "rspec-rails", "~> 8.0.0" end -group :development do - # Use console on exceptions pages [https://github.com/rails/web-console] - gem "web-console" - - gem "rubocop-rails", require: false - gem "rubocop-performance", require: false - gem "rubocop-rspec", require: false - gem "rubocop-rspec_rails", require: false -end diff --git a/Gemfile.lock b/Gemfile.lock index ba4e61f..6ca3d03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM uri (>= 0.13.1) ast (2.4.3) base64 (0.3.0) + bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) bcrypt_pbkdf (1.1.1-arm64-darwin) bcrypt_pbkdf (1.1.1-x86_64-darwin) @@ -401,6 +402,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap brakeman debug @@ -411,13 +413,10 @@ DEPENDENCIES puma (>= 5.0) rails (~> 8.0.2) rails_db - rspec-rails (~> 8.0.0) - rubocop rubocop-performance rubocop-rails - rubocop-rails-omakase rubocop-rspec rubocop-rspec_rails diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db2..a26e8b4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,23 @@ class ApplicationController < ActionController::Base - # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. - allow_browser versions: :modern + helper_method :current_user, :logged_in? + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] + end + + def logged_in? + current_user.present? + end + + private + + def require_admin + unless current_user&.role == "admin" + flash[:alert] = "You do not have permission to do that." + redirect_to root_path + end + end + + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern end diff --git a/app/controllers/artists_controller.rb b/app/controllers/artists_controller.rb index b1e4610..a0c2e29 100644 --- a/app/controllers/artists_controller.rb +++ b/app/controllers/artists_controller.rb @@ -1,6 +1,6 @@ class ArtistsController < ApplicationController def index - @artists = Artist.all + @artists = Artist.alphabetical end def show @@ -14,7 +14,7 @@ def new def create @artist = Artist.new(artist_params) if @artist.save - redirect_to @artist, notice: "Artist Created" + redirect_to new_track_path(artist_id: @artist.id), notice: "Artist Created" else render :new, status: :unprocessable_entity end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..095eeb1 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,22 @@ +class SessionsController < ApplicationController + def new + # renders the login form + end + + def create + user = User.find_by(username: params[:username]) + if user&.authenticate(params[:password]) + session[:user_id] = user.id + session[:role] = user.role + redirect_to root_path, notice: "Logged in as #{user.username}" + else + flash.now[:alert] = "Invalid username or password" + render :new, status: :unprocessable_entity + end + end + + def destroy + reset_session + redirect_to root_path, notice: "Logged out" + end +end diff --git a/app/controllers/tracks_controller.rb b/app/controllers/tracks_controller.rb index 77d3e67..8e6eeb7 100644 --- a/app/controllers/tracks_controller.rb +++ b/app/controllers/tracks_controller.rb @@ -1,4 +1,5 @@ class TracksController < ApplicationController + before_action :require_admin, only: [ :destroy ] def index @tracks = Track.all end @@ -9,6 +10,10 @@ def show def new @track = Track.new + @artists = Artist.alphabetical + if params[:artist_id].present? + @track.artist_id = params[:artist_id] + end end def create diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..309f8b2 --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/models/artist.rb b/app/models/artist.rb index 8083841..563c36b 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -3,10 +3,13 @@ class Artist < ApplicationRecord validates :name, presence: true + # alpha scope + scope :alphabetical, -> { order("LOWER(name) ASC") } + # logic for sample connections, artist + method + each loop def sampled_by Track.joins(:artist, :samples_used) - .where(samples: { source_track_id: tracks.select(:id) }) - .select("tracks.title AS track_title, artists.name AS artist_name") + .where(samples: { source_track_id: tracks.select(:id) }) + .select("tracks.title AS track_title, artists.name AS artist_name") end end diff --git a/app/models/track.rb b/app/models/track.rb index a617471..07954bc 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -1,5 +1,5 @@ class Track < ApplicationRecord - belongs_to :user, optional: true + belongs_to :user belongs_to :artist has_many :comments, dependent: :destroy diff --git a/app/models/user.rb b/app/models/user.rb index 1f9e77f..ac6aa84 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,8 @@ class User < ApplicationRecord has_many :comments # TODO gameplan user.delete, what happens to their entries and comments. + has_secure_password + validates :email, presence: true, uniqueness: true validates :username, presence: true, uniqueness: true diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 27151dc..d5d1cd2 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -22,7 +22,22 @@ <%= javascript_importmap_tags %> - - <%= yield %> - + + <% if flash[:notice] %> +

<%= flash[:notice] %>

+ <% end %> + + <% if flash[:alert] %> +

<%= flash[:alert] %>

+ <% end %> + + <% if logged_in? %> +

Logged in as <%= current_user.username %>

+ <%= button_to "Logout", logout_path, method: :delete %> + <% else %> + <%= link_to "Login", login_path %> + <% end %> + + <%= yield %> + diff --git a/app/views/sessions/create.html.erb b/app/views/sessions/create.html.erb new file mode 100644 index 0000000..c251174 --- /dev/null +++ b/app/views/sessions/create.html.erb @@ -0,0 +1,2 @@ +

Sessions#create

+

Find me in app/views/sessions/create.html.erb

diff --git a/app/views/sessions/destroy.html.erb b/app/views/sessions/destroy.html.erb new file mode 100644 index 0000000..d75237d --- /dev/null +++ b/app/views/sessions/destroy.html.erb @@ -0,0 +1,2 @@ +

Sessions#destroy

+

Find me in app/views/sessions/destroy.html.erb

diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..0fa8185 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,17 @@ +

Login

+ +<%= form_with url: login_path, local: true do |form| %> +
+ <%= form.label :username %>
+ <%= form.text_field :username %> +
+ +
+ <%= form.label :password %>
+ <%= form.password_field :password %> +
+ +
+ <%= form.submit "Login" %> +
+<% end %> diff --git a/app/views/tracks/_form.html.erb b/app/views/tracks/_form.html.erb index 29ac095..43fec37 100644 --- a/app/views/tracks/_form.html.erb +++ b/app/views/tracks/_form.html.erb @@ -17,7 +17,7 @@
<%= form.label :artist_id, "Artist" %>
- <%= form.collection_select :artist_id, Artist.all, :id, :name, prompt: "Choose an artist" %> + <%= form.collection_select :artist_id, @artists, :id, :name, prompt: "Choose an artist" %>

<%= link_to "Add New Artist", new_artist_path %> diff --git a/app/views/tracks/show.html.erb b/app/views/tracks/show.html.erb index 534c482..0b15a5d 100644 --- a/app/views/tracks/show.html.erb +++ b/app/views/tracks/show.html.erb @@ -11,7 +11,7 @@

<% else %> @@ -24,7 +24,7 @@ <% else %> diff --git a/config/routes.rb b/config/routes.rb index b9c90f9..06d2d06 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,18 +1,17 @@ Rails.application.routes.draw do - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html resources :artists, only: [ :index, :show, :new, :create ] resources :tracks, only: [ :index, :show, :new, :create, :edit, :update, :destroy ] get "/dashboard", to: "dashboard#index" - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # 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 + # session routes + get "login", to: "sessions#new" + post "login", to: "sessions#create" + delete "logout", to: "sessions#destroy" - # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) - # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest - # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + # health check + get "up" => "rails/health#show", as: :rails_health_check - # Defines the root path route ("/") - # root "posts#index" + # ("/") makes root dashboard + root "dashboard#index" end diff --git a/db/migrate/20251007212920_add_password_digest_to_users.rb b/db/migrate/20251007212920_add_password_digest_to_users.rb new file mode 100644 index 0000000..81c7c9e --- /dev/null +++ b/db/migrate/20251007212920_add_password_digest_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordDigestToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :password_digest, :string + end +end diff --git a/db/migrate/20251007214251_add_role_to_users.rb b/db/migrate/20251007214251_add_role_to_users.rb new file mode 100644 index 0000000..936b670 --- /dev/null +++ b/db/migrate/20251007214251_add_role_to_users.rb @@ -0,0 +1,5 @@ +class AddRoleToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :role, :string, null: false, default: "user" + end +end diff --git a/db/schema.rb b/db/schema.rb index f728641..6b9d551 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[8.0].define(version: 2025_08_12_165319) do +ActiveRecord::Schema[8.0].define(version: 2025_10_07_214251) do create_table "artists", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false @@ -67,6 +67,8 @@ t.string "username", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "password_digest" + t.string "role", default: "user", null: false t.index ["email"], name: "index_users_on_email", unique: true t.index ["username"], name: "index_users_on_username", unique: true end diff --git a/db/seeds.rb b/db/seeds.rb index 7d97a5f..cde7dee 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,22 +7,30 @@ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| # MovieGenre.find_or_create_by!(name: genre_name) # end - # Clear existing Data -SampleSegment.delete_all -Sample.delete_all -Comment.delete_all -Track.delete_all -Artist.delete_all -User.delete_all +[ SampleSegment, Sample, Comment, Track, Artist, User ].each do |model| + model.destroy_all + ActiveRecord::Base.connection.reset_sequence!(model.table_name, model.primary_key) +end -# create user -user_twhite = User.create!( +# create users +user_admin = User.create!( email: "Twhite@mail.com", - username: "Twhite" + username: "Twhite", + password: "password123", + password_confirmation: "password123", + role: "admin" +) + +user_regular = User.create!( + email: "user@mail.com", + username: "RegularJoe", + password: "userpassword", + password_confirmation: "userpassword", + role: "user" ) -# create artist +# create artists artist_bob_james = Artist.create!(name: "Bob James") artist_slick_rick = Artist.create!(name: "Slick Rick") artist_ghostface = Artist.create!(name: "Ghostface Killah") @@ -33,21 +41,21 @@ year: 1974, bpm: 99, artist: artist_bob_james, - user: user_twhite + user: user_admin ) track_childrens_story = Track.create!( title: "Children’s Story", year: 1988, bpm: 103, artist: artist_slick_rick, - user: user_twhite + user: user_admin ) track_daytona_500 = Track.create!( title: "Daytona 500", year: 1996, bpm: 98, artist: artist_ghostface, - user: user_twhite + user: user_admin ) # create samples @@ -67,21 +75,21 @@ # create comments Comment.create!( body: "did rza produce daytona 500", - user: user_twhite, + user: user_admin, track: track_daytona_500 ) Comment.create!( body: "who was slick ricks producer", - user: user_twhite, + user: user_admin, track: track_childrens_story ) Comment.create!( body: "intersting that slick ricks version is faster than the original", - user: user_twhite, + user: user_admin, track: track_childrens_story ) -# succes message +# success message puts "Seeded: #{User.count} users, #{Artist.count} artists, diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb new file mode 100644 index 0000000..c373b75 --- /dev/null +++ b/spec/helpers/sessions_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the SessionsHelper. For example: +# +# describe SessionsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe SessionsHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb new file mode 100644 index 0000000..33654ed --- /dev/null +++ b/spec/requests/sessions_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe "Sessions" do + describe "GET /new" do + it "returns http success" do + get "/sessions/new" + expect(response).to have_http_status(:success) + end + end + + describe "GET /create" do + it "returns http success" do + get "/sessions/create" + expect(response).to have_http_status(:success) + end + end + + describe "GET /destroy" do + it "returns http success" do + get "/sessions/destroy" + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/views/sessions/create.html.erb_spec.rb b/spec/views/sessions/create.html.erb_spec.rb new file mode 100644 index 0000000..6f5417d --- /dev/null +++ b/spec/views/sessions/create.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "sessions/create.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/sessions/destroy.html.erb_spec.rb b/spec/views/sessions/destroy.html.erb_spec.rb new file mode 100644 index 0000000..ec626a5 --- /dev/null +++ b/spec/views/sessions/destroy.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "sessions/destroy.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/sessions/new.html.erb_spec.rb b/spec/views/sessions/new.html.erb_spec.rb new file mode 100644 index 0000000..667b06c --- /dev/null +++ b/spec/views/sessions/new.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "sessions/new.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end