diff --git a/Gemfile b/Gemfile index e51b9d5..0dc8661 100644 --- a/Gemfile +++ b/Gemfile @@ -52,6 +52,7 @@ group :development do gem 'bullet' gem 'dotenv-rails' + end gem 'rack-cors' diff --git a/Gemfile.lock b/Gemfile.lock index 1817669..1c26195 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -186,14 +186,10 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) - nokogiri (1.16.7-arm64-darwin) - racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.16.7-x64-mingw-ucrt) racc (~> 1.4) orm_adapter (0.5.0) - pg (1.5.8) + pg (1.5.8-x64-mingw-ucrt) psych (5.1.2) stringio puma (6.4.3) @@ -259,9 +255,7 @@ GEM skinny (0.2.2) eventmachine (~> 1.0) thin - sqlite3 (2.1.0-arm64-darwin) - sqlite3 (2.1.0-x86_64-darwin) - sqlite3 (2.1.0-x86_64-linux-gnu) + sqlite3 (2.1.0-x64-mingw-ucrt) sqlite3-ruby (1.3.3) sqlite3 (>= 1.3.3) stringio (3.1.1) @@ -275,6 +269,8 @@ GEM timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + tzinfo-data (1.2024.2) + tzinfo (>= 1.0.0) uniform_notifier (1.16.0) warden (1.2.9) rack (>= 2.0.9) @@ -295,10 +291,7 @@ GEM zeitwerk (2.7.1) PLATFORMS - arm64-darwin-22 - x86_64-darwin-19 - x86_64-darwin-21 - x86_64-linux + x64-mingw-ucrt DEPENDENCIES bootsnap @@ -322,7 +315,7 @@ DEPENDENCIES web-console RUBY VERSION - ruby 3.3.2p78 + ruby 3.3.2p79 BUNDLED WITH 2.5.11 diff --git a/app/controllers/api/v1/patient_profiles_controller.rb b/app/controllers/api/v1/patient_profiles_controller.rb new file mode 100644 index 0000000..a52a384 --- /dev/null +++ b/app/controllers/api/v1/patient_profiles_controller.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true +module Api + module V1 + class PatientProfilesController < ApplicationController + before_action :sanitize_request_path + skip_before_action :authenticate_user!, only: [:index] + + # POST /patient_profiles + def create + ActiveRecord::Base.transaction do + address_params = params.require(:address).permit(:address_line_1, :address_line_2, :city, :state, :country, :pincode) + address = Address.create!(address_params) + + establishment_defaults = { + name: "Default Hospital", + latitude: 0.0, + longitude: 0.0, + maps_location: "https://maps.example.com/default_location" + } + + establishment_params = params.fetch(:establishment, {}).permit(:name, :latitude, :longitude, :maps_location).to_h + establishment = Establishment.create!( + establishment_defaults.merge(establishment_params).merge(address: address) + ) + + patient_params = params.require(:patient_profile).permit(:name, :gender, :dob, :blood_group) + patient_profile = PatientProfile.create!( + patient_params.merge(address: address, establishment: establishment, user_id: current_user.id) + ) + + current_user.update!(patient_profile_id: patient_profile.id) + + render json: { + code: 201, + message: 'Patient profile created successfully.', + patient_profile: patient_profile, + }, status: :created + end + rescue ActiveRecord::RecordInvalid => e + render json: { error: e.message }, status: :unprocessable_entity + rescue StandardError => e + render json: { error: "An unexpected error occurred: #{e.message}" }, status: :internal_server_error + end + + # GET /all patient_profiles + def index + + patient_profiles = PatientProfile.includes(:address, :user) + + patient_profiles = patient_profiles.where("name ILIKE ?", "%#{params[:name]}%") if params[:name].present? + patient_profiles = patient_profiles.joins(:address).where("addresses.city ILIKE ?", "%#{params[:city]}%") if params[:city].present? + patient_profiles = patient_profiles.joins(:address).where("addresses.state ILIKE ?", "%#{params[:state]}%") if params[:state].present? + patient_profiles = patient_profiles.joins(:address).where("addresses.country ILIKE ?", "%#{params[:country]}%") if params[:country].present? + patient_profiles = patient_profiles.joins(:address).where("addresses.pincode = ?", params[:pincode]) if params[:pincode].present? + patient_profiles = patient_profiles.joins(:user).where(users: { patient_profile_id: params[:patient_profile_id] }) if params[:patient_profile_id].present? + + if patient_profiles.empty? + render json: { + code: 404, + message: 'No patient profiles found for the given filters.' + }, status: :not_found + else + + render json: { + code: 200, + message: 'Patient profiles retrieved successfully.', + data: patient_profiles.as_json( + include: { + address: {}, + user: { only: [:id, :email, :patient_profile_id] } + } + ) + }, status: :ok + end + end + + + # GET /patient_profiles/:user_id + + def show + + user = current_user + + patient_profile = PatientProfile.includes(:address, :user).find_by(id: params[:id], user_id: user.id) + + if patient_profile + render json: { + code: 200, + message: 'Patient profile retrieved successfully.', + data: patient_profile.as_json(include: { + address: { only: [:id, :address_line_1, :city, :state, :country, :pincode] }, + user: { only: [:id, :email, :patient_profile_id] } + }) + }, status: :ok + else + render json: { + code: 404, + message: 'Patient profile not found or you are not authorized to view it.' + }, status: :not_found + end + end + + # DELETE /patient_profiles/:user_id + def destroy + + user = current_user + + patient_profile = PatientProfile.find_by(id: params[:id], user_id: user.id) + + if patient_profile + + patient_profile.destroy + + render json: { + code: 200, + message: 'Patient profile deleted successfully.' + }, status: :ok + else + + render json: { + code: 404, + message: 'Patient profile not found or you are not authorized to delete it.' + }, status: :not_found + end + end + + # PATCH/PUT /patient_profiles/:id + def update + + user = current_user + + patient_profile = PatientProfile.find_by(id: params[:id], user_id: user.id) + + if patient_profile.nil? + render json: { + code: 404, + message: 'Patient profile not found or you are not authorized to update it.' + }, status: :not_found + return + end + + if patient_profile.update(patient_profile_params) && patient_profile.address.update(address_params) + render json: { + code: 200, + message: 'Patient profile updated successfully.', + data: patient_profile.as_json(include: :address) + }, status: :ok + else + render json: { + code: 422, + message: 'Failed to update patient profile.', + errors: patient_profile.errors.full_messages + patient_profile.address.errors.full_messages + }, status: :unprocessable_entity + end + end + + private + + def address_params + params.require(:address).permit( + :address_line_1, + :address_line_2, + :city, + :state, + :country, + :pincode + ) + end + def sanitize_request_path + request.path.gsub!(/\s+/, '') + end + def patient_profile_params + params.require(:patient_profile).permit(:name, :gender, :dob, :blood_group) + end + + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ae4c524..e6d4773 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,14 +1,27 @@ # frozen_string_literal: true class ApplicationController < ActionController::API + + include Devise::Controllers::Helpers + + before_action :authenticate_user! + before_action :configure_permitted_parameters, if: :devise_controller? + + rescue_from Devise::MissingWarden do + render json: { error: 'User not authenticated' }, status: :unauthorized + end + protected - def configure_permitted_parameters - devise_parameter_sanitizer.permit(:sign_up) do |user_params| - user_params.permit(:email, :password, :password_confirmation, :user_type) + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up) do |user_params| + user_params.permit(:email, :password, :password_confirmation, :user_type) + end + def authenticate_user! + super + rescue Devise::MissingWarden + render json: { error: 'User not authenticated' }, status: :unauthorized end - end - end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index e3c6f04..cbc3470 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,37 +1,43 @@ # frozen_string_literal: true + class Users::RegistrationsController < Devise::RegistrationsController include RackSessionFix respond_to :json def create - user = User.new( - email: params[:user][:email], - password: params[:user][:password], - password_confirmation: params[:user][:password_confirmation] - ) - user_type = params[:user][:user_type] - user.create_empty_profile!(user_type) - user.save - respond_with user + @user = User.new(user_params) + + if @user.save + respond_with(@user) + else + render_error(@user.errors.full_messages.to_sentence) + end end private + + def user_params + params.require(:user).permit(:email, :password, :password_confirmation, :user_type) + end + + def render_error(message, status = :unprocessable_entity) + render json: { message: "User could not be created successfully. #{message}" }, status: status + end + def respond_with(resource, _opts = {}) if request.method == 'POST' && resource.persisted? render json: { - code: 200, + code: 201, message: 'Signed up successfully.', data: UserSerializer.new(resource).serializable_hash[:data][:attributes] - }, status: :ok + }, status: :created elsif request.method == 'DELETE' render json: { code: 200, message: 'Account deleted successfully.' }, status: :ok else - render json: { - message: "User could not be created successfully. #{resource.errors.full_messages.to_sentence}" - }, status: :unprocessable_entity + render_error(resource.errors.full_messages.to_sentence) end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index f3e79ad..b6d1e9d 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -7,10 +7,12 @@ class Users::SessionsController < Devise::SessionsController private def respond_with(resource, _opts = {}) + token = Warden::JWTAuth::UserEncoder.new.call(resource, :user, nil)[0] render json: { code: 200, message: 'Logged in successfully.', - data: UserSerializer.new(resource).serializable_hash[:data][:attributes] + data: UserSerializer.new(resource).serializable_hash[:data][:attributes], + token: token }, status: :ok end diff --git a/app/models/address.rb b/app/models/address.rb index a19addc..6b8208a 100644 --- a/app/models/address.rb +++ b/app/models/address.rb @@ -1,2 +1,7 @@ class Address < ApplicationRecord + has_many :patient_profiles, dependent: :destroy + + # Validations + validates :address_line_1, :city, :state, :country, :pincode, presence: true + validates :pincode, numericality: { only_integer: true } end diff --git a/app/models/establishment.rb b/app/models/establishment.rb index a8fc9a0..883f2e3 100644 --- a/app/models/establishment.rb +++ b/app/models/establishment.rb @@ -1,2 +1,4 @@ class Establishment < ApplicationRecord + belongs_to :address + has_many :patient_profiles, dependent: :destroy end diff --git a/app/models/patient_profile.rb b/app/models/patient_profile.rb index a98b3f7..8ab36cd 100644 --- a/app/models/patient_profile.rb +++ b/app/models/patient_profile.rb @@ -1,4 +1,22 @@ class PatientProfile < ApplicationRecord belongs_to :address, optional: true - has_one :user + belongs_to :establishment, optional: true + belongs_to :user, optional: true + + + # Validations for PatientProfile fields + validates :name, presence: true, length: { minimum: 1, message: "cannot be blank" } + validates :gender, inclusion: { in: %w[male female other], message: "must be male, female, or other" }, allow_blank: true + validates :blood_group, inclusion: { in: %w[A+ A- B+ B- AB+ AB- O+ O-], message: "is invalid" }, allow_blank: true + validate :dob_in_the_past, if: -> { dob.present? } + + + # Custom validation for date_of_birth + private + + def dob_in_the_past + if dob > Date.today + errors.add(:dob, 'must be in the past') + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 20ffc7c..e2759ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,19 +1,17 @@ class User < ApplicationRecord - belongs_to :patient_profile, optional: true - belongs_to :doctor_profile, optional: true + has_one :patient_profile + has_one :doctor_profile include Devise::JWT::RevocationStrategies::JTIMatcher - # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + + # Devise modules devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable, :confirmable, :pwned_password, - :jwt_authenticatable, jwt_revocation_strategy: self + :recoverable, :rememberable, :validatable, :confirmable, + :pwned_password, :jwt_authenticatable, jwt_revocation_strategy: self + + VALID_USER_TYPES = %w[patient doctor].freeze + + # Validations + validates :user_type, presence: true, inclusion: { in: VALID_USER_TYPES, message: "must be 'patient' or 'doctor'" } - def create_empty_profile!(user_type) - if user_type == 'patient' - create_patient_profile! - elsif user_type == 'doctor' - create_doctor_profile! - end - end end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 57fc66c..84e31cc 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -1,4 +1,4 @@ class UserSerializer include JSONAPI::Serializer - attributes :id, :email, :created_at + attributes :id, :email, :created_at, :user_type end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 4baedbb..49f0821 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -LspezHxBXYYDjpJT6leazJwF4EBpyRhnCx7dMpAFB1/Zs/gxpLSkWJLwUXqix1VSfvGf0MQ2mdlhlSpL/eawUhSaB08jEcnk1QZVb8FrOXhdL/0YzxAxSwkOfaVQUh+cbFjPWnLkjCVVzb1rJ7U8hJ1F127ELhAz7z0vdpV07aNyc3QEjujZSL412DGe0mJDttEqMvv7OKXEkLtmlZX3/LDtM8W8M/6BjJmTFHUs/2TuarmFv9InJTtVamgI1Owhp52suRDKNw7BAInBbcCZheC0vKvptUCfxUFd2a2yWqD5W3RATzKmy5v03O8vLRCmzbq8fiq87K2bYQVnw0DLja80TDt/U5Q4lLVvMUnVBRpqeW+hZKSlI2R4aVZysPBIdttsetWvwBfGtVKrHN29w/d+9lfq--qC55RquQklVlF31K--DbXL0CO115WRkLbEhR5tSQ== \ No newline at end of file +y9jicnEiK/SWh89YIY7joIRVUxoLM5iDVODY6A3eash/0bypAO7tvLDmNtVwE2MpYwoG0ga9jcnQnIQhoCmvWSX5K/ZKex0qEGR+r6PUe0Ib0r81A2LmtAMaBrRU0UoGyy+y3FdnZpoaUiQAx0uJs9pbReLgoD/BqDAmOtqVMIAXYfs559zbRzcfwWW7+LdqrO/yxeIrrLYzI6AV+iuFUbGh2mDJ2lEsUXVfJ6eGRolMoTSr53knS3yTFYetU5K5urIAu8ITW2uDE0Cik4ZptuVQQpr8rJuHHFtbZZoIzESB5J/meLtQi0nzkpVKX5BRTsUYPKQfX9WAgz9otHYZ24V//C2ir0NFmPn/w5ukQI4sDYuqZNwBn4tfnkwRxq4S0d3CCPkduoCdc5A4zOOMVRy8PrFZ--WgA40i5gBUNysw9B--tWd+CXWQYp0Seo0TvzCBPA== \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f25e2bf..4114613 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -315,12 +315,17 @@ config.jwt do |jwt| jwt.secret = Rails.application.credentials.fetch(:secret_key_base) + + # Define the dispatch and revocation requests as before jwt.dispatch_requests = [ ['POST', %r{^/login$}] ] jwt.revocation_requests = [ ['DELETE', %r{^/logout$}] ] + + # Set the expiration time for the token jwt.expiration_time = 1.month.to_i + end end diff --git a/config/routes.rb b/config/routes.rb index 7703399..77bbae0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,4 +15,11 @@ # Defines the root path route ("/") # root "posts#index" + namespace :api do + namespace :v1 do + resources :patient_profiles, only: [:index, :create, :destroy, :update] do + get ':id', on: :collection, action: :show, as: :show_by_id + end + end + end end diff --git a/db/migrate/20241114133344_add_foreign_keys_to_users.rb b/db/migrate/20241114133344_add_foreign_keys_to_users.rb new file mode 100644 index 0000000..4e33d0d --- /dev/null +++ b/db/migrate/20241114133344_add_foreign_keys_to_users.rb @@ -0,0 +1,11 @@ +class AddForeignKeysToUsers < ActiveRecord::Migration[6.1] + def change + unless foreign_key_exists?(:users, :patient_profiles, column: :patient_profile_id) + add_foreign_key :users, :patient_profiles, column: :patient_profile_id, on_delete: :nullify + end + + unless foreign_key_exists?(:users, :doctor_profiles, column: :doctor_profile_id) + add_foreign_key :users, :doctor_profiles, column: :doctor_profile_id, on_delete: :nullify + end + end +end diff --git a/db/migrate/20241121134445_rename_pin_code_to_pincode_in_addresses.rb b/db/migrate/20241121134445_rename_pin_code_to_pincode_in_addresses.rb new file mode 100644 index 0000000..01b71a1 --- /dev/null +++ b/db/migrate/20241121134445_rename_pin_code_to_pincode_in_addresses.rb @@ -0,0 +1,5 @@ +class RenamePinCodeToPincodeInAddresses < ActiveRecord::Migration[7.1] + def change + rename_column :addresses, :pin_code, :pincode + end +end diff --git a/db/migrate/20241121144125_add_user_id_to_patient_profiles.rb b/db/migrate/20241121144125_add_user_id_to_patient_profiles.rb new file mode 100644 index 0000000..c1852a7 --- /dev/null +++ b/db/migrate/20241121144125_add_user_id_to_patient_profiles.rb @@ -0,0 +1,6 @@ +class AddUserIdToPatientProfiles < ActiveRecord::Migration[7.1] + def change + add_column :patient_profiles, :user_id, :integer + add_index :patient_profiles, :user_id + end +end diff --git a/db/migrate/20241202143035_add_user_type_to_users.rb b/db/migrate/20241202143035_add_user_type_to_users.rb new file mode 100644 index 0000000..eb768e2 --- /dev/null +++ b/db/migrate/20241202143035_add_user_type_to_users.rb @@ -0,0 +1,5 @@ +class AddUserTypeToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :user_type, :string + end +end diff --git a/db/migrate/20241206134104_rename_date_of_birth_to_dob_in_patient_profiles.rb b/db/migrate/20241206134104_rename_date_of_birth_to_dob_in_patient_profiles.rb new file mode 100644 index 0000000..0e39df8 --- /dev/null +++ b/db/migrate/20241206134104_rename_date_of_birth_to_dob_in_patient_profiles.rb @@ -0,0 +1,5 @@ +class RenameDateOfBirthToDobInPatientProfiles < ActiveRecord::Migration[7.1] + def change + rename_column :patient_profiles, :date_of_birth, :dob + end +end diff --git a/db/migrate/20241208191724_add_establishment_id_to_patient_profiles.rb b/db/migrate/20241208191724_add_establishment_id_to_patient_profiles.rb new file mode 100644 index 0000000..e7db66f --- /dev/null +++ b/db/migrate/20241208191724_add_establishment_id_to_patient_profiles.rb @@ -0,0 +1,7 @@ +class AddEstablishmentIdToPatientProfiles < ActiveRecord::Migration[7.1] + def change + add_column :patient_profiles, :establishment_id, :bigint + add_foreign_key :patient_profiles, :establishments + add_index :patient_profiles, :establishment_id + end +end diff --git a/db/schema.rb b/db/schema.rb index b812df4..622c7c8 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_11_06_140928) do +ActiveRecord::Schema[7.1].define(version: 2024_12_08_191724) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -20,7 +20,7 @@ t.string "city" t.string "state" t.string "country" - t.string "pin_code" + t.string "pincode" t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -76,8 +76,6 @@ t.string "maps_location" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "city" - t.string "address" t.index ["address_id"], name: "index_establishments_on_address_id" end @@ -91,12 +89,16 @@ create_table "patient_profiles", force: :cascade do |t| t.string "name" t.string "gender" - t.datetime "date_of_birth" + t.datetime "dob" t.string "blood_group" t.bigint "address_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "user_id" + t.bigint "establishment_id" t.index ["address_id"], name: "index_patient_profiles_on_address_id" + t.index ["establishment_id"], name: "index_patient_profiles_on_establishment_id" + t.index ["user_id"], name: "index_patient_profiles_on_user_id" end create_table "payments", force: :cascade do |t| @@ -164,6 +166,7 @@ t.string "jti", null: false t.bigint "patient_profile_id" t.bigint "doctor_profile_id" + t.string "user_type" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["doctor_profile_id"], name: "index_users_on_doctor_profile_id" t.index ["email"], name: "index_users_on_email", unique: true @@ -181,6 +184,8 @@ add_foreign_key "doctor_profiles", "specializations" add_foreign_key "establishments", "addresses" add_foreign_key "patient_profiles", "addresses" + add_foreign_key "patient_profiles", "establishments" + add_foreign_key "payments", "appointments" add_foreign_key "reviews", "doctor_profiles" add_foreign_key "reviews", "patient_profiles" add_foreign_key "services", "doctor_profiles"