diff --git a/Gemfile b/Gemfile index 66c0587..2fa449b 100644 --- a/Gemfile +++ b/Gemfile @@ -79,3 +79,5 @@ group :test do end gem 'devise', '~> 4.9' + +gem 'cancancan', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index a3e56bc..ddb4ff8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM bootsnap (1.16.0) msgpack (~> 1.2) builder (3.2.4) + cancancan (3.5.0) capybara (3.39.2) addressable matrix @@ -268,6 +269,7 @@ PLATFORMS DEPENDENCIES bootsnap + cancancan (~> 3.5) capybara debug devise (~> 4.9) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 218a5a1..2fbaefe 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -4,14 +4,39 @@ box-sizing: border-box; } +.wrapper { + margin: 1rem auto; + gap: 1rem; + box-shadow: 0 0 2px 2px rgb(231, 231, 237); +} + +.wrapper, .container { - width: 80vw; + width: 90vw; height: 100vh; display: flex; flex-direction: column; align-items: center; - margin: 2rem 0; + background-color: whitesmoke; +} + +.navbar { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; gap: 1rem; + padding: 1rem; + background-color: #1868f1; + border: 1px solid rgb(128, 128, 128); +} + +.logout { + cursor: pointer; + padding: 5px 10px; + font-size: 15px; + font-weight: bold; } .user { @@ -21,6 +46,7 @@ justify-content: center; align-items: center; gap: 2rem; + padding: 1rem; } .user-photo { @@ -36,7 +62,7 @@ } .user-info { - width: 46%; + width: 80%; display: flex; flex-direction: row; justify-content: space-between; @@ -51,7 +77,9 @@ } .user-posts { - justify-content: flex-end; + display: flex; + flex-direction: column; + gap: 1rem; align-self: flex-end; padding: 2rem 1rem 1rem 1rem; font-size: small; @@ -70,10 +98,11 @@ a { } .post { - width: 60%; + width: 90%; display: flex; flex-direction: column; border: 3px solid black; + margin-bottom: 1rem; } .post h3 { @@ -86,42 +115,82 @@ a { padding: 0.5rem 1rem; } +.post-text { + display: grid; + grid-template-columns: 2fr 0.5fr; + gap: 1rem; +} + .count { display: flex; flex-direction: row; - justify-content: flex-end; + justify-content: space-around; + align-items: center; padding: 0.5rem 1rem; - font-size: small; + font-size: medium; + gap: 0.5rem; +} + +.post-text .count { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: flex-start; + text-align: center; + padding: 0.5rem 2rem 0.5rem 10vh; + font-size: medium; + gap: 0.5rem; } .count span { padding: 0 0.25rem; } +.like_count_btn { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; +} + .post-comments { - width: 60%; + width: 100%; display: flex; flex-direction: column; padding: 1rem; gap: 0.5rem; - border: 3px solid black; } .btn-link { + width: auto; align-self: center; } -button { +button, +form input[type="submit"] { border-bottom: 5px solid black; border-right: 3px solid black; border-radius: 5px; + padding: 5px; +} + +.like_count_btn button { + width: 2vw; + height: 2vw; + font-size: x-large; + padding: 0; +} + +form input[type="hidden"] { + width: 1px; + height: 1px; } .btn-link, .btn-link button { cursor: pointer; - padding: 5px; font-size: 15px; + margin: 10px 0; } .post-form, @@ -135,21 +204,39 @@ button { background-color: #f5f5f5; } -.comments, .posts { width: 80%; display: flex; - flex-direction: column; + flex-direction: row; padding: 1rem; gap: 1rem; } +.comments { + display: grid; + grid-template-columns: 0.5fr 2.5fr 0.5fr; +} + +.post .add-comment { + margin: 0 auto; + align-self: center; +} + +.comment-form textarea, +.post-form textarea { + width: 100%; + height: 100px; + padding: 0.5rem; + border: 1px solid black; + border-radius: 5px; +} + .comment-btn, .like-button { + width: auto; cursor: pointer; padding: 5px 10px; margin-left: 5rem; - font-size: 15px; border-bottom: 5px solid black; border-right: 3px solid black; border-radius: 5px; @@ -184,6 +271,15 @@ form input { padding: 0.5rem; } +.like_count_btn form { + width: 3vw; +} + +.comments form { + width: auto; + align-self: flex-end; +} + .shared { display: flex; flex-direction: column-reverse; @@ -226,7 +322,7 @@ ul { .auth_links { display: flex; - flex-direction: row; + flex-direction: column; justify-content: flex-start; align-items: center; gap: 1rem; @@ -234,6 +330,12 @@ ul { margin: auto auto; } +.sign_in_up { + display: flex; + flex-direction: row; + gap: 1rem; +} + .auth_links a { color: blue; padding: 0.5rem; @@ -284,8 +386,44 @@ ul { } .form-elements { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + margin: 1rem 0; +} + +.like-button { + border: none; + background-color: transparent; + font-size: 2rem; +} + +.short-button { + width: 7vw; + align-items: flex-end; +} + +.user-comments-likes { display: flex; flex-direction: row; + justify-content: flex-end; + align-items: center; gap: 1rem; + width: 100%; + margin: 1rem 0; +} + +.count form { + width: auto; +} + +.post .count { + display: grid; + grid-template-columns: 0.5fr 0.5fr; + justify-items: end; + gap: 1rem; + width: 100%; margin: 1rem 0; } diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index cf3ebcb..fc1d81a 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -18,6 +18,18 @@ def create end end + def destroy + @comment = Comment.find(params[:id]) + @post = @comment.post + + if can? :destroy, @comment + @comment.destroy + redirect_to user_post_path(@post.author, @post), notice: 'Comment was successfully deleted.' + else + redirect_to user_posts_path(current_user), alert: 'You are not authorized to delete this comment.' + end + end + private def comment_params diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 54c250d..caee4ac 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,4 +1,6 @@ class PostsController < ApplicationController + load_and_authorize_resource + def index @user = User.includes(posts: { comments: :author }).find(params[:user_id]) @current_user = current_user @@ -27,6 +29,17 @@ def create end end + def destroy + @post = Post.find(params[:id]) + + if can? :destroy, @post + @post.destroy + redirect_to "/users/#{current_user.id}/posts", notice: 'Successfully deleted.' + else + redirect_to user_post_path(@post.author_id, @post), alert: 'Unauthorized action.' + end + end + private def post_params diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000..1ccac09 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,38 @@ +class Ability + include CanCan::Ability + + def initialize(user) + # Define abilities for the user here. For example: + can :read, :all + + return unless user.present? + + can :manage, User, id: user.id # user can manage only his own profile + can :manage, Post, author_id: user.id # user can manage only his own posts + can :manage, Comment, user_id: user.id # user can manage only his own comments + can :create, Like # user can create likes + + return unless user.role == 'admin' + + can :destroy, Post # admin can delete any post + can :destroy, Comment # admin can delete any comment + + # The first argument to `can` is the action you are giving the user + # permission to do. + # If you pass :manage it will apply to every action. Other common actions + # here are :read, :create, :update and :destroy. + # + # The second argument is the resource the user can perform the action on. + # If you pass :all it will apply to every resource. Otherwise pass a Ruby + # class of the resource. + # + # The third argument is an optional hash of conditions to further filter the + # objects. + # For example, here the user can only update published articles. + # + # can :update, Article, published: true + # + # See the wiki for details: + # https://github.com/CanCanCommunity/cancancan/blob/develop/docs/define_check_abilities.md + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 9605970..3609298 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,8 +1,9 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, + devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :validatable + has_many :posts, foreign_key: :author_id, dependent: :destroy has_many :comments, foreign_key: :user_id, dependent: :destroy has_many :likes, foreign_key: :user_id, dependent: :destroy diff --git a/app/views/comments/new.html.erb b/app/views/comments/new.html.erb index 5a14028..8dbecb3 100644 --- a/app/views/comments/new.html.erb +++ b/app/views/comments/new.html.erb @@ -2,9 +2,7 @@

Add Comment

<%= form_with(model: @comment, url: user_post_comments_url(@post.author, @post), local: true) do |form| %> -
- <%= form.text_area :text %> -
+ <%= form.text_area :text %>
<%= form.submit 'Create Comment', class: 'comment-btn'%>
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index ac81ff3..dfef229 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -6,12 +6,12 @@
<%= f.label :name %>
- <%= f.text_field :name, autofocus: true, placeholder: "Name" %> + <%= f.text_field :name, autofocus: true, placeholder: "Full Name" %>
<%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: "Email address" %>
@@ -19,12 +19,12 @@ <% if @minimum_password_length %> (<%= @minimum_password_length %> characters minimum) <% end %>
- <%= f.password_field :password, autocomplete: "new-password" %> + <%= f.password_field :password, autocomplete: "new-password", placeholder: "New Password" %>
<%= f.label :password_confirmation %>
- <%= f.password_field :password_confirmation, autocomplete: "new-password" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", placeholder: "Confirm Password" %>
<%= render "devise/shared/error_messages", resource: resource %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index d7f9526..f815e09 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -1,15 +1,17 @@ - -
- <% post.comments.each do |comment| %> -
- <%= comment.author.name %>: - <%= comment.text %> +
+ <% if post.recent_comments.length > 0 %> +
+

Comments

+ <% post.recent_comments.each do |comment| %> +
+

<%= comment.user.name %>:

+

<%= comment.text %>

+ + <% if can? :destroy, comment %> + <%= form_with url: "/users/#{post.author.id}/posts/#{post.id}/comments/#{comment.id}" , method: :delete do |f|%> + <%= f.submit 'Delete', class: 'short-button' %> + <%end%> + <%end%> +
+ <% end %>
<% end %> -
<% end %> - + - + \ No newline at end of file diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb index 4dc676a..fc943f8 100644 --- a/app/views/posts/show.html.erb +++ b/app/views/posts/show.html.erb @@ -4,47 +4,56 @@ by <%= @user.name %> -
- Comments: - <% if @post.comments_counter %> - <%= @post.comments_counter %> - <% else %> - 0 - <% end %> - - Likes: - <% if @post.likes_counter %> - <%= @post.likes_counter %> - <% else %> - 0 - <% end %> - +
+

<%= @post.text %>

+
+ Comments: + <% if @post.comments_counter %> + <%= @post.comments_counter %> + <% else %> + 0 + <% end %> + + + +
-

<%= @post.text %>

-
- <% if @current_user %> - <%= form_with(model: @post, url: like_user_post_path(user_id: @current_user.id, id: @post.id), method: :post, local: true) do |form| %> - <%= form.button type: :submit, class: 'like-button' do %> - Like - <% end %> - <% end %> +
+ + <% if @post.comments.length > 0%> +
+

Comments

+ <% @post.comments.each do |comment| %> +
+

<%= comment.user.name %>:

+

<%= comment.text %>

+ + <% if can? :destroy, comment %> + <%= form_with url: "/users/#{@post.author.id}/posts/#{@post.id}/comments/#{comment.id}" , method: :delete do |f|%> + <%= f.submit 'Delete', class: 'short-button' %> + <%end%> + <%end%> +
<% end %>
-
- -
- <% @post.comments.each do |comment| %> -
- <%= comment.user.name %>: - <%= comment.text %> -
- <% end %> -
- - diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 197d430..6ec44cd 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -16,39 +16,72 @@ 0 <% end %>

+ + <%if @user.name === current_user.name %> + + <%end%>
- -
-

<%= @user.bio %>

-
+ + <% if @user.bio%> +
+

<%= @user.bio %>

+
+ <% end %> - <% @user.recent_posts.each do |post| %> -
+
+ <% @user.recent_posts.each do |post| %> <%= link_to user_post_url(user_id: @user.id, id: post.id), class: 'post-link' do %>

Post #<%= @user.posts.index(post) + 1 %>

<% end %> +

<%= post.text %>

- Comments: - <% if post.comments_counter %> - <%= post.comments_counter %>, - <% else %> - 0, - <% end %> - - Likes: - <% if post.likes_counter %> - <%= post.likes_counter %> - <% else %> - 0 - <% end %> - +
+ <% if can? :destroy, post %> + <%= link_to user_post_url(post.author_id, post), method: :delete, class: 'btn-link' do %> + + <% end %> + <% end %> +
+
+ Comments: + <% if post.comments_counter %> + <%= post.comments_counter %>, + <% else %> + 0, + <% end %> + + Likes: + <% if post.likes_counter %> + <%= post.likes_counter %> + <% else %> + 0 + <% end %> + + + <%= form_with(model: Like.new, url: like_user_post_path(post.author, post)) do |form| %> + <%= form.button type: :submit do %> + ♥ + <% end %> + <% end %> + +
-
- <% end %> - <%= link_to user_posts_url(@user.id), class: 'btn-link' do %> - - <% end %> +
+ <% end %> +
+ + <%= link_to user_posts_url(@user.id), class: 'btn-link' do %> + + <% end %> diff --git a/config/database.yml b/config/database.yml index 85675c9..dcaf49b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -32,7 +32,7 @@ development: username: postgres # The password associated with the postgres role (username). - password: 243243 + password: postgres#13579 # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have @@ -59,7 +59,7 @@ test: <<: *default database: rails_blog_app username: postgres - password: 243243 + password: postgres#13579 # As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is @@ -85,4 +85,4 @@ production: <<: *default database: rails_blog_app username: postgres - password: 243243 + password: postgres#13579 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 6916f65..7f0f95a 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -143,7 +143,7 @@ # without confirming their account. # Default is 0.days, meaning the user cannot access the website without # confirming their account. - # config.allow_unconfirmed_access_for = 2.days + config.allow_unconfirmed_access_for = nil # A period that the user is allowed to confirm their account before their # token becomes invalid. For example, if set to 3.days, the user can confirm diff --git a/config/routes.rb b/config/routes.rb index 06779d6..35ccdcf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,9 +6,9 @@ root "users#index" resources :users, only: [:index, :show] do - resources :posts, only: [:index, :show, :new, :create] do - resources :comments, only: [:new, :create] + resources :posts, only: [:index, :show, :new, :create, :destroy] do + resources :comments, only: [:new, :create, :destroy] post 'like', on: :member end end -end +end \ No newline at end of file diff --git a/db/migrate/20230712183456_add_devise_to_users.rb b/db/migrate/20230712183456_add_devise_to_users.rb index 98ed9dc..d143713 100644 --- a/db/migrate/20230712183456_add_devise_to_users.rb +++ b/db/migrate/20230712183456_add_devise_to_users.rb @@ -22,10 +22,10 @@ def self.up # t.string :last_sign_in_ip ## Confirmable - # t.string :confirmation_token - # t.datetime :confirmed_at - # t.datetime :confirmation_sent_at - # t.string :unconfirmed_email # Only if using reconfirmable + t.string :confirmation_token + t.datetime :confirmed_at + t.datetime :confirmation_sent_at + t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts diff --git a/db/migrate/20230713185223_add_role_to_user.rb b/db/migrate/20230713185223_add_role_to_user.rb new file mode 100644 index 0000000..ba3c97c --- /dev/null +++ b/db/migrate/20230713185223_add_role_to_user.rb @@ -0,0 +1,5 @@ +class AddRoleToUser < ActiveRecord::Migration[7.0] + def change + add_column :users, :role, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1084210..ea67bfb 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.0].define(version: 2023_07_12_200923) do +ActiveRecord::Schema[7.0].define(version: 2023_07_13_185223) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -56,6 +56,11 @@ t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.string "role" t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end