dscf-core 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/dscf/core/authenticatable.rb +81 -0
- data/app/controllers/concerns/dscf/core/common.rb +200 -0
- data/app/controllers/concerns/dscf/core/filterable.rb +12 -0
- data/app/controllers/concerns/dscf/core/json_response.rb +66 -0
- data/app/controllers/concerns/dscf/core/pagination.rb +71 -0
- data/app/controllers/concerns/dscf/core/token_authenticatable.rb +53 -0
- data/app/controllers/dscf/core/application_controller.rb +19 -0
- data/app/controllers/dscf/core/auth_controller.rb +120 -0
- data/app/errors/dscf/core/authentication_error.rb +19 -0
- data/app/models/concerns/dscf/core/user_authenticatable.rb +58 -0
- data/app/models/dscf/core/refresh_token.rb +36 -0
- data/app/models/dscf/core/user.rb +11 -4
- data/app/models/dscf/core/user_profile.rb +19 -0
- data/app/services/dscf/core/auth_service.rb +75 -0
- data/app/services/dscf/core/token_service.rb +55 -0
- data/config/initializers/jwt.rb +23 -0
- data/config/initializers/ransack.rb +4 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20250822092031_create_dscf_core_user_profiles.rb +3 -3
- data/db/migrate/20250824114725_create_dscf_core_refresh_tokens.rb +15 -0
- data/db/migrate/20250824191957_change_pep_status_to_integer_in_user_profiles.rb +5 -0
- data/db/migrate/20250824200927_make_email_and_phone_optional_for_users.rb +6 -0
- data/db/migrate/20250825192113_add_defaults_to_user_profiles.rb +6 -0
- data/lib/dscf/core/version.rb +1 -1
- data/lib/dscf/core.rb +2 -0
- data/spec/factories/dscf/core/refresh_tokens.rb +25 -0
- data/spec/factories/dscf/core/user_profiles.rb +4 -4
- metadata +149 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4107923adddc9fd29ff5ac9526119693b1fce5d4aabc9a401bcc0f38a1b97c2
|
4
|
+
data.tar.gz: 4f23eecacbcb23c3cf40adbb0ed1dd467f48352b97aa7e6f9c6b51592d278719
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c11ba4c15b33280800732598d62e028269ce3ad4eecf8aa642c5d9825df338ff3cc9f0266697459914e44a9773fba326c876fec3347a1808d04ff02925fcd07
|
7
|
+
data.tar.gz: a3806b08908d24011e7390a4cd0e134f4f18d728029cebf4cfc16cf2fcc1512358a38422b6fd755f01a0a220f3c0c00b0fffacc1769a659ef699c5c4fae836e6
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module Authenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :authenticate_user, if: :authentication_required?
|
8
|
+
rescue_from AuthenticationError, with: :handle_authentication_error
|
9
|
+
end
|
10
|
+
|
11
|
+
def current_user
|
12
|
+
@current_user ||= authenticate_from_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def authenticate_user!
|
16
|
+
raise AuthenticationError, "Authentication required" unless current_user
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticate_user
|
20
|
+
authenticate_user! if authentication_required?
|
21
|
+
end
|
22
|
+
|
23
|
+
def sign_in(user, request)
|
24
|
+
tokens = user.generate_auth_tokens(request)
|
25
|
+
@current_user = user
|
26
|
+
tokens
|
27
|
+
end
|
28
|
+
|
29
|
+
def sign_out
|
30
|
+
# Revoke the current refresh token if available
|
31
|
+
refresh_token_value = extract_refresh_token_from_params
|
32
|
+
AuthService.revoke_refresh_token(refresh_token_value) if refresh_token_value
|
33
|
+
@current_user = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def refresh_token
|
37
|
+
refresh_token_value = extract_refresh_token_from_params
|
38
|
+
return nil unless refresh_token_value
|
39
|
+
|
40
|
+
begin
|
41
|
+
AuthService.refresh_access_token(refresh_token_value, request)
|
42
|
+
rescue AuthenticationError
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def authenticate_from_token
|
50
|
+
access_token = extract_access_token_from_header
|
51
|
+
return nil unless access_token
|
52
|
+
|
53
|
+
payload = TokenService.decode(access_token)
|
54
|
+
return nil unless payload && payload["type"] == "access"
|
55
|
+
|
56
|
+
User.find_by(id: payload["user_id"])
|
57
|
+
rescue AuthenticationError
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_access_token_from_header
|
62
|
+
auth_header = request.headers["Authorization"]
|
63
|
+
return nil unless auth_header&.start_with?("Bearer ")
|
64
|
+
|
65
|
+
auth_header.split(" ").last
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_refresh_token_from_params
|
69
|
+
params[:refresh_token]
|
70
|
+
end
|
71
|
+
|
72
|
+
def authentication_required?
|
73
|
+
true # Override in specific controllers if needed
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_authentication_error(error)
|
77
|
+
render json: error.to_hash, status: error.status_code
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module Common
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Dscf::Core::Pagination
|
6
|
+
include Dscf::Core::JsonResponse
|
7
|
+
include Dscf::Core::Filterable
|
8
|
+
|
9
|
+
included do
|
10
|
+
before_action :set_clazz
|
11
|
+
before_action :set_object, only: %i[show update]
|
12
|
+
end
|
13
|
+
|
14
|
+
def index
|
15
|
+
data = nil
|
16
|
+
options = {}
|
17
|
+
if block_given?
|
18
|
+
incoming = yield
|
19
|
+
if incoming.instance_of?(Array)
|
20
|
+
data, options = incoming
|
21
|
+
elsif incoming.instance_of?(Hash)
|
22
|
+
options = incoming
|
23
|
+
else
|
24
|
+
data = incoming
|
25
|
+
end
|
26
|
+
else
|
27
|
+
data = @clazz.all
|
28
|
+
end
|
29
|
+
|
30
|
+
# Apply eager loading if defined
|
31
|
+
data = data.includes(eager_loaded_associations) if eager_loaded_associations.present?
|
32
|
+
|
33
|
+
# Apply Ransack filtering
|
34
|
+
data = filter_records(data)
|
35
|
+
|
36
|
+
# Get total count before pagination
|
37
|
+
total_count = data.count if params[:page]
|
38
|
+
|
39
|
+
data = data.then(&paginate) if params[:page]
|
40
|
+
|
41
|
+
# Add action specific serializer includes
|
42
|
+
includes = serializer_includes_for_action(:index)
|
43
|
+
options[:include] = includes if includes.present?
|
44
|
+
|
45
|
+
# Add pagination metadata if paginated
|
46
|
+
if params[:page]
|
47
|
+
total_pages = (total_count.to_f / per_page).ceil
|
48
|
+
count = data.is_a?(Array) ? data.length : data.count
|
49
|
+
|
50
|
+
options[:pagination] = {
|
51
|
+
current_page: page_no,
|
52
|
+
per_page: per_page,
|
53
|
+
count: count,
|
54
|
+
total_count: total_count,
|
55
|
+
total_pages: total_pages,
|
56
|
+
links: pagination_links(total_pages)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
render_success(data: data, serializer_options: options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def show
|
64
|
+
data = nil
|
65
|
+
options = {}
|
66
|
+
if block_given?
|
67
|
+
incoming = yield
|
68
|
+
if incoming.instance_of?(Array)
|
69
|
+
data, options = incoming
|
70
|
+
elsif incoming.instance_of?(Hash)
|
71
|
+
data = @obj
|
72
|
+
options = incoming
|
73
|
+
else
|
74
|
+
data = incoming
|
75
|
+
end
|
76
|
+
else
|
77
|
+
data = @obj
|
78
|
+
end
|
79
|
+
|
80
|
+
data = @clazz.includes(eager_loaded_associations).find(params[:id]) if data.is_a?(@clazz) && eager_loaded_associations.present?
|
81
|
+
|
82
|
+
includes = serializer_includes_for_action(:show)
|
83
|
+
options[:include] = includes if includes.present?
|
84
|
+
|
85
|
+
render_success(data: data, serializer_options: options)
|
86
|
+
end
|
87
|
+
|
88
|
+
def create
|
89
|
+
obj = nil
|
90
|
+
options = {}
|
91
|
+
if block_given?
|
92
|
+
incoming = yield
|
93
|
+
if incoming.instance_of?(Array)
|
94
|
+
obj, options = incoming
|
95
|
+
elsif incoming.instance_of?(Hash)
|
96
|
+
obj = @clazz.new(model_params)
|
97
|
+
options = incoming
|
98
|
+
else
|
99
|
+
obj = incoming
|
100
|
+
end
|
101
|
+
else
|
102
|
+
obj = @clazz.new(model_params)
|
103
|
+
end
|
104
|
+
|
105
|
+
if obj.save
|
106
|
+
obj = @clazz.includes(eager_loaded_associations).find(obj.id) if eager_loaded_associations.present?
|
107
|
+
|
108
|
+
includes = serializer_includes_for_action(:create)
|
109
|
+
options[:include] = includes if includes.present?
|
110
|
+
|
111
|
+
render_success(data: obj, serializer_options: options, status: :created)
|
112
|
+
else
|
113
|
+
render_error(errors: obj.errors.full_messages[0], status: :unprocessable_entity)
|
114
|
+
end
|
115
|
+
rescue StandardError => e
|
116
|
+
render_error(error: e.message)
|
117
|
+
end
|
118
|
+
|
119
|
+
def update
|
120
|
+
obj = nil
|
121
|
+
options = {}
|
122
|
+
if block_given?
|
123
|
+
incoming = yield
|
124
|
+
if incoming.instance_of?(Array)
|
125
|
+
obj, options = incoming
|
126
|
+
elsif incoming.instance_of?(Hash)
|
127
|
+
obj = set_object
|
128
|
+
options = incoming
|
129
|
+
else
|
130
|
+
obj = incoming
|
131
|
+
end
|
132
|
+
else
|
133
|
+
obj = set_object
|
134
|
+
end
|
135
|
+
|
136
|
+
if obj.update(model_params)
|
137
|
+
obj = @clazz.includes(eager_loaded_associations).find(obj.id) if eager_loaded_associations.present?
|
138
|
+
|
139
|
+
includes = serializer_includes_for_action(:update)
|
140
|
+
options[:include] = includes if includes.present?
|
141
|
+
|
142
|
+
render_success(data: obj, serializer_options: options)
|
143
|
+
else
|
144
|
+
render_error(errors: obj.errors.full_messages[0], status: :unprocessable_entity)
|
145
|
+
end
|
146
|
+
rescue StandardError => e
|
147
|
+
render_error(error: e.message)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def set_clazz
|
153
|
+
model_name = controller_name.classify
|
154
|
+
|
155
|
+
controller_namespace = self.class.name.deconstantize
|
156
|
+
engine_namespace = controller_namespace.split("::")[0..1].join("::")
|
157
|
+
|
158
|
+
@clazz = "#{engine_namespace}::#{model_name}".constantize
|
159
|
+
rescue NameError
|
160
|
+
@clazz = "Dscf::Core::#{model_name}".constantize
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_object
|
164
|
+
@obj = if eager_loaded_associations.present?
|
165
|
+
@clazz.includes(eager_loaded_associations).find(params[:id])
|
166
|
+
else
|
167
|
+
@clazz.find(params[:id])
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Override in controllers to define what to eager load
|
172
|
+
def eager_loaded_associations
|
173
|
+
[]
|
174
|
+
end
|
175
|
+
|
176
|
+
# Override in controllers to define allowed order columns for pagination
|
177
|
+
def allowed_order_columns
|
178
|
+
%w[id created_at updated_at]
|
179
|
+
end
|
180
|
+
|
181
|
+
# Override in controllers to define serializer includes
|
182
|
+
def default_serializer_includes
|
183
|
+
{}
|
184
|
+
end
|
185
|
+
|
186
|
+
def serializer_includes_for_action(action)
|
187
|
+
includes = default_serializer_includes
|
188
|
+
|
189
|
+
if includes.is_a?(Hash)
|
190
|
+
includes[action] || includes[:default] || []
|
191
|
+
else
|
192
|
+
includes.present? ? includes : []
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# This method should be overridden by respective child controllers
|
197
|
+
def model_params; end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module JsonResponse
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def serialize(data, options = {})
|
7
|
+
case data
|
8
|
+
when ActiveRecord::Base, ActiveRecord::Relation
|
9
|
+
ActiveModelSerializers::SerializableResource.new(data, options)
|
10
|
+
when Hash
|
11
|
+
data.transform_values { |value| serialize(value, options) }
|
12
|
+
when Array
|
13
|
+
data.map { |value| serialize(value, options) }
|
14
|
+
else
|
15
|
+
data
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_success(message_key = nil, data: nil, status: :ok, serializer_options: {}, **options)
|
20
|
+
response = {success: true}
|
21
|
+
|
22
|
+
if message_key
|
23
|
+
response[:message] = I18n.t(message_key)
|
24
|
+
else
|
25
|
+
model_key = @clazz&.name&.demodulize&.underscore
|
26
|
+
action_key = action_name
|
27
|
+
i18n_key = "#{model_key}.success.#{action_key}"
|
28
|
+
|
29
|
+
response[:message] = I18n.t(i18n_key) if I18n.exists?(i18n_key)
|
30
|
+
end
|
31
|
+
|
32
|
+
if data
|
33
|
+
serialized_data = serialize(data, serializer_options)
|
34
|
+
response[:data] = serialized_data
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add pagination metadata if present (handled by Common concern)
|
38
|
+
response[:pagination] = serializer_options[:pagination] if serializer_options[:pagination]
|
39
|
+
|
40
|
+
response.merge!(options)
|
41
|
+
render json: response, status: status
|
42
|
+
end
|
43
|
+
|
44
|
+
def render_error(message_key = nil, status: :unprocessable_entity, errors: nil, **options)
|
45
|
+
response = {
|
46
|
+
success: false
|
47
|
+
}
|
48
|
+
|
49
|
+
if message_key
|
50
|
+
response[:error] = I18n.t(message_key)
|
51
|
+
else
|
52
|
+
model_key = @clazz&.name&.demodulize&.underscore
|
53
|
+
action_key = action_name
|
54
|
+
i18n_key = "#{model_key}.errors.#{action_key}"
|
55
|
+
|
56
|
+
response[:error] = I18n.t(i18n_key) if I18n.exists?(i18n_key)
|
57
|
+
end
|
58
|
+
|
59
|
+
response[:errors] = errors if errors
|
60
|
+
response.merge!(options)
|
61
|
+
|
62
|
+
render json: response, status: status
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module Pagination
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def default_per_page
|
7
|
+
10
|
8
|
+
end
|
9
|
+
|
10
|
+
def page_no
|
11
|
+
params[:page]&.to_i || 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def per_page
|
15
|
+
params[:per_page]&.to_i || default_per_page
|
16
|
+
end
|
17
|
+
|
18
|
+
def paginate_offset
|
19
|
+
(page_no - 1) * per_page
|
20
|
+
end
|
21
|
+
|
22
|
+
def order_by
|
23
|
+
allowed_columns = if respond_to?(:allowed_order_columns, true)
|
24
|
+
allowed_order_columns
|
25
|
+
else
|
26
|
+
%w[id created_at updated_at]
|
27
|
+
end
|
28
|
+
requested_column = params.fetch(:order_by, "id").to_s
|
29
|
+
allowed_columns.include?(requested_column) ? requested_column : "id"
|
30
|
+
end
|
31
|
+
|
32
|
+
def order_direction
|
33
|
+
if %w[asc desc].include?(params.fetch(:order_direction, "asc").to_s.downcase)
|
34
|
+
params.fetch(:order_direction, "asc").to_s.downcase
|
35
|
+
else
|
36
|
+
"asc"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def paginate
|
41
|
+
->(it) { it.limit(per_page).offset(paginate_offset).order("#{order_by}": order_direction) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Generate HATEOAS pagination links
|
45
|
+
def pagination_links(total_pages)
|
46
|
+
base_path = request.path
|
47
|
+
current_params = request.query_parameters.except("page")
|
48
|
+
|
49
|
+
links = {}
|
50
|
+
|
51
|
+
links[:first] = build_page_url(base_path, current_params, 1)
|
52
|
+
|
53
|
+
links[:prev] = (build_page_url(base_path, current_params, page_no - 1) if page_no > 1)
|
54
|
+
|
55
|
+
links[:next] = (build_page_url(base_path, current_params, page_no + 1) if page_no < total_pages)
|
56
|
+
|
57
|
+
links[:last] = build_page_url(base_path, current_params, total_pages)
|
58
|
+
|
59
|
+
links
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def build_page_url(base_path, params, page)
|
65
|
+
params_with_page = params.merge(page: page, per_page: per_page)
|
66
|
+
query_string = params_with_page.to_query
|
67
|
+
"#{base_path}?#{query_string}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module TokenAuthenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :validate_token_expiry
|
8
|
+
before_action :validate_device_consistency, if: :current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_token_expiry
|
12
|
+
return unless current_user
|
13
|
+
|
14
|
+
access_token = extract_access_token_from_header
|
15
|
+
return unless access_token
|
16
|
+
|
17
|
+
payload = TokenService.decode(access_token)
|
18
|
+
return unless payload
|
19
|
+
|
20
|
+
# Check if token is close to expiry (within 5 minutes)
|
21
|
+
if payload["exp"] && payload["exp"] - Time.current.to_i < 300
|
22
|
+
Rails.logger.info("Access token close to expiry for user #{current_user.id}")
|
23
|
+
end
|
24
|
+
rescue AuthenticationError
|
25
|
+
handle_expired_token
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_device_consistency
|
29
|
+
return unless current_user && request.params[:device_id]
|
30
|
+
|
31
|
+
refresh_token_record = current_user.refresh_tokens.active.find_by(device: request.params[:device_id])
|
32
|
+
return if refresh_token_record
|
33
|
+
|
34
|
+
# Device mismatch - could indicate suspicious activity
|
35
|
+
Rails.logger.warn("Device mismatch for user #{current_user.id}: #{request.params[:device_id]}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def require_valid_refresh_token
|
39
|
+
refresh_token_value = extract_refresh_token_from_params
|
40
|
+
return if refresh_token_value && RefreshToken.active.exists?(refresh_token: refresh_token_value)
|
41
|
+
|
42
|
+
raise AuthenticationError, "Valid refresh token required"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def handle_expired_token
|
48
|
+
Rails.logger.warn("Expired token detected for request to #{request.path}")
|
49
|
+
raise AuthenticationError, "Session expired"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,6 +1,25 @@
|
|
1
1
|
module Dscf
|
2
2
|
module Core
|
3
3
|
class ApplicationController < ActionController::API
|
4
|
+
include Authenticatable
|
5
|
+
include TokenAuthenticatable
|
6
|
+
include JsonResponse
|
7
|
+
|
8
|
+
# Handle CORS for authentication
|
9
|
+
before_action :set_cors_headers
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def set_cors_headers
|
14
|
+
headers["Access-Control-Allow-Origin"] = request.headers["Origin"] || "*"
|
15
|
+
headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
16
|
+
headers["Access-Control-Allow-Headers"] = "Origin, Content-Type, Accept, Authorization, X-Requested-With"
|
17
|
+
headers["Access-Control-Allow-Credentials"] = "false"
|
18
|
+
end
|
19
|
+
|
20
|
+
def authentication_required?
|
21
|
+
false # Override in specific controllers
|
22
|
+
end
|
4
23
|
end
|
5
24
|
end
|
6
25
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
class AuthController < ApplicationController
|
4
|
+
skip_before_action :authenticate_user, only: %i[login signup refresh]
|
5
|
+
skip_before_action :validate_token_expiry, only: %i[login signup refresh]
|
6
|
+
skip_before_action :validate_device_consistency, only: %i[login signup refresh]
|
7
|
+
|
8
|
+
def login
|
9
|
+
user = AuthService.authenticate_user(params[:email_or_phone], params[:password])
|
10
|
+
|
11
|
+
if user&.valid_for_authentication?
|
12
|
+
tokens = sign_in(user, request)
|
13
|
+
render_success(
|
14
|
+
"auth.success.login",
|
15
|
+
data: {
|
16
|
+
user: user_with_profile(user),
|
17
|
+
access_token: tokens[:access_token],
|
18
|
+
refresh_token: tokens[:refresh_token].refresh_token
|
19
|
+
}
|
20
|
+
)
|
21
|
+
else
|
22
|
+
render_error("auth.errors.invalid_credentials", status: :unauthorized)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def signup
|
27
|
+
user = User.new(user_params)
|
28
|
+
|
29
|
+
return render_error("auth.errors.missing_email_or_phone") unless user.email.present? || user.phone.present?
|
30
|
+
|
31
|
+
ActiveRecord::Base.transaction do
|
32
|
+
if user.save
|
33
|
+
assign_default_role(user)
|
34
|
+
render_success(
|
35
|
+
"auth.success.signup",
|
36
|
+
data: {
|
37
|
+
user: user_with_profile(user)
|
38
|
+
},
|
39
|
+
status: :created
|
40
|
+
)
|
41
|
+
else
|
42
|
+
render_error(
|
43
|
+
"auth.errors.signup_failed",
|
44
|
+
errors: user.errors.full_messages,
|
45
|
+
status: :unprocessable_entity
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def logout
|
52
|
+
sign_out
|
53
|
+
render_success("auth.success.logout")
|
54
|
+
end
|
55
|
+
|
56
|
+
def me
|
57
|
+
render_success(
|
58
|
+
"auth.success.me",
|
59
|
+
data: {
|
60
|
+
user: user_with_profile(current_user)
|
61
|
+
}
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def refresh
|
66
|
+
new_tokens = refresh_token
|
67
|
+
if new_tokens
|
68
|
+
render_success(
|
69
|
+
"auth.success.refresh",
|
70
|
+
data: {
|
71
|
+
access_token: new_tokens[:access_token]
|
72
|
+
}
|
73
|
+
)
|
74
|
+
else
|
75
|
+
render_error("auth.errors.invalid_token", status: :unauthorized)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def user_params
|
82
|
+
params.require(:user).permit(
|
83
|
+
:email,
|
84
|
+
:phone,
|
85
|
+
:password,
|
86
|
+
:password_confirmation,
|
87
|
+
:temp_password,
|
88
|
+
user_profile_attributes: %i[first_name last_name date_of_birth address]
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def refresh_params
|
93
|
+
params.permit(:refresh_token)
|
94
|
+
end
|
95
|
+
|
96
|
+
def assign_default_role(user)
|
97
|
+
role = Role.find_or_create_by!(name: "user") do |r|
|
98
|
+
r.code = "USER"
|
99
|
+
end
|
100
|
+
|
101
|
+
UserRole.create!(user: user, role: role)
|
102
|
+
end
|
103
|
+
|
104
|
+
def user_with_profile(user)
|
105
|
+
user.as_json(
|
106
|
+
only: %i[id email phone],
|
107
|
+
include: {
|
108
|
+
user_profile: {only: %i[first_name last_name verification_status]},
|
109
|
+
roles: {only: %i[name description]}
|
110
|
+
}
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def authentication_required?
|
115
|
+
# Skip authentication for login, signup, and refresh endpoints
|
116
|
+
%w[login signup refresh].exclude?(action_name)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
class AuthenticationError < StandardError
|
4
|
+
attr_reader :status_code
|
5
|
+
|
6
|
+
def initialize(message = "Authentication failed", status_code = 401)
|
7
|
+
super(message)
|
8
|
+
@status_code = status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
{
|
13
|
+
error: message,
|
14
|
+
status: status_code
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|