diff --git a/Gemfile b/Gemfile index 9eebefb9..b7b70470 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,8 @@ ruby "3.2.1" gem "simple_form" +gem "pundit" + # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.0.4", ">= 7.0.4.3" diff --git a/Gemfile.lock b/Gemfile.lock index 42668dd8..e73e59d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -233,6 +233,8 @@ GEM public_suffix (5.0.1) puma (5.6.5) nio4r (~> 2.0) + pundit (2.3.1) + activesupport (>= 3.0.0) racc (1.6.2) rack (2.2.7) rack-protection (3.0.6) @@ -425,6 +427,7 @@ DEPENDENCIES pg (~> 1.1) pry-rails puma (~> 5.0) + pundit rails (~> 7.0.4, >= 7.0.4.3) rails-erd rails_db diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bd664b1d..99411628 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,10 +3,26 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? + include Pundit::Authorization + + + + + rescue_from Pundit::NotAuthorizedError, with: :user_not_auhtorized + + protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :private, :name, :bio, :website, :avatar_image]) devise_parameter_sanitizer.permit(:account_update, keys: [:username, :private, :name, :bio, :website, :avatar_image]) end + + private + + def user_not_authorized + flash[:alert] = "You are not authorized to perform this action." + + redirect_back fallback_location: root_url + end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 046a8e5d..ebe41025 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,5 +1,6 @@ class CommentsController < ApplicationController before_action :set_comment, only: %i[ show edit update destroy ] + before_action :is_an_authorized_user, only: [:destroy, :create] # GET /comments or /comments.json def index @@ -63,6 +64,13 @@ def set_comment @comment = Comment.find(params[:id]) end + def is_an_authorized_user + @photo = Photo.find(params.fetch(:comment).fetch(:photo_id)) + if current_user != @photo.owner && @photo.owner.private? && !current_user.leaders.include?(@photo.owner) + redirect_back fallback_location: root_url, alert: "Not authorized" + end + end + # Only allow a list of trusted parameters through. def comment_params params.require(:comment).permit(:author_id, :photo_id, :body) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 78e53163..230ce5e5 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -1,5 +1,7 @@ class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] + before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] + # before_action :ensure_user_is_authorized, only: [:show] # GET /photos or /photos.json def index @@ -8,6 +10,7 @@ def index # GET /photos/1 or /photos/1.json def show + authorize @photo end # GET /photos/new @@ -51,10 +54,11 @@ def update # DELETE /photos/1 or /photos/1.json def destroy @photo.destroy - respond_to do |format| - format.html { redirect_back fallback_location: root_url, notice: "Photo was successfully destroyed." } - format.json { head :no_content } - end + + respond_to do |format| + format.html { redirect_back fallback_location: root_url, notice: "Photo was successfully destroyed." } + format.json { head :no_content } + end end private @@ -63,6 +67,18 @@ def set_photo @photo = Photo.find(params[:id]) end + def ensure_current_user_is_owner + if current_user != @photo.owner + redirect_back fallback_location: root_url, alert: "You're not authorized for that." + end + end + + # def ensure_user_is_authorized + # if !PhotoPolicy.new(current_user, @photo).show? + # raise Pundit::NotAuthorizedError, "not allowed" + # end + # end + # Only allow a list of trusted parameters through. def photo_params params.require(:photo).permit(:image, :comments_count, :likes_count, :caption, :owner_id) diff --git a/app/models/comment.rb b/app/models/comment.rb index 14a8eb00..0761b0e8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -22,6 +22,7 @@ class Comment < ApplicationRecord belongs_to :author, class_name: "User", counter_cache: true belongs_to :photo, counter_cache: true + has_one :owner, through: :photo validates :body, presence: true end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 00000000..e000cba5 --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def index? + false + end + + def show? + false + end + + def create? + false + end + + def new? + create? + end + + def update? + false + end + + def edit? + update? + end + + def destroy? + false + end + + class Scope + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + raise NotImplementedError, "You must define #resolve in #{self.class}" + end + + private + + attr_reader :user, :scope + end +end diff --git a/app/policies/photo_policy.rb b/app/policies/photo_policy.rb new file mode 100644 index 00000000..7641ddfa --- /dev/null +++ b/app/policies/photo_policy.rb @@ -0,0 +1,17 @@ +class PhotoPolicy + attr_reader :user, :photo + + def initialize(user, photo) + @user = user + @photo = photo + end + + # Our policy is that a photo should only be seen by the owner or followers + # of the owner, unless the owner is not private in which case anyone can + # see it + def show? + user == photo.owner || + !photo.owner.private? || + photo.owner.followers.include?(user) + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..bd575717 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,14 @@ +class UserPolicy + attr_reader :current_user, :user + + def initialize(current_user, user) + @current_user = current_user + @user = user + end + + def show? + user == current_user || + !user.private? || + user.followers.include?(current_user) + end +end diff --git a/app/views/photos/_photo.html.erb b/app/views/photos/_photo.html.erb index f0de50b8..57799bc8 100644 --- a/app/views/photos/_photo.html.erb +++ b/app/views/photos/_photo.html.erb @@ -5,7 +5,7 @@ <%= link_to photo.owner.username, user_path(photo.owner.username), class: "text-dark" %> - + <% if current_user.username == photo.owner.username %>
<%= link_to edit_photo_path(photo), class: "btn btn-link btn-sm text-muted" do %> @@ -15,6 +15,7 @@ <% end %>
+ <% end %> <%= image_tag photo.image, class: "img-fluid" %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 5656d7d5..124f0fd8 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -4,6 +4,7 @@ +<% if policy(@user).show? %>
<%= render "users/profile_nav", user: @user %> @@ -16,4 +17,5 @@ <%= render "photos/photo", photo: photo %>
+ <% end %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 47050a54..53545094 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,9 +4,9 @@ devise_for :users resources :comments - resources :follow_requests - resources :likes - resources :photos + resources :follow_requests, except: [:index, :show, :new, :edit] + resources :likes, only: [:create, :destroy] + resources :photos, except: [:index] get ":username" => "users#show", as: :user get ":username/liked" => "users#liked", as: :liked @@ -14,4 +14,4 @@ get ":username/discover" => "users#discover", as: :discover get ":username/followers" => "users#followers", as: :followers get ":username/following" => "users#following", as: :following -end \ No newline at end of file +end diff --git a/db/schema.rb b/db/schema.rb index 9793baa9..04d2af9e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -35,6 +35,11 @@ t.index ["sender_id"], name: "index_follow_requests_on_sender_id" end + create_table "friends", id: :serial, force: :cascade do |t| + t.text "name", null: false + t.integer "age", null: false + end + create_table "likes", force: :cascade do |t| t.bigint "fan_id", null: false t.bigint "photo_id", null: false