Skip to content

Commit 37db7ed

Browse files
committed
fix: duplicated auth route scope and serializer issue
1 parent 355e2b9 commit 37db7ed

File tree

4 files changed

+304
-40
lines changed

4 files changed

+304
-40
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
module Api
2+
module V1
3+
class AuthController < BaseController
4+
skip_before_action :authenticate_request!, only: [:register, :login, :forgot_password, :reset_password, :refresh]
5+
6+
# POST /api/v1/auth/register
7+
def register
8+
ActiveRecord::Base.transaction do
9+
organization = create_organization!
10+
user = create_user!(organization)
11+
tokens = Authentication::Services::JwtService.generate_tokens(user)
12+
13+
# Log audit action with user's organization
14+
AuditLog.create!(
15+
organization: organization,
16+
user: user,
17+
action: 'register',
18+
entity_type: 'User',
19+
entity_id: user.id,
20+
ip_address: request.remote_ip,
21+
user_agent: request.user_agent
22+
)
23+
24+
render_created(
25+
{
26+
user: JSON.parse(UserSerializer.render(user)),
27+
organization: JSON.parse(OrganizationSerializer.render(organization)),
28+
**tokens
29+
},
30+
message: 'Registration successful'
31+
)
32+
end
33+
rescue ActiveRecord::RecordInvalid => e
34+
render_validation_errors(e)
35+
rescue => e
36+
render_error(message: 'Registration failed', code: 'REGISTRATION_ERROR')
37+
end
38+
39+
# POST /api/v1/auth/login
40+
def login
41+
user = authenticate_user!
42+
43+
if user
44+
tokens = Authentication::Services::JwtService.generate_tokens(user)
45+
user.update_last_login!
46+
47+
# Log audit action with user's organization
48+
AuditLog.create!(
49+
organization: user.organization,
50+
user: user,
51+
action: 'login',
52+
entity_type: 'User',
53+
entity_id: user.id,
54+
ip_address: request.remote_ip,
55+
user_agent: request.user_agent
56+
)
57+
58+
render_success(
59+
{
60+
user: JSON.parse(UserSerializer.render(user)),
61+
organization: JSON.parse(OrganizationSerializer.render(user.organization)),
62+
**tokens
63+
},
64+
message: 'Login successful'
65+
)
66+
else
67+
render_error(
68+
message: 'Invalid email or password',
69+
code: 'INVALID_CREDENTIALS',
70+
status: :unauthorized
71+
)
72+
end
73+
end
74+
75+
# POST /api/v1/auth/refresh
76+
def refresh
77+
refresh_token = params[:refresh_token]
78+
79+
if refresh_token.blank?
80+
return render_error(
81+
message: 'Refresh token is required',
82+
code: 'MISSING_REFRESH_TOKEN',
83+
status: :bad_request
84+
)
85+
end
86+
87+
begin
88+
tokens = Authentication::Services::JwtService.refresh_access_token(refresh_token)
89+
render_success(tokens, message: 'Token refreshed successfully')
90+
rescue Authentication::Services::JwtService::AuthenticationError => e
91+
render_error(
92+
message: e.message,
93+
code: 'INVALID_REFRESH_TOKEN',
94+
status: :unauthorized
95+
)
96+
end
97+
end
98+
99+
# POST /api/v1/auth/logout
100+
def logout
101+
# For JWT, we don't need to do anything server-side for logout
102+
# The client should remove the token
103+
104+
log_user_action(
105+
action: 'logout',
106+
entity_type: 'User',
107+
entity_id: current_user.id
108+
)
109+
110+
render_success({}, message: 'Logout successful')
111+
end
112+
113+
# POST /api/v1/auth/forgot-password
114+
def forgot_password
115+
email = params[:email]&.downcase&.strip
116+
117+
if email.blank?
118+
return render_error(
119+
message: 'Email is required',
120+
code: 'MISSING_EMAIL',
121+
status: :bad_request
122+
)
123+
end
124+
125+
user = User.find_by(email: email)
126+
127+
if user
128+
# Generate password reset token
129+
reset_token = generate_reset_token(user)
130+
131+
# Here you would send an email with the reset token
132+
# For now, we'll just return success
133+
134+
AuditLog.create!(
135+
organization: user.organization,
136+
user: user,
137+
action: 'password_reset_requested',
138+
entity_type: 'User',
139+
entity_id: user.id,
140+
ip_address: request.remote_ip,
141+
user_agent: request.user_agent
142+
)
143+
end
144+
145+
# Always return success to prevent email enumeration
146+
render_success(
147+
{},
148+
message: 'If the email exists, a password reset link has been sent'
149+
)
150+
end
151+
152+
# POST /api/v1/auth/reset-password
153+
def reset_password
154+
token = params[:token]
155+
new_password = params[:password]
156+
password_confirmation = params[:password_confirmation]
157+
158+
if token.blank? || new_password.blank?
159+
return render_error(
160+
message: 'Token and password are required',
161+
code: 'MISSING_PARAMETERS',
162+
status: :bad_request
163+
)
164+
end
165+
166+
if new_password != password_confirmation
167+
return render_error(
168+
message: 'Password confirmation does not match',
169+
code: 'PASSWORD_MISMATCH',
170+
status: :bad_request
171+
)
172+
end
173+
174+
user = verify_reset_token(token)
175+
176+
if user
177+
user.update!(password: new_password)
178+
179+
AuditLog.create!(
180+
organization: user.organization,
181+
user: user,
182+
action: 'password_reset_completed',
183+
entity_type: 'User',
184+
entity_id: user.id,
185+
ip_address: request.remote_ip,
186+
user_agent: request.user_agent
187+
)
188+
189+
render_success({}, message: 'Password reset successful')
190+
else
191+
render_error(
192+
message: 'Invalid or expired reset token',
193+
code: 'INVALID_RESET_TOKEN',
194+
status: :bad_request
195+
)
196+
end
197+
end
198+
199+
# GET /api/v1/auth/me
200+
def me
201+
render_success(
202+
{
203+
user: JSON.parse(UserSerializer.render(current_user)),
204+
organization: JSON.parse(OrganizationSerializer.render(current_organization))
205+
}
206+
)
207+
end
208+
209+
private
210+
211+
def create_organization!
212+
Organization.create!(organization_params)
213+
end
214+
215+
def create_user!(organization)
216+
User.create!(user_params.merge(
217+
organization: organization,
218+
role: 'owner' # First user is always the owner
219+
))
220+
end
221+
222+
def authenticate_user!
223+
email = params[:email]&.downcase&.strip
224+
password = params[:password]
225+
226+
return nil if email.blank? || password.blank?
227+
228+
user = User.find_by(email: email)
229+
user&.authenticate(password) ? user : nil
230+
end
231+
232+
def organization_params
233+
params.require(:organization).permit(:name, :region, :tier)
234+
end
235+
236+
def user_params
237+
params.require(:user).permit(:email, :password, :full_name, :timezone, :language)
238+
end
239+
240+
def generate_reset_token(user)
241+
# In a real app, you'd store this token in the database or Redis
242+
# For now, we'll use JWT with a short expiration
243+
payload = {
244+
user_id: user.id,
245+
type: 'password_reset',
246+
exp: 1.hour.from_now.to_i,
247+
iat: Time.current.to_i
248+
}
249+
250+
JWT.encode(payload, Authentication::Services::JwtService::SECRET_KEY, 'HS256')
251+
end
252+
253+
def verify_reset_token(token)
254+
begin
255+
decoded = JWT.decode(token, Authentication::Services::JwtService::SECRET_KEY, true, { algorithm: 'HS256' })
256+
payload = HashWithIndifferentAccess.new(decoded[0])
257+
258+
return nil unless payload[:type] == 'password_reset'
259+
260+
User.find(payload[:user_id])
261+
rescue JWT::DecodeError, JWT::ExpiredSignature, ActiveRecord::RecordNotFound
262+
nil
263+
end
264+
end
265+
end
266+
end
267+
end

app/serializers/organization_serializer.rb

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
class OrganizationSerializer
2-
include Blueprinter::Base
1+
class OrganizationSerializer < Blueprinter::Base
32

43
identifier :id
54

@@ -25,18 +24,21 @@ class OrganizationSerializer
2524
end
2625

2726
field :tier_display do |org|
28-
return 'Not set' if org.tier.blank?
29-
30-
org.tier.humanize
27+
if org.tier.blank?
28+
'Not set'
29+
else
30+
org.tier.humanize
31+
end
3132
end
3233

3334
field :subscription_display do |org|
34-
return 'Free' if org.subscription_plan.blank?
35-
36-
plan = org.subscription_plan.humanize
37-
status = org.subscription_status&.humanize || 'Active'
38-
39-
"#{plan} (#{status})"
35+
if org.subscription_plan.blank?
36+
'Free'
37+
else
38+
plan = org.subscription_plan.humanize
39+
status = org.subscription_status&.humanize || 'Active'
40+
"#{plan} (#{status})"
41+
end
4042
end
4143

4244
field :statistics do |org|

app/serializers/user_serializer.rb

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
class UserSerializer
2-
include Blueprinter::Base
1+
class UserSerializer < Blueprinter::Base
32

43
identifier :id
54

@@ -21,30 +20,26 @@ class UserSerializer
2120
end
2221

2322
field :last_login_display do |user|
24-
if user.last_login_at
25-
time_ago_in_words(user.last_login_at)
26-
else
27-
'Never'
28-
end
23+
user.last_login_at ? time_ago_in_words(user.last_login_at) : 'Never'
2924
end
3025

31-
private
32-
3326
def self.time_ago_in_words(time)
34-
return 'Never' if time.nil?
35-
36-
diff = Time.current - time
37-
case diff
38-
when 0...60
39-
"#{diff.to_i} seconds ago"
40-
when 60...3600
41-
"#{(diff / 60).to_i} minutes ago"
42-
when 3600...86400
43-
"#{(diff / 3600).to_i} hours ago"
44-
when 86400...2592000
45-
"#{(diff / 86400).to_i} days ago"
27+
if time.nil?
28+
'Never'
4629
else
47-
time.strftime('%B %d, %Y')
30+
diff = Time.current - time
31+
case diff
32+
when 0...60
33+
"#{diff.to_i} seconds ago"
34+
when 60...3600
35+
"#{(diff / 60).to_i} minutes ago"
36+
when 3600...86400
37+
"#{(diff / 3600).to_i} hours ago"
38+
when 86400...2592000
39+
"#{(diff / 86400).to_i} days ago"
40+
else
41+
time.strftime('%B %d, %Y')
42+
end
4843
end
4944
end
5045
end

config/routes.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
namespace :v1 do
1212
# Auth
1313
scope :auth do
14-
post 'auth/register', to: 'authentication/auth#register'
15-
post 'auth/login', to: 'authentication/auth#login'
16-
post 'auth/refresh', to: 'authentication/auth#refresh'
17-
post 'auth/logout', to: 'authentication/auth#logout'
18-
post 'auth/forgot-password', to: 'authentication/auth#forgot_password'
19-
post 'auth/reset-password', to: 'authentication/auth#reset_password'
20-
get 'auth/me', to: 'authentication/auth#me'
14+
post 'register', to: 'auth#register'
15+
post 'login', to: 'auth#login'
16+
post 'refresh', to: 'auth#refresh'
17+
post 'logout', to: 'auth#logout'
18+
post 'forgot-password', to: 'auth#forgot_password'
19+
post 'reset-password', to: 'auth#reset_password'
20+
get 'me', to: 'auth#me'
2121
end
2222

2323
# Dashboard

0 commit comments

Comments
 (0)