Skip to content
Draft
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
72 changes: 72 additions & 0 deletions app/controllers/mobile/v1/auth_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
class Mobile::V1::AuthController < Mobile::V1::BaseController
skip_before_action :authenticate_mobile_user!, only: [:login]

TOKEN_TTL = 30.days

def login
email = params[:email].to_s.downcase.strip
password = params[:password].to_s

user = User.find_by(email: email)
return login_error!(email: email, user: user, message: "Credenciais inválidas.", status: :unauthorized) if user.blank? || !user.valid_password?(password)
return login_error!(email: email, user: user, message: "Usuário inativo.", status: :forbidden) unless user.active?
return login_error!(email: email, user: user, message: "Acesso da empresa está desativado.", status: :forbidden) unless user.access_enabled?

expires_at = (Time.current + TOKEN_TTL).end_of_day
token = MobileApiSession.issue_for!(user: user, expires_at: expires_at)
Current.user = user
Current.source = "mobile"
Audit::AuthLogger.login_succeeded(user: user)

render json: {
token: token,
token_type: "Bearer",
expires_at: expires_at.iso8601,
user: mobile_user_payload(user)
}, status: :ok
end

def me
mobile_audit(
action: "mobile.api.auth.me.viewed",
metadata: { user_id: current_mobile_user.id }
)

render json: { user: mobile_user_payload(current_mobile_user) }, status: :ok
end

def logout
current_mobile_session&.revoke!
Audit::AuthLogger.logout_succeeded(user: current_mobile_user)
render json: { message: "Logout realizado com sucesso." }, status: :ok
end

def logout_all
current_mobile_user.mobile_api_sessions.active.update_all(revoked_at: Time.current, updated_at: Time.current)
mobile_audit(
action: "mobile.api.auth.logout_all.succeeded",
metadata: { user_id: current_mobile_user.id }
)
render json: { message: "Logout de todos os dispositivos realizado com sucesso." }, status: :ok
end

private

def login_error!(email:, user:, message:, status:)
Audit::AuthLogger.login_failed(email: email, user: user)
render json: { error: message }, status: status
end

def mobile_user_payload(user)
{
id: user.id,
name: user.name,
email: user.email,
role: user.role,
company: {
id: user.company_id,
name: user.company&.name
}
}
end
end
93 changes: 93 additions & 0 deletions app/controllers/mobile/v1/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
class Mobile::V1::BaseController < ActionController::API
before_action :authenticate_mobile_user!
around_action :with_mobile_current_context

attr_reader :current_mobile_session
attr_reader :current_mobile_user

private

def authenticate_mobile_user!
token = bearer_token
return render_unauthorized("Token de acesso não informado.") if token.blank?

session = MobileApiSession.find_active_by_raw_token(token)
return render_unauthorized("Token inválido ou expirado.") if session.blank?

user = session.user
return render_unauthorized("Usuário inativo.") unless user.active?
return render_unauthorized("Acesso da empresa está desativado.") unless user.access_enabled?

@current_mobile_session = session
@current_mobile_user = user
touch_mobile_session_usage
end

def bearer_token
header = request.headers["Authorization"].to_s
return if header.blank?

scheme, token = header.split(" ", 2)
return if scheme.to_s.downcase != "bearer"

token
end

def render_unauthorized(message)
render json: { error: message }, status: :unauthorized
end

def mobile_company
current_mobile_user&.company
end

def mobile_audit(action:, resource: nil, metadata: {}, actor: nil, company: nil)
resolved_actor = actor || current_mobile_user
resolved_company = company || resolved_actor&.company || mobile_company

Audit::Log.call(
action: action,
actor: resolved_actor,
company: resolved_company,
resource: resource,
metadata: metadata
)
end

def pagination_params(default_per: 20, max_per: 100)
page = [params[:page].to_i, 1].max
per = params[:per].to_i
per = default_per if per <= 0
per = max_per if per > max_per
[page, per]
end

def render_paginated(data:, collection:)
render json: {
data: data,
meta: {
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count,
per_page: collection.limit_value
}
}, status: :ok
end

def touch_mobile_session_usage
return if current_mobile_session.blank?

current_mobile_session.update_column(:last_used_at, Time.current)
end

def with_mobile_current_context
Current.user = current_mobile_user
Current.request_id = request.request_id
Current.ip_address = request.remote_ip
Current.user_agent = request.user_agent
Current.source = "mobile"
yield
ensure
Current.reset
end
end
90 changes: 90 additions & 0 deletions app/controllers/mobile/v1/budgets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
class Mobile::V1::BudgetsController < Mobile::V1::BaseController
def index
scope = mobile_company.budgets.includes(:client, :order_service)
scope = scope.where(status: params[:status]) if valid_status_filter?

page, per = pagination_params
budgets = scope.order(created_at: :desc).page(page).per(per)
mobile_audit(
action: "mobile.api.budgets.listed",
metadata: {
status: params[:status],
page: page,
per: per,
count: budgets.size
}
)

render_paginated(
data: budgets.map { |budget| budget_index_payload(budget) },
collection: budgets
)
end

def show
budget = mobile_company.budgets
.includes(:client, :order_service, :service_items)
.find(params[:id])
mobile_audit(
action: "mobile.api.budgets.viewed",
resource: budget,
metadata: { budget_id: budget.id }
)

render json: { data: budget_show_payload(budget) }, status: :ok
end

private

def valid_status_filter?
params[:status].present? && Budget.statuses.key?(params[:status].to_s)
end

def budget_index_payload(budget)
{
id: budget.id,
code: budget.code,
title: budget.title,
status: budget.status,
client_name: budget.client&.name,
valid_until: budget.valid_until&.iso8601,
approval_expires_at: budget.approval_expires_at&.iso8601,
total_value: budget.total_value.to_s
}
end

def budget_show_payload(budget)
{
id: budget.id,
code: budget.code,
title: budget.title,
description: budget.description,
status: budget.status,
valid_until: budget.valid_until&.iso8601,
approval_expires_at: budget.approval_expires_at&.iso8601,
approval_sent_at: budget.approval_sent_at&.iso8601,
approved_at: budget.approved_at&.iso8601,
rejected_at: budget.rejected_at&.iso8601,
rejection_reason: budget.rejection_reason,
estimated_delivery_days: budget.estimated_delivery_days,
total_value: budget.total_value.to_s,
client: {
id: budget.client_id,
name: budget.client&.name
},
order_service: {
id: budget.order_service_id,
code: budget.order_service&.code
},
service_items: budget.service_items.order(:created_at).map do |item|
{
id: item.id,
description: item.description,
quantity: item.quantity,
unit_price: item.unit_price.to_s,
total_price: (item.quantity.to_d * item.unit_price.to_d).to_s
}
end
}
end
end
11 changes: 11 additions & 0 deletions app/controllers/mobile/v1/health_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Mobile::V1::HealthController < Mobile::V1::BaseController
skip_before_action :authenticate_mobile_user!

def show
render json: {
status: "ok",
service: "catalystops-mobile-api",
version: "v1"
}, status: :ok
end
end
93 changes: 93 additions & 0 deletions app/controllers/mobile/v1/order_services_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
class Mobile::V1::OrderServicesController < Mobile::V1::BaseController
def index
scope = mobile_company.order_services.includes(:client, :users)
scope = scope.where(status: params[:status]) if valid_status_filter?

page, per = pagination_params
order_services = scope.order(created_at: :desc).page(page).per(per)
mobile_audit(
action: "mobile.api.order_services.listed",
metadata: {
status: params[:status],
page: page,
per: per,
count: order_services.size
}
)

render_paginated(
data: order_services.map { |order_service| order_service_index_payload(order_service) },
collection: order_services
)
end

def show
order_service = mobile_company.order_services
.includes(:client, :users, :service_items)
.find(params[:id])
mobile_audit(
action: "mobile.api.order_services.viewed",
resource: order_service,
metadata: { order_service_id: order_service.id }
)

render json: { data: order_service_show_payload(order_service) }, status: :ok
end

private

def valid_status_filter?
params[:status].present? && OrderService.statuses.key?(params[:status].to_s)
end

def order_service_index_payload(order_service)
{
id: order_service.id,
code: order_service.code,
title: order_service.title,
status: order_service.status,
client_name: order_service.client&.name,
technicians: order_service.users.map(&:name),
scheduled_at: order_service.scheduled_at&.iso8601,
expected_end_at: order_service.expected_end_at&.iso8601,
total_value: order_service.total_value.to_s
}
end

def order_service_show_payload(order_service)
{
id: order_service.id,
code: order_service.code,
title: order_service.title,
description: order_service.description,
status: order_service.status,
observations: order_service.observations,
rejection_reason: order_service.rejection_reason,
scheduled_at: order_service.scheduled_at&.iso8601,
expected_end_at: order_service.expected_end_at&.iso8601,
started_at: order_service.started_at&.iso8601,
finished_at: order_service.finished_at&.iso8601,
client: {
id: order_service.client_id,
name: order_service.client&.name
},
technicians: order_service.users.map { |user| { id: user.id, name: user.name } },
financial: {
subtotal_value: order_service.subtotal_value.to_s,
discount_type: order_service.discount_type,
discount_value: order_service.discount_value.to_s,
discount_amount: order_service.discount_amount.to_s,
total_value: order_service.total_value.to_s
},
service_items: order_service.service_items.order(:created_at).map do |item|
{
id: item.id,
description: item.description,
quantity: item.quantity,
unit_price: item.unit_price.to_s,
total_price: (item.quantity.to_d * item.unit_price.to_d).to_s
}
end
}
end
end
2 changes: 1 addition & 1 deletion app/models/audit_event.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class AuditEvent < ApplicationRecord
SOURCES = %w[app admin login cliente webhook job system].freeze
SOURCES = %w[app admin login cliente webhook mobile job system].freeze

belongs_to :company, optional: true

Expand Down
Loading
Loading