diff --git a/Gemfile b/Gemfile index 9eebefb9..4ac5df7f 100644 --- a/Gemfile +++ b/Gemfile @@ -85,6 +85,7 @@ gem "htmlbeautifier" gem "http" gem "sqlite3", "~> 1.4" gem "table_print" +gem "pundit" group :development do gem "annotate" diff --git a/Gemfile.lock b/Gemfile.lock index c4999ef9..59b103d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -217,6 +217,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.9) + nokogiri (1.15.5-arm64-darwin) + racc (~> 1.4) nokogiri (1.15.5-x86_64-darwin) racc (~> 1.4) nokogiri (1.15.5-x86_64-linux) @@ -235,6 +237,8 @@ GEM public_suffix (5.0.4) puma (5.6.7) nio4r (~> 2.0) + pundit (2.3.1) + activesupport (>= 3.0.0) racc (1.6.2) rack (2.2.8) rack-protection (3.0.6) @@ -345,6 +349,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + sqlite3 (1.6.8-arm64-darwin) sqlite3 (1.6.8-x86_64-darwin) sqlite3 (1.6.8-x86_64-linux) stimulus-rails (1.2.2) @@ -402,6 +407,7 @@ GEM zeitwerk (2.6.12) PLATFORMS + arm64-darwin-22 x86_64-darwin-22 x86_64-linux @@ -427,6 +433,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..4161bb79 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,8 +1,23 @@ class ApplicationController < ActionController::Base + include Pundit + before_action :authenticate_user! before_action :configure_permitted_parameters, if: :devise_controller? + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized + + after_action :verify_authorized, unless: :devise_controller? + after_action :verify_policy_scoped, only: :index, unless: :devise_controller? + + private + + def user_not_authorized + flash[:alert] = "You are not authorized to perform this action" + + redirect_back fallback_location: root_url + end + protected def configure_permitted_parameters diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 046a8e5d..d62a657f 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -8,6 +8,7 @@ def index # GET /comments/1 or /comments/1.json def show + authorize @comment end # GET /comments/new @@ -17,12 +18,14 @@ def new # GET /comments/1/edit def edit + authorize @comment end # POST /comments or /comments.json def create @comment = Comment.new(comment_params) @comment.author = current_user + authorize @comment respond_to do |format| if @comment.save @@ -37,6 +40,7 @@ def create # PATCH/PUT /comments/1 or /comments/1.json def update + authorize @comment respond_to do |format| if @comment.update(comment_params) format.html { redirect_to root_url, notice: "Comment was successfully updated." } @@ -50,6 +54,7 @@ def update # DELETE /comments/1 or /comments/1.json def destroy + authorize @comment @comment.destroy respond_to do |format| format.html { redirect_back fallback_location: root_url, notice: "Comment was successfully destroyed." } diff --git a/app/controllers/follow_requests_controller.rb b/app/controllers/follow_requests_controller.rb index 9c30da7c..71193fb2 100644 --- a/app/controllers/follow_requests_controller.rb +++ b/app/controllers/follow_requests_controller.rb @@ -3,11 +3,13 @@ class FollowRequestsController < ApplicationController # GET /follow_requests or /follow_requests.json def index - @follow_requests = FollowRequest.all + authorize (@follow_request || FollowRequest) + @follow_requests = policy_scope(FollowRequest) end # GET /follow_requests/1 or /follow_requests/1.json def show + authorize @follow_request end # GET /follow_requests/new @@ -17,12 +19,14 @@ def new # GET /follow_requests/1/edit def edit + authorize @follow_request end # POST /follow_requests or /follow_requests.json def create @follow_request = FollowRequest.new(follow_request_params) @follow_request.sender = current_user + authorize @follow_request respond_to do |format| if @follow_request.save @@ -37,6 +41,7 @@ def create # PATCH/PUT /follow_requests/1 or /follow_requests/1.json def update + authorize @follow_request respond_to do |format| if @follow_request.update(follow_request_params) format.html { redirect_back fallback_location: root_url, notice: "Follow request was successfully updated." } @@ -50,6 +55,7 @@ def update # DELETE /follow_requests/1 or /follow_requests/1.json def destroy + authorize @follow_request @follow_request.destroy respond_to do |format| format.html { redirect_back fallback_location: root_url, notice: "Follow request was successfully destroyed." } diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 78e53163..3edd4f97 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -1,6 +1,10 @@ class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] + before_action :authorize_photo, except: %i[index new] + def authorize_photo + authorize @photo + end # GET /photos or /photos.json def index @photos = Photo.all @@ -13,6 +17,7 @@ def show # GET /photos/new def new @photo = Photo.new + authorize @photo end # GET /photos/1/edit @@ -23,6 +28,7 @@ def edit def create @photo = Photo.new(photo_params) @photo.owner = current_user + authorize @photo respond_to do |format| if @photo.save @@ -50,7 +56,10 @@ def update # DELETE /photos/1 or /photos/1.json def destroy + @photo = Photo.find(params[:id]) + authorize @photo @photo.destroy + respond_to do |format| format.html { redirect_back fallback_location: root_url, notice: "Photo was successfully destroyed." } format.json { head :no_content } diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 31db66e9..49a2debb 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,12 @@ class UsersController < ApplicationController before_action :set_user, only: %i[ show liked feed followers following discover ] + before_action { authorize @user } + + def see_follow_request_button + end + + def view_private_profile_content + end private @@ -10,4 +17,4 @@ def set_user @user = current_user end end -end \ No newline at end of file +end 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/comment_policy.rb b/app/policies/comment_policy.rb new file mode 100644 index 00000000..10e44045 --- /dev/null +++ b/app/policies/comment_policy.rb @@ -0,0 +1,27 @@ +# app/policies/comment_policy.rb + +class CommentPolicy < ApplicationPolicy + attr_reader :user, :comment + + def initialize(user, comment) + @user = user + @comment = comment + end + + def show? + user == comment.author || PhotoPolicy.new(user, comment.photo).show? + end + + def create? + true + end + + def update? + user == comment.author + end + + def destroy? + user == comment.author || user == comment.photo.owner + end + +end diff --git a/app/policies/follow_request_policy.rb b/app/policies/follow_request_policy.rb new file mode 100644 index 00000000..b7ef49e4 --- /dev/null +++ b/app/policies/follow_request_policy.rb @@ -0,0 +1,37 @@ +class FollowRequestPolicy < ApplicationPolicy + attr_reader :user, :follow_request + + def initialize(user, follow_request) + @user = user + @follow_request = follow_request + end + + def create? + true + end + + def destroy? + user == follow_request.sender || user == follow_request.recipient + end + + def update? + user == follow_request.recipient || user == follow_request.sender + end + + # Scope class + class Scope + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + # This method defines which follow requests the user is allowed to view + def resolve + scope.where(sender_id: user.id).or(scope.where(recipient_id: user.id)) + end + + end + +end diff --git a/app/policies/like_policy.rb b/app/policies/like_policy.rb new file mode 100644 index 00000000..34ddddb8 --- /dev/null +++ b/app/policies/like_policy.rb @@ -0,0 +1,11 @@ +# app/policies/like_policy.rb + +class LikePolicy < ApplicationPolicy + attr_reader :user, :like + + def initialize(user, like) + @user = user + @like = like + end + +end diff --git a/app/policies/photo_policy.rb b/app/policies/photo_policy.rb new file mode 100644 index 00000000..73400d39 --- /dev/null +++ b/app/policies/photo_policy.rb @@ -0,0 +1,29 @@ +# app/policies/photo_policy.rb + +class PhotoPolicy < ApplicationPolicy + attr_reader :user, :photo + + def initialize(user, photo) + @user = user + @photo = photo + end + + def show? + user == photo.owner || + !photo.owner.private? || + photo.owner.followers.include?(user) + end + + def update? + user == photo.owner + + end + + def create? + !user.nil? + end + + def destroy? + user == photo.owner + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..3f18e4e6 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,34 @@ +# app/policies/user_policy.rb + +class UserPolicy < ApplicationPolicy + attr_reader :current_user, :user + + def initialize(current_user, user) + @current_user = current_user + @user = user + end + + def show? + true + end + + def feed? + user == current_user + end + + def discover? + user == current_user + end + + def liked? + true + end + + def view_private_profile_content? + !user.private? || user == current_user || user.followers.include?(current_user) + end + + def see_follow_request_button? + current_user != user + end +end diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index a7ee4c56..26e0185e 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -10,12 +10,16 @@

<%= comment.body %>

- <%= link_to edit_comment_path(comment), class: "btn btn-link btn-sm text-muted" do %> - + <% if policy(comment).update? %> + <%= link_to edit_comment_path(comment), class: "btn btn-link btn-sm text-muted" do %> + + <% end %> <% end %> - <%= link_to comment, data: { turbo_method: :delete }, class: "btn btn-link btn-sm text-muted" do %> - + <% if policy(comment).destroy? %> + <%= link_to comment, data: { turbo_method: :delete }, class: "btn btn-link btn-sm text-muted" do %> + + <% end %> <% end %>
diff --git a/app/views/follow_requests/_form.html.erb b/app/views/follow_requests/_form.html.erb index 89ea4b1f..1e88d8f3 100644 --- a/app/views/follow_requests/_form.html.erb +++ b/app/views/follow_requests/_form.html.erb @@ -13,7 +13,9 @@ <%= form.hidden_field :recipient_id %> -
- <%= form.submit "Follow", class: "btn btn-outline-secondary btn-block" %> -
+ <% if policy(@user).see_follow_request_button? %> +
+ <%= form.submit "Follow", class: "btn btn-outline-secondary btn-block" %> +
+ <% end %> <% end %> diff --git a/app/views/follow_requests/index.html.erb b/app/views/follow_requests/index.html.erb index 0d211fd5..c1758e8e 100644 --- a/app/views/follow_requests/index.html.erb +++ b/app/views/follow_requests/index.html.erb @@ -14,13 +14,11 @@ <% @follow_requests.each do |follow_request| %> - <%= follow_request.recipient_id %> - <%= follow_request.sender_id %> + <%= follow_request.recipient.username %> + <%= follow_request.sender.username %> <%= follow_request.status %> - <%= link_to 'Show', follow_request %> - <%= link_to 'Edit', edit_follow_request_path(follow_request) %> - <%= link_to 'Destroy', follow_request, data: { turbo_method: :delete }, data: { confirm: 'Are you sure?' } %> + <% end %> diff --git a/app/views/photos/_photo.html.erb b/app/views/photos/_photo.html.erb index f0de50b8..1d2f8454 100644 --- a/app/views/photos/_photo.html.erb +++ b/app/views/photos/_photo.html.erb @@ -7,12 +7,14 @@
- <%= link_to edit_photo_path(photo), class: "btn btn-link btn-sm text-muted" do %> - - <% end %> + <% if current_user == photo.owner %> + <%= link_to edit_photo_path(photo), class: "btn btn-link btn-sm text-muted" do %> + + <% end %> - <%= link_to photo, data: { turbo_method: :delete }, class: "btn btn-link btn-sm text-muted" do %> - + <%= link_to photo, data: { turbo_method: :delete }, class: "btn btn-link btn-sm text-muted" do %> + + <% end %> <% end %>
diff --git a/app/views/users/_profile_nav.html.erb b/app/views/users/_profile_nav.html.erb index 88884ade..501b1f00 100644 --- a/app/views/users/_profile_nav.html.erb +++ b/app/views/users/_profile_nav.html.erb @@ -1,3 +1,4 @@ +<% if policy(@user).view_private_profile_content? %> + +<% end %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 5656d7d5..74a817eb 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -4,16 +4,18 @@ -
-
- <%= render "users/profile_nav", user: @user %> -
-
- -<% @user.own_photos.each do |photo| %> -
+<% if policy(@user).view_private_profile_content? %> +
- <%= render "photos/photo", photo: photo %> + <%= render "users/profile_nav", user: @user %>
+ + <% @user.own_photos.each do |photo| %> +
+
+ <%= render "photos/photo", photo: photo %> +
+
+ <% end %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 47050a54..1c33cc5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,17 +1,17 @@ Rails.application.routes.draw do root "users#feed" - + 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 get ":username/feed" => "users#feed", as: :feed 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