Skip to content

Commit 628f290

Browse files
author
tsoganov
committed
Added authentication and api connector service
1 parent 9634b60 commit 628f290

File tree

15 files changed

+332
-33
lines changed

15 files changed

+332
-33
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ TODO
4141

4242
/app/assets/builds/*
4343
!/app/assets/builds/.keep
44+
45+
# Ignore application configuration
46+
/config/application.yml

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ gem "thruster", require: false
4242

4343
gem 'cancancan'
4444
gem 'devise'
45+
gem 'faraday'
46+
gem 'figaro'
4547

4648
gem 'font-awesome-sass', '~> 5.15.1'
4749

Gemfile.lock

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,16 @@ GEM
122122
erubi (1.13.1)
123123
et-orbi (1.2.11)
124124
tzinfo
125+
faraday (2.13.2)
126+
faraday-net_http (>= 2.0, < 3.5)
127+
json
128+
logger
129+
faraday-net_http (3.4.1)
130+
net-http (>= 0.5.0)
125131
ffi (1.17.2-aarch64-linux-gnu)
126132
ffi (1.17.2-x86_64-linux-gnu)
133+
figaro (1.3.0)
134+
thor (>= 0.14.0, < 2)
127135
font-awesome-sass (5.15.1)
128136
sassc (>= 1.11)
129137
fugit (1.11.1)
@@ -151,6 +159,7 @@ GEM
151159
jbuilder (2.13.0)
152160
actionview (>= 5.0.0)
153161
activesupport (>= 5.0.0)
162+
json (2.12.2)
154163
kamal (2.7.0)
155164
activesupport (>= 7.0)
156165
base64 (~> 0.2)
@@ -176,6 +185,8 @@ GEM
176185
mini_mime (1.1.5)
177186
minitest (5.25.5)
178187
msgpack (1.8.0)
188+
net-http (0.6.0)
189+
uri
179190
net-imap (0.5.9)
180191
date
181192
net-protocol
@@ -359,6 +370,8 @@ DEPENDENCIES
359370
dartsass-rails (~> 0.5.1)
360371
debug
361372
devise
373+
faraday
374+
figaro
362375
font-awesome-sass (~> 5.15.1)
363376
importmap-rails
364377
jbuilder

app/controllers/application_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ class ApplicationController < ActionController::Base
1313
protected
1414

1515
def configure_permitted_parameters
16-
added_attrs = [:username, :email, :password, :password_confirmation, :remember_me]
16+
added_attrs = %i[username email password password_confirmation remember_me]
1717
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
18-
devise_parameter_sanitizer.permit :sign_in, keys: [:login, :password]
18+
devise_parameter_sanitizer.permit :sign_in, keys: %i[username password]
1919
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
2020
end
2121
end

app/controllers/users/sessions_controller.rb

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,64 @@
33
class Users::SessionsController < Devise::SessionsController
44
# before_action :configure_sign_in_params, only: [:create]
55

6-
# GET /resource/sign_in
7-
# def new
8-
# super
9-
# end
10-
116
# POST /resource/sign_in
12-
# def create
13-
# super
14-
# end
7+
def create
8+
username = params[:user][:username]
9+
password = params[:user][:password]
1510

16-
# DELETE /resource/sign_out
17-
# def destroy
18-
# super
19-
# end
11+
# Validate input
12+
if username.blank? || password.blank?
13+
flash.now[:alert] = t('devise.failure.invalid', authentication_keys: 'username')
14+
render :new, status: :unauthorized
15+
return
16+
end
17+
18+
user = User.find_by(username: username)
19+
20+
if user&.valid_password?(password) && user&.admin?
21+
sign_in(user)
22+
redirect_after_sign_in(user)
23+
else
24+
api_authenticate_user(username, password)
25+
end
26+
end
2027

2128
# protected
2229

2330
# If you have extra params to permit, append them to the sanitizer.
2431
# def configure_sign_in_params
2532
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
2633
# end
34+
35+
private
36+
37+
def api_authenticate_user(username, password)
38+
auth_service = AuthenticationService.new
39+
response = auth_service.authenticate_user(username, password)
40+
41+
unless response[:success]
42+
flash.now[:alert] = response[:message]
43+
render :new, status: :unauthorized
44+
return
45+
end
46+
47+
user = User.find_or_initialize_by(username: response[:username])
48+
if user.new_record?
49+
user.email = response[:registrar_email]
50+
user.role = 'user'
51+
user.password = password
52+
user.save!
53+
end
54+
55+
sign_in(user)
56+
redirect_after_sign_in(user)
57+
end
58+
59+
def redirect_after_sign_in(user)
60+
if user.admin?
61+
redirect_to admin_dashboard_path, notice: "Welcome back, #{user.username}!"
62+
else
63+
redirect_to root_path, notice: "Welcome back, #{user.username}!"
64+
end
65+
end
2766
end

app/models/user.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ class User < ApplicationRecord
22
# Include default devise modules. Others available are:
33
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
44
devise :database_authenticatable, :registerable,
5-
:recoverable, :rememberable, :validatable
5+
:recoverable, :rememberable, :validatable, :trackable
66

77
validates :username, presence: true, uniqueness: { case_sensitive: false }
88

@@ -13,4 +13,16 @@ class User < ApplicationRecord
1313
def set_default_role
1414
self.role ||= 'user'
1515
end
16+
17+
def first_sign_in?
18+
sign_in_count == 1
19+
end
20+
21+
def last_sign_in_ip_address
22+
last_sign_in_ip
23+
end
24+
25+
def current_sign_in_ip_address
26+
current_sign_in_ip
27+
end
1628
end

app/services/api_connector.rb

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# frozen_string_literal: true
2+
3+
# Base class for API services with HTTP client and error handling
4+
class ApiConnector
5+
# Class-level configuration
6+
class << self
7+
attr_accessor :timeout, :max_retries, :retry_delay, :logging
8+
end
9+
10+
# Default configuration
11+
self.timeout = 10
12+
self.max_retries = 3
13+
self.retry_delay = 1
14+
self.logging = Rails.env.development?
15+
16+
def initialize
17+
@connection = build_connection
18+
end
19+
20+
# Generic method to make API requests with error handling
21+
def make_request(method, url, options = {})
22+
return error_response('API endpoint not configured') unless url
23+
24+
begin
25+
response = @connection.send(method) do |req|
26+
req.url url
27+
req.headers['Content-Type'] = 'application/json'
28+
29+
# Add custom headers
30+
options[:headers]&.each { |key, value| req.headers[key] = value }
31+
32+
# Add body for POST/PUT requests
33+
req.body = options[:body] if options[:body]
34+
end
35+
36+
handle_response(response)
37+
rescue Faraday::TimeoutError => e
38+
handle_timeout_error(e)
39+
rescue Faraday::ConnectionFailed => e
40+
handle_connection_error(e)
41+
rescue Faraday::Error => e
42+
handle_faraday_error(e)
43+
rescue StandardError => e
44+
handle_generic_error(e)
45+
end
46+
end
47+
48+
def handle_response(response)
49+
case response.status
50+
when 200
51+
success_response(response.body)
52+
when 401
53+
error_response('Invalid credentials')
54+
when 403
55+
error_response('Access denied')
56+
when 404
57+
error_response('Service not found')
58+
when 422
59+
error_response('Invalid data')
60+
when 500..599
61+
error_response('Service error')
62+
else
63+
error_response('Unexpected response from service')
64+
end
65+
end
66+
67+
private
68+
69+
def build_connection
70+
Faraday.new do |faraday|
71+
faraday.request :url_encoded
72+
faraday.request :json
73+
faraday.response :json
74+
faraday.adapter Faraday.default_adapter
75+
76+
# Configure timeout
77+
faraday.options.timeout = self.class.timeout
78+
faraday.options.open_timeout = self.class.timeout
79+
80+
# Configure logging if enabled
81+
if self.class.logging
82+
faraday.response :logger, Rails.logger, { headers: false, bodies: false }
83+
end
84+
end
85+
end
86+
87+
def generate_api_token(username, password)
88+
# Generate a token based on username and password
89+
# You can customize this method based on your API requirements
90+
91+
# Get token generation method from configuration
92+
token_method = ENV['API_TOKEN_METHOD'] || 'base64'
93+
94+
case token_method
95+
when 'hmac'
96+
generate_hmac_token(username, password)
97+
when 'base64'
98+
generate_base64_token(username, password)
99+
when 'simple'
100+
generate_simple_token(username, password)
101+
else
102+
generate_base64_token(username, password) # Default to base64
103+
end
104+
end
105+
106+
def generate_hmac_token(username, password)
107+
# Using HMAC for secure token generation
108+
secret_key = ENV['API_SECRET_KEY'] || 'default_secret_key'
109+
timestamp = Time.current.to_i
110+
token_data = "#{username}:#{password}:#{timestamp}"
111+
OpenSSL::HMAC.hexdigest('SHA256', secret_key, token_data)
112+
end
113+
114+
def generate_base64_token(username, password)
115+
# Base64 encoded token
116+
token_data = "#{username}:#{password}"
117+
Base64.strict_encode64(token_data)
118+
end
119+
120+
def generate_simple_token(username, password)
121+
# Simple concatenation (less secure, but simple)
122+
timestamp = Time.current.to_i
123+
"#{username}:#{password}:#{timestamp}"
124+
end
125+
126+
def handle_timeout_error(error)
127+
Rails.logger.error "API Connector timeout: #{error.message}" if self.class.logging
128+
error_response('Service timeout')
129+
end
130+
131+
def handle_connection_error(error)
132+
Rails.logger.error "API Connector connection failed: #{error.message}" if self.class.logging
133+
error_response('Cannot connect to service')
134+
end
135+
136+
def handle_faraday_error(error)
137+
Rails.logger.error "API Connector Faraday error: #{error.message}" if self.class.logging
138+
error_response('Network error')
139+
end
140+
141+
def handle_generic_error(error)
142+
Rails.logger.error "API Connector error: #{error.message}"
143+
error_response('Service temporarily unavailable')
144+
end
145+
146+
def success_response(data)
147+
{
148+
success: true,
149+
data: data,
150+
message: 'Operation successful'
151+
}
152+
end
153+
154+
def error_response(message)
155+
{
156+
success: false,
157+
message: message,
158+
data: nil
159+
}
160+
end
161+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
# Service for handling user authentication via external API
4+
class AuthenticationService < ApiConnector
5+
def initialize
6+
# Use authentication-specific API URL
7+
@api_url = ENV['BASE_URL'] + ENV['AUTH_API_URL']
8+
super
9+
end
10+
11+
# Authenticate user via API using GET request
12+
def authenticate_user(username, password)
13+
# Generate API token from username and password
14+
api_token = generate_api_token(username, password)
15+
headers = { 'Authorization' => "Basic #{api_token}" }
16+
17+
# Use base class make_request method with error handling
18+
result = make_request(:get, @api_url, { headers: headers })
19+
20+
# Handle authentication-specific response processing
21+
if result[:success]
22+
handle_auth_success(result[:data])
23+
else
24+
result
25+
end
26+
end
27+
28+
private
29+
30+
def handle_auth_success(data)
31+
# Check if the response has the expected structure
32+
if data.is_a?(Hash) && data['code'] == 1000
33+
success_auth_response(data['data'])
34+
else
35+
# If not the expected structure, treat as direct data
36+
success_auth_response(data)
37+
end
38+
end
39+
40+
def success_auth_response(data)
41+
{
42+
success: true,
43+
user_id: data['id'],
44+
username: data['username'],
45+
roles: data['roles'],
46+
uuid: data['uuid'],
47+
registrar_name: data['registrar_name'],
48+
registrar_reg_no: data['registrar_reg_no'],
49+
registrar_email: data['registrar_email'],
50+
}
51+
end
52+
end

app/views/common/_header.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<nav class="menu--portal">
44
<a href="https://internet.ee"><span><%= t('.open_portal') %></span></a>
55
<a href="https://auction.internet.ee/"><%= t('.auction_portal') %></a>
6-
<a href="https://registrar.internet.ee" class="active"><span><%= t('.registrar_portal') %></span></a>
6+
<a href="https://registrar.internet.ee"><span><%= t('.registrar_portal') %></span></a>
77
<a href="https://registrant.internet.ee"><span><%= t('.registrant_portal') %></span></a>
88
</nav>
99
<%= render 'common/menu_language' %>

0 commit comments

Comments
 (0)