diff --git a/README.md b/README.md index 316518e..4d3897b 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,8 @@ This is the backend API repository for TurLink. TurLink is a link shortener app "short": "tur.link/4a7c204baeacaf2c", "user_id": 1, "click_count": 1, - "last_click": "2024-08-28T12:34:56.789Z" + "last_click": "2024-08-28T12:34:56.789Z", + "private": false } } } @@ -450,3 +451,47 @@ This is the backend API repository for TurLink. TurLink is a link shortener app - This endpoint currently returns mock data. - In the future, it will provide an actual summary of the content at the given link. - The summary is expected to be a string with numbered points, separated by newline characters. + + ### Update Link Privacy + - **PATCH** `/api/v1/users/:user_id/links/:id/update_privacy` + - Description: Updates the privacy setting of a specific link for a user. + - Request Parameters: + - `user_id`: The ID of the user who owns the link + - `id`: The ID of the link to update + - Request Body: + ```json + { + "private": "true" // or "false" to make the link public + } + ``` + - Example Request: PATCH `https://turlink-be-53ba7254a7c1.herokuapp.com/api/v1/users/1/links/1/update_privacy` + - Successful Response (200 OK): + ```json + { + "message": "Privacy setting updated successfully" + } + ``` + - Error Responses: + - 403 Forbidden (If the user doesn't own the link): + ```json + { + "error": "Unauthorized to update this link" + } + ``` + - 404 Not Found (If the user or link doesn't exist): + ```json + { + "error": "User or Link not found" + } + ``` + - 422 Unprocessable Entity (If the update fails): + ```json + { + "error": "Failed to update privacy setting" + } + ``` + + - Notes: + - Private links are only accessible by their owners. + - Private links are excluded from the top links listing. + - Attempting to access a private link without proper authorization will result in a 404 Not Found error. diff --git a/app/controllers/api/v1/links_controller.rb b/app/controllers/api/v1/links_controller.rb index e62845a..2092d93 100644 --- a/app/controllers/api/v1/links_controller.rb +++ b/app/controllers/api/v1/links_controller.rb @@ -17,7 +17,7 @@ def index def show link = Link.find_by(short: params[:short]) - if link + if link && (!link.private? || (current_user && link.user_id == current_user.id)) link.increment!(:click_count) link.update(last_click: Time.current) render json: LinkSerializer.new(link) @@ -27,7 +27,7 @@ def show end def top_links - query = Link.order(click_count: :desc).limit(5) + query = Link.where(private: false).order(click_count: :desc).limit(5) query = query.joins(:tags).where(tags: { name: params[:tag] }) if params[:tag].present? @@ -36,6 +36,24 @@ def top_links render json: LinkSerializer.new(links) end + def update_privacy + user = User.find(params[:user_id]) + link = Link.find(params[:id]) + is_private = params[:private] == 'true' + + if link.user_id == user.id + if link.update(private: is_private) + render json: { message: 'Privacy setting updated successfully' }, status: :ok + else + render json: { error: 'Failed to update privacy setting' }, status: :unprocessable_entity + end + else + render json: { error: 'Unauthorized to update this link' }, status: :forbidden + end + rescue ActiveRecord::RecordNotFound + render json: { error: 'User or Link not found' }, status: :not_found + end + private def not_found_error diff --git a/app/models/link.rb b/app/models/link.rb index 8bf2ec2..00af7c5 100644 --- a/app/models/link.rb +++ b/app/models/link.rb @@ -6,6 +6,8 @@ class Link < ApplicationRecord validates_presence_of :user_id validates_presence_of :original validates :short, uniqueness: true, presence: true + validates :private, inclusion: { in: [true, false] } + belongs_to :user has_many :link_tags @@ -19,4 +21,8 @@ def self.create_new(user_id, original) def self.create_short_link "tur.link/#{SecureRandom.hex(4)}" end + + def update_privacy(is_private) + update(private: is_private) + end end diff --git a/app/serializers/link_serializer.rb b/app/serializers/link_serializer.rb index d3c3619..0db10d3 100644 --- a/app/serializers/link_serializer.rb +++ b/app/serializers/link_serializer.rb @@ -1,4 +1,4 @@ class LinkSerializer include JSONAPI::Serializer - attributes :original, :short, :user_id, :tags, :click_count, :last_click + attributes :original, :short, :user_id, :tags, :click_count, :last_click, :private end diff --git a/config/routes.rb b/config/routes.rb index 8cab46d..c16a8fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,7 +9,11 @@ namespace :api do namespace :v1 do resources :users, only: %i[create] do - resources :links, only: %i[create index] + resources :links, only: %i[create index] do + member do + patch :update_privacy + end + end end resources :sessions, only: %i[create] resources :links, only: %i[index], action: :show diff --git a/db/migrate/20240911225335_add_private_to_links.rb b/db/migrate/20240911225335_add_private_to_links.rb new file mode 100644 index 0000000..94a694a --- /dev/null +++ b/db/migrate/20240911225335_add_private_to_links.rb @@ -0,0 +1,5 @@ +class AddPrivateToLinks < ActiveRecord::Migration[7.1] + def change + add_column :links, :private, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index e82edf4..9623859 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.1].define(version: 2024_08_30_004510) do +ActiveRecord::Schema[7.1].define(version: 2024_09_11_225335) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -32,6 +32,7 @@ t.integer "click_count", default: 0 t.datetime "last_click" t.boolean "disabled", default: false + t.boolean "private", default: false t.index ["user_id"], name: "index_links_on_user_id" end diff --git a/dump.rdb b/dump.rdb index 2e6596c..6bdd47b 100644 Binary files a/dump.rdb and b/dump.rdb differ diff --git a/spec/models/link_spec.rb b/spec/models/link_spec.rb index db7e8a3..fff5c17 100644 --- a/spec/models/link_spec.rb +++ b/spec/models/link_spec.rb @@ -18,17 +18,43 @@ it { should have_many(:tags).through(:link_tags) } end - describe 'class methods' do + describe 'methods' do describe 'create_new' do it 'can create a new link' do - user1 = User.create(email: "user@example.com", password: "user123") - link = Link.create_new(user1.id, "long-link-example.com") + user1 = User.create(email: 'user@example.com', password: 'user123') + link = Link.create_new(user1.id, 'long-link-example.com') expect(link).to be_a Link expect(link.user_id).to eq(user1.id) - expect(link.original).to eq("long-link-example.com") + expect(link.original).to eq('long-link-example.com') expect(link.short).to be_a String end end + + describe '.create_short_link' do + it 'creates a unique short link' do + short1 = Link.create_short_link + short2 = Link.create_short_link + expect(short1).not_to eq(short2) + end + end + + describe '#update_privacy' do + it 'updates the privacy setting' do + user = User.create!(email: 'test@example.com', password: 'password') + link = Link.create!(user:, original: 'https://example.com', short: 'tur.link/abc123', private: false) + link.update_privacy(true) + expect(link.reload.private).to be true + end + end + + describe 'default scope' do + it 'excludes disabled links' do + user = User.create!(email: 'test@example.com', password: 'password') + Link.create!(user:, original: 'https://example1.com', short: 'tur.link/abc123', disabled: false) + Link.create!(user:, original: 'https://example2.com', short: 'tur.link/def456', disabled: true) + expect(Link.count).to eq(1) + end + end end end diff --git a/spec/requests/api/v1/links_request_spec.rb b/spec/requests/api/v1/links_request_spec.rb index e1314f3..32309f3 100644 --- a/spec/requests/api/v1/links_request_spec.rb +++ b/spec/requests/api/v1/links_request_spec.rb @@ -230,4 +230,52 @@ expect(links.last[:attributes][:click_count]).to eq(50) end end + + describe 'PATCH /users/:user_id/links/:id/update_privacy' do + let(:user) { User.create(email: 'user@example.com', password: 'password') } + let(:link) { Link.create(original: 'https://example.com', short: 'tur.link/abc123', user:) } + + it 'updates the privacy setting of a link' do + patch "/api/v1/users/#{user.id}/links/#{link.id}/update_privacy", params: { private: 'true' } + + expect(response).to be_successful + expect(response.status).to eq(200) + + json_response = JSON.parse(response.body, symbolize_names: true) + expect(json_response[:message]).to eq('Privacy setting updated successfully') + + link.reload + expect(link.private).to be true + end + + it 'returns an error if the user does not own the link' do + other_user = User.create(email: 'other@example.com', password: 'password') + patch "/api/v1/users/#{other_user.id}/links/#{link.id}/update_privacy", params: { private: 'true' } + + expect(response).to have_http_status(:forbidden) + + json_response = JSON.parse(response.body, symbolize_names: true) + expect(json_response[:error]).to eq('Unauthorized to update this link') + end + + it 'returns an error if the link does not exist' do + patch "/api/v1/users/#{user.id}/links/9999/update_privacy", params: { private: 'true' } + + expect(response).to have_http_status(:not_found) + + json_response = JSON.parse(response.body, symbolize_names: true) + expect(json_response[:error]).to eq('User or Link not found') + end + + it 'returns an error if the update fails' do + allow_any_instance_of(Link).to receive(:update).and_return(false) + + patch "/api/v1/users/#{user.id}/links/#{link.id}/update_privacy", params: { private: 'true' } + + expect(response).to have_http_status(:unprocessable_entity) + + json_response = JSON.parse(response.body, symbolize_names: true) + expect(json_response[:error]).to eq('Failed to update privacy setting') + end + end end