Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addresses API with HTTP Basic Authentication and Permissions #1023

Merged
merged 2 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions app/controllers/api/v1/addresses_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
module Api
module V1
class AddressesController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_api_user
before_action :authorize_api_user
before_action :set_address, only: [:show, :update, :destroy]

# GET /api/v1/addresses
def index
addresses = Address.includes(:country, :geo_state).all
render json: addresses.as_json(include: {
country: { only: [:id, :name] },
geo_state: { only: [:id, :name] }
}), status: :ok
end

# GET /api/v1/addresses/:id
def show
render json: @address.as_json(include: {
country: { only: [:id, :name] },
geo_state: { only: [:id, :name] }
}), status: :ok
end

# POST /api/v1/addresses
def create
address = Address.new(address_params)
if address.save
render json: address.as_json(include: {
country: { only: [:id, :name] },
geo_state: { only: [:id, :name] }
}), status: :created
else
render json: { errors: address.errors.full_messages }, status: :unprocessable_entity
end
end

# PUT /api/v1/addresses/:id
def update
if @address.update(address_params)
render json: @address.as_json(include: {
country: { only: [:id, :name] },
geo_state: { only: [:id, :name] }
}), status: :ok
else
render json: { errors: @address.errors.full_messages }, status: :unprocessable_entity
end
end

# DELETE /api/v1/addresses/:id
def destroy
@address.destroy
head :no_content
end

private

def authenticate_api_user
authenticate_or_request_with_http_basic do |username, password|
@current_api_user = ApiUser.find_by(username: username)&.authenticate(password)
end
end

def authorize_api_user
unless @current_api_user&.api_permissions&.exists?(endpoint_name: 'addresses')
render json: { error: 'Forbidden' }, status: :forbidden
end
end

def set_address
@address = Address.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Address not found' }, status: :not_found
end

def address_params
params.require(:address).permit(
:street_address, :city, :state_id, :postal_code,
:country_id, :geo_lat, :geo_long, :geo_labelx, :geo_labely
)
end
end
end
end
12 changes: 12 additions & 0 deletions app/models/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@ class Address < ApplicationRecord
belongs_to :geo_state, optional: true, foreign_key: 'state_id'

validates :street_address, :country_id, :geo_lat, :geo_long, presence: true
validates :geo_lat, :geo_long, :geo_labelx, :geo_labely, numericality: true
validates :state_id, presence: true, if: -> { GeoState.exists?(state_id) }
validate :state_must_exist
validate :country_must_exist

has_many :divisions
has_many :organizations

def state_must_exist
errors.add(:state_id, "must exist as a GeoState") unless GeoState.exists?(state_id)
end

def country_must_exist
errors.add(:country_id, "must exist as a Country") unless Country.exists?(country_id)
end
end
5 changes: 5 additions & 0 deletions app/models/api_permission.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ApiPermission < ApplicationRecord
belongs_to :api_user

validates :endpoint_name, presence: true
end
7 changes: 7 additions & 0 deletions app/models/api_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ApiUser < ApplicationRecord
has_secure_password

has_many :api_permissions, dependent: :destroy

validates :username, presence: true, uniqueness: true
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace :v1 do
resources :divisions, only: [:index]
resources :loans, only: [:index]
resources :addresses
end
end

Expand Down
19 changes: 19 additions & 0 deletions db/migrate/20241120202216_create_api_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class CreateApiUsers < ActiveRecord::Migration[6.1]
def change
create_table :api_users do |t|
t.string :username, null: false, unique: true
t.string :password_digest, null: false

t.timestamps
end

add_index :api_users, :username, unique: true

ApiUser.find_or_create_by(id: 1) do |api_user|
api_user.username = 'madeline'
api_user.password = 'k2j3j10pp'
api_user.password_confirmation = 'k2j3j10pp'
end

end
end
18 changes: 18 additions & 0 deletions db/migrate/20241120234518_create_api_permissions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class CreateApiPermissions < ActiveRecord::Migration[6.1]
def change
create_table :api_permissions do |t|
t.references :api_user, null: false, foreign_key: true
t.string :endpoint_name, null: false

t.timestamps
end

add_index :api_permissions, [:api_user_id, :endpoint_name], unique: true

ApiPermission.find_or_create_by(id: 1) do |permission|
permission.api_user_id = 1
permission.endpoint_name = 'addresses'
end

end
end
20 changes: 19 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2024_05_20_210320) do
ActiveRecord::Schema.define(version: 2024_11_20_234518) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -137,6 +137,23 @@
t.datetime "updated_at", precision: 6, null: false
end

create_table "api_permissions", force: :cascade do |t|
t.bigint "api_user_id", null: false
t.datetime "created_at", precision: 6, null: false
t.string "endpoint_name", null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["api_user_id", "endpoint_name"], name: "index_api_permissions_on_api_user_id_and_endpoint_name", unique: true
t.index ["api_user_id"], name: "index_api_permissions_on_api_user_id"
end

create_table "api_users", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.string "password_digest", null: false
t.datetime "updated_at", precision: 6, null: false
t.string "username", null: false
t.index ["username"], name: "index_api_users_on_username", unique: true
end

create_table "countries", id: :serial, force: :cascade do |t|
t.datetime "created_at", null: false
t.integer "default_currency_id", null: false
Expand Down Expand Up @@ -566,6 +583,7 @@
add_foreign_key "accounting_transactions", "projects"
add_foreign_key "addresses", "countries"
add_foreign_key "addresses", "geo_states", column: "state_id"
add_foreign_key "api_permissions", "api_users"
add_foreign_key "countries", "currencies", column: "default_currency_id"
add_foreign_key "data_exports", "divisions"
add_foreign_key "divisions", "accounting_accounts", column: "interest_income_account_id"
Expand Down
Loading