From 170098046e546e11699756de1efc777acfb7bf41 Mon Sep 17 00:00:00 2001 From: Dan Elbert Date: Tue, 19 Jan 2016 16:50:39 -0600 Subject: [PATCH] Added authentication; cleaned up some UI --- .ruby-version | 1 + Gemfile | 2 +- Gemfile.lock | 11 +++-- app/controllers/application_controller.rb | 27 ++++++++++ app/controllers/ingredients_controller.rb | 3 ++ app/controllers/recipes_controller.rb | 44 ++++++----------- app/controllers/users_controller.rb | 60 +++++++++++++++++++++++ app/helpers/application_helper.rb | 22 ++++++++- app/models/recipe.rb | 1 + app/models/user.rb | 15 ++++++ app/views/ingredients/_form.html.erb | 6 +-- app/views/ingredients/index.html.erb | 43 +++++++++------- app/views/ingredients/index.json.jbuilder | 4 -- app/views/layouts/application.html.erb | 5 ++ app/views/recipes/index.json.jbuilder | 4 -- app/views/recipes/show.json.jbuilder | 1 - app/views/users/_form.html.erb | 30 ++++++++++++ app/views/users/edit.html.erb | 11 +++++ app/views/users/login.html.erb | 31 ++++++++++++ app/views/users/new.html.erb | 11 +++++ config/routes.rb | 6 +++ db/migrate/20160119212055_create_users.rb | 15 ++++++ db/schema.rb | 13 ++++- spec/factories/users.rb | 10 ++++ spec/models/user_spec.rb | 5 ++ 25 files changed, 316 insertions(+), 65 deletions(-) create mode 100644 .ruby-version create mode 100644 app/controllers/users_controller.rb create mode 100644 app/models/user.rb delete mode 100644 app/views/ingredients/index.json.jbuilder delete mode 100644 app/views/recipes/index.json.jbuilder delete mode 100644 app/views/recipes/show.json.jbuilder create mode 100644 app/views/users/_form.html.erb create mode 100644 app/views/users/edit.html.erb create mode 100644 app/views/users/login.html.erb create mode 100644 app/views/users/new.html.erb create mode 100644 db/migrate/20160119212055_create_users.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/models/user_spec.rb diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..276cbf9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.3.0 diff --git a/Gemfile b/Gemfile index 86c8b99..d0eed19 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'cocoon', '~> 1.2.6' gem 'unitwise', '~> 2.0.0' # Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' +gem 'bcrypt', '~> 3.1.7' # Use Unicorn as the app server # gem 'unicorn' diff --git a/Gemfile.lock b/Gemfile.lock index be65b38..7323d14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,9 +37,10 @@ GEM thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.3) - autoprefixer-rails (6.2.3) + autoprefixer-rails (6.3.1) execjs json + bcrypt (3.1.10) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) blankslate (3.1.3) @@ -73,7 +74,7 @@ GEM jbuilder (2.4.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.0.5) + jquery-rails (4.1.0) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -122,7 +123,7 @@ GEM activesupport (= 4.2.5) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.4.2) + rake (10.5.0) ref (2.0.0) rspec-core (3.4.1) rspec-support (~> 3.4.0) @@ -185,6 +186,7 @@ PLATFORMS ruby DEPENDENCIES + bcrypt (~> 3.1.7) bootstrap-sass (~> 3.3.6) byebug cocoon (~> 1.2.6) @@ -202,3 +204,6 @@ DEPENDENCIES uglifier (>= 1.3.0) unitwise (~> 2.0.0) web-console (~> 2.0) + +BUNDLED WITH + 1.11.2 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d83690e..c3b0f23 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,4 +2,31 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception + + def ensure_valid_user + if current_user.nil? + flash[:warning] = "You must login" + redirect_to login_path + end + end + + def ensure_admin_user + unless current_user && current_user.is_admin? + flash[:warning] = "You must login as an admin" + redirect_to login_path + end + end + + def current_user + @current_user ||= User.find(session[:user_id]) if session[:user_id] + end + helper_method :current_user + + def set_current_user(user) + if user + session[:user_id] = user.id + else + session[:user_id] = nil + end + end end diff --git a/app/controllers/ingredients_controller.rb b/app/controllers/ingredients_controller.rb index 7d50dab..2afab75 100644 --- a/app/controllers/ingredients_controller.rb +++ b/app/controllers/ingredients_controller.rb @@ -1,6 +1,9 @@ class IngredientsController < ApplicationController + before_action :set_ingredient, only: [:edit, :update, :destroy] + before_filter :ensure_valid_user, only: [:new, :edit, :create, :update, :destroy] + # GET /ingredients # GET /ingredients.json def index diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb index 0f19340..88c1fd8 100644 --- a/app/controllers/recipes_controller.rb +++ b/app/controllers/recipes_controller.rb @@ -1,8 +1,10 @@ class RecipesController < ApplicationController + before_action :set_recipe, only: [:show, :edit, :update, :destroy, :scale] + before_filter :ensure_valid_user, only: [:new, :edit, :create, :update, :destroy] + # GET /recipes - # GET /recipes.json def index @recipes = Recipe.active end @@ -29,48 +31,34 @@ class RecipesController < ApplicationController end # POST /recipes - # POST /recipes.json def create @recipe = Recipe.new(recipe_params) + @recipe.user = current_user - respond_to do |format| - if @recipe.save - format.html { redirect_to @recipe, notice: 'Recipe was successfully created.' } - format.json { render :show, status: :created, location: @recipe } - else - format.html { render :new } - format.json { render json: @recipe.errors, status: :unprocessable_entity } - end + if @recipe.save + redirect_to @recipe, notice: 'Recipe was successfully created.' + else + render :new end end # PATCH/PUT /recipes/1 - # PATCH/PUT /recipes/1.json def update - respond_to do |format| - if @recipe.update(recipe_params) - format.html { redirect_to @recipe, notice: 'Recipe was successfully updated.' } - format.json { render :show, status: :ok, location: @recipe } - else - format.html { render :edit } - format.json { render json: @recipe.errors, status: :unprocessable_entity } - end + if @recipe.update(recipe_params) + redirect_to @recipe, notice: 'Recipe was successfully updated.' + else + render :edit end end # DELETE /recipes/1 - # DELETE /recipes/1.json def destroy @recipe.deleted = true - respond_to do |format| - if @recipe.save - format.html { redirect_to recipes_url, notice: 'Recipe was successfully destroyed.' } - format.json { head :no_content } - else - format.html { redirect_to recipes_url, error: 'Recipe could not be destroyed.' } - format.json { head :no_content } - end + if @recipe.save + redirect_to recipes_url, notice: 'Recipe was successfully destroyed.' + else + redirect_to recipes_url, error: 'Recipe could not be destroyed.' end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..6b3e614 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,60 @@ +class UsersController < ApplicationController + + before_filter :ensure_valid_user, except: [:login, :verify_login, :new, :create] + + def login + + end + + def logout + set_current_user(nil) + session.destroy + flash[:notice] = "Logged out" + redirect_to root_path + end + + def verify_login + if user = User.authenticate(params[:email], params[:password]) + set_current_user(user) + flash[:notice] = "Welcome, #{user.full_name}" + redirect_to root_path + else + flash[:error] = "Invalid credentials" + render :login + end + end + + def new + @user = User.new + end + + def create + @user = User.new(user_params) + + if @user.save + set_current_user(@user) + redirect_to root_path, notice: 'User was successfully created.' + else + render action: :new + end + end + + def edit + @user = current_user + end + + def update + @user = current_user + if @user.update(user_params) + redirect_to root_path, notice: 'User account updated' + else + render action: 'edit' + end + end + + private + + def user_params + params.require(:user).permit(:username, :email, :full_name, :password, :password_confirmation) + end +end \ No newline at end of file diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e030367..24837eb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,7 +11,27 @@ module ApplicationHelper ] end - def nav_item(name, url, controller) + def profile_nav_items + if current_user + [content_tag('li', class: 'dropdown') do + li_cnt = ''.html_safe + + li_cnt << link_to('#', class: 'dropdown-toggle', data: {toggle: 'dropdown'}, role: 'button') do + ''.html_safe << "#{current_user.display_name}" << content_tag('span', '', class: 'caret') + end + + li_cnt << content_tag('ul', class: 'dropdown-menu') do + ''.html_safe << nav_item('Profile', edit_user_path) << nav_item('Logout', logout_path) + end + + li_cnt + end] + else + [nav_item('Login', login_path)] + end + end + + def nav_item(name, url, controller = nil) content_tag('li', link_to(name, url), class: active_for_controller(controller)) end diff --git a/app/models/recipe.rb b/app/models/recipe.rb index 0c6c5ac..8c27cd4 100644 --- a/app/models/recipe.rb +++ b/app/models/recipe.rb @@ -2,6 +2,7 @@ class Recipe < ActiveRecord::Base has_many :recipe_ingredients, -> { order :sort_order }, inverse_of: :recipe, dependent: :destroy has_many :recipe_steps, -> { order :sort_order }, inverse_of: :recipe, dependent: :destroy + belongs_to :user scope :active, -> { where('deleted <> ? OR deleted IS NULL', true) } diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..2df7250 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,15 @@ +class User < ActiveRecord::Base + + has_secure_password + + validates :username, presence: true, uniqueness: { case_sensitive: false } + + def self.authenticate(email, password) + find_by_email(email).try(:authenticate, password) + end + + def display_name + self.full_name.present? ? self.full_name : self.username + end + +end diff --git a/app/views/ingredients/_form.html.erb b/app/views/ingredients/_form.html.erb index 0c3ef34..9235d50 100644 --- a/app/views/ingredients/_form.html.erb +++ b/app/views/ingredients/_form.html.erb @@ -5,17 +5,17 @@ <%= render partial: 'shared/error_list', locals: {model: @ingredient} %>
- <%= f.label :name %> + <%= f.label :name, class: 'control-label' %> <%= f.text_field :name, class: 'form-control', autofocus: true %>
- <%= f.label :density %> + <%= f.label :density, class: 'control-label' %> <%= f.text_field :density, class: 'form-control' %>
- <%= f.label :notes %> + <%= f.label :notes, class: 'control-label' %> <%= f.text_area :notes, class: 'form-control' %>
diff --git a/app/views/ingredients/index.html.erb b/app/views/ingredients/index.html.erb index ece143f..dad4916 100644 --- a/app/views/ingredients/index.html.erb +++ b/app/views/ingredients/index.html.erb @@ -4,30 +4,35 @@

Ingredients

- - - - - - - - + <% if @ingredients.empty? %> +

No Ingredients

+ <% else %> - - <% @ingredients.each do |ingredient| %> +
NameDensity
+ - - - + + + - <% end %> - -
<%= link_to ingredient.name, edit_ingredient_path(ingredient) %><%= ingredient.density %><%= link_to 'Destroy', ingredient, method: :delete, data: { confirm: 'Are you sure?' } %>NameDensity
+ -
+ + <% @ingredients.each do |ingredient| %> + + <%= link_to ingredient.name, edit_ingredient_path(ingredient) %> + <%= ingredient.density %> + <%= link_to 'Destroy', ingredient, method: :delete, data: { confirm: 'Are you sure?' } %> + + <% end %> + + + <% end %> - <%= link_to 'New Ingredient', new_ingredient_path, class: 'btn btn-primary' %> +
+ + <%= link_to 'New Ingredient', new_ingredient_path, class: 'btn btn-default' %> - \ No newline at end of file + diff --git a/app/views/ingredients/index.json.jbuilder b/app/views/ingredients/index.json.jbuilder deleted file mode 100644 index 8a6a136..0000000 --- a/app/views/ingredients/index.json.jbuilder +++ /dev/null @@ -1,4 +0,0 @@ -json.array!(@ingredients) do |ingredient| - json.extract! ingredient, :id - json.url ingredient_url(ingredient, format: :json) -end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d29b0d9..d82e36e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -29,6 +29,11 @@ <%= li %> <% end %> + diff --git a/app/views/recipes/index.json.jbuilder b/app/views/recipes/index.json.jbuilder deleted file mode 100644 index 2298d85..0000000 --- a/app/views/recipes/index.json.jbuilder +++ /dev/null @@ -1,4 +0,0 @@ -json.array!(@recipes) do |recipe| - json.extract! recipe, :id - json.url recipe_url(recipe, format: :json) -end diff --git a/app/views/recipes/show.json.jbuilder b/app/views/recipes/show.json.jbuilder deleted file mode 100644 index 4e12951..0000000 --- a/app/views/recipes/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.extract! @recipe, :id, :created_at, :updated_at diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb new file mode 100644 index 0000000..f90cd32 --- /dev/null +++ b/app/views/users/_form.html.erb @@ -0,0 +1,30 @@ +<%= form_for(@user, url: user_path) do |f| %> + + <%= render partial: 'shared/error_list', locals: {model: @user} %> + +
+ <%= f.label :username, class: 'control-label' %> + <%= f.text_field :username, class: 'form-control' %> +
+
+ <%= f.label :full_name, 'Name', class: 'control-label' %> + <%= f.text_field :full_name, class: 'form-control' %> +
+
+ <%= f.label :email, class: 'control-label' %> + <%= f.text_field :email, class: 'form-control' %> +
+
+ <%= f.label :password, class: 'control-label' %> + <%= f.password_field :password, class: 'form-control' %> +
+
+ <%= f.label :password_confirmation, class: 'control-label' %> + <%= f.password_field :password_confirmation, class: 'form-control' %> +
+ +
+ <%= f.submit class: 'btn btn-primary' %> + <%= link_to "Back", root_path, class: 'btn btn-default' %> +
+<% end %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb new file mode 100644 index 0000000..d15fd3f --- /dev/null +++ b/app/views/users/edit.html.erb @@ -0,0 +1,11 @@ +
+
+ + + + <%= render partial: 'form' %> + +
+
diff --git a/app/views/users/login.html.erb b/app/views/users/login.html.erb new file mode 100644 index 0000000..5ed4e1d --- /dev/null +++ b/app/views/users/login.html.erb @@ -0,0 +1,31 @@ + + +
+
+ + + + + <%= form_tag(login_path, :method => :post) do %> + +
+ <%= label_tag :email, "Email", class: 'control-label' %> + <%= text_field_tag :email, nil, class: 'form-control' %> +
+ +
+ <%= label_tag :password, "Password", class: 'control-label' %> + <%= password_field_tag :password, nil, class: 'form-control' %> +
+ + <%= submit_tag("Login", class: 'btn btn-primary') %> + <% end %> + +
+ + <%= link_to "Create an Account", new_user_path, class: 'btn btn-default' %> + +
+
\ No newline at end of file diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 0000000..39b2b52 --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,11 @@ +
+
+ + + + <%= render partial: 'form' %> + +
+
diff --git a/config/routes.rb b/config/routes.rb index f0a3718..f20c78a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,12 @@ Rails.application.routes.draw do end end + resource :user, only: [:new, :create, :edit, :update] + + get '/login' => 'users#login', as: :login + post '/login' => 'users#verify_login' + get '/logout' => 'users#logout', as: :logout + root 'recipes#index' diff --git a/db/migrate/20160119212055_create_users.rb b/db/migrate/20160119212055_create_users.rb new file mode 100644 index 0000000..4294cbf --- /dev/null +++ b/db/migrate/20160119212055_create_users.rb @@ -0,0 +1,15 @@ +class CreateUsers < ActiveRecord::Migration + def change + create_table :users do |t| + t.string :username + t.string :email + t.string :full_name + t.string :password_digest + t.boolean :admin + + t.timestamps null: false + end + + add_column :recipes, :user_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 0885b4a..6bc89ae 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160119012704) do +ActiveRecord::Schema.define(version: 20160119212055) do create_table "ingredients", force: :cascade do |t| t.string "name" @@ -55,6 +55,17 @@ ActiveRecord::Schema.define(version: 20160119012704) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "deleted" + t.integer "user_id" + end + + create_table "users", force: :cascade do |t| + t.string "username" + t.string "email" + t.string "full_name" + t.string "password_digest" + t.boolean "admin" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..1eeebb6 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :user do + username "MyString" +email "MyString" +full_name "MyString" +password_digest "MyString" +admin false + end + +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..47a31bb --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end