Class: Rhino::AuthController
- Inherits:
-
ActionController::API
- Object
- ActionController::API
- Rhino::AuthController
- Defined in:
- lib/rhino/controllers/auth_controller.rb
Overview
Authentication controller — mirrors Laravel AuthController exactly.
Endpoints:
POST /api/auth/login
POST /api/auth/logout
POST /api/auth/password/recover
POST /api/auth/password/reset
POST /api/auth/register
Instance Method Summary collapse
-
#login ⇒ Object
POST /api/auth/login.
-
#logout ⇒ Object
POST /api/auth/logout.
-
#recover_password ⇒ Object
POST /api/auth/password/recover.
-
#register_with_invitation ⇒ Object
POST /api/auth/register.
-
#reset ⇒ Object
POST /api/auth/password/reset.
Instance Method Details
#login ⇒ Object
POST /api/auth/login
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/rhino/controllers/auth_controller.rb', line 16 def login email = params[:email].to_s.strip password = params[:password].to_s if email.blank? || password.blank? return render json: { message: "Invalid credentials" }, status: :unauthorized end user_class = "User".safe_constantize return render json: { message: "Invalid credentials" }, status: :unauthorized unless user_class user = user_class.find_by(email: email) unless user&.authenticate(password) return render json: { message: "Invalid credentials" }, status: :unauthorized end # Group membership is a coarse access gate (GROUP_AUTH_DESIGN.md §6). # Gated entirely by the enforce_group_membership flag; off = unchanged. if membership_enforced? && !group_member?(user) return render json: { message: "You are not a member of this group" }, status: :forbidden end token = generate_api_token(user) # Get the first organization the user belongs to organization_slug = nil if user.respond_to?(:organizations) first_org = user.organizations.first organization_slug = first_org&.slug end # Lifecycle hook (GROUP_AUTH_DESIGN.md §7). A reject revokes the token. hook_response = run_hook(:after_login, user, token: token, revoke_on_reject: true) return if hook_response render json: { token: token, organization_slug: organization_slug }, status: :ok end |
#logout ⇒ Object
POST /api/auth/logout
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/rhino/controllers/auth_controller.rb', line 59 def logout user = current_user if user.respond_to?(:regenerate_api_token) user.regenerate_api_token elsif user.respond_to?(:update_column) && user.class.column_names.include?("api_token") user.update_column(:api_token, SecureRandom.hex(32)) end # Token is already gone; a rejecting hook only changes the status code. hook_response = run_hook(:after_logout, user, revoke_on_reject: false) return if hook_response render json: { message: "Logged out successfully" }, status: :ok end |
#recover_password ⇒ Object
POST /api/auth/password/recover
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/rhino/controllers/auth_controller.rb', line 76 def recover_password email = params[:email].to_s.strip if email.blank? return render json: { errors: { email: ["The email field is required."] } }, status: :unprocessable_entity end user_class = "User".safe_constantize user = user_class&.find_by(email: email) if user token = SecureRandom.hex(32) # Store reset token if user.respond_to?(:update_columns) user.update_columns( reset_password_token: token, reset_password_sent_at: Time.current ) end # Send email via mailer if available mailer_class = "Rhino::PasswordRecoveryMailer".safe_constantize mailer_class&.recover(user, token)&.deliver_later # Lifecycle hook fires only when a user actually exists, and its # rejection is SWALLOWED here. recover_password must be an enumeration # oracle-free endpoint: a rejecting hook would otherwise return a 403 # only for existing emails, letting a caller distinguish real accounts # from fake ones. The hook still runs for its side effects (e.g. # auditing, throttling), but its reject never changes the response. run_hook(:after_password_recover, user, revoke_on_reject: false, swallow_reject: true) end # Always return the same response (existing OR non-existing email) to # prevent email enumeration — this is the documented contract. render json: { message: "Password recovery email sent." }, status: :ok end |
#register_with_invitation ⇒ Object
POST /api/auth/register
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/rhino/controllers/auth_controller.rb', line 165 def register_with_invitation errors = {} errors[:token] = ["The token field is required."] if params[:token].blank? errors[:name] = ["The name field is required."] if params[:name].blank? errors[:email] = ["The email field is required."] if params[:email].blank? errors[:password] = ["The password field is required."] if params[:password].blank? if params[:password].present? && params[:password].length < 8 errors[:password] = ["The password must be at least 8 characters."] end if params[:password].present? && params[:password] != params[:password_confirmation] errors[:password_confirmation] = ["The password confirmation does not match."] end user_class = "User".safe_constantize if user_class && params[:email].present? && user_class.exists?(email: params[:email]) errors[:email] = ["The email has already been taken."] end unless errors.empty? return render json: { errors: errors }, status: :unprocessable_entity end # Find invitation invitation = OrganizationInvitation.find_by(token: params[:token], status: "pending") unless invitation return render json: { message: "Invalid or expired invitation token" }, status: :not_found end if invitation.expired? invitation.update!(status: "expired") return render json: { message: "This invitation has expired" }, status: :unprocessable_entity end # Validate email matches invitation unless invitation.email == params[:email] return render json: { message: "Email does not match the invitation" }, status: :unprocessable_entity end # Create user user = user_class.create!( name: params[:name], email: params[:email], password: params[:password] ) # Accept invitation (adds user to organization, carrying its route_group) invitation.accept!(user) # Generate token token = generate_api_token(user) # Get organization slug for redirect organization = invitation.organization organization_slug = organization&.slug # Lifecycle hook for the group the invitee joined (from the invitation). invite_group = invitation.respond_to?(:route_group) ? invitation.route_group : nil hook_response = run_hook( :after_register, user, token: token, revoke_on_reject: true, group_override: invite_group, organization_override: organization ) return if hook_response render json: { message: "Registration successful", token: token, user: user.as_json(except: %w[password_digest api_token reset_password_token]), organization_slug: organization_slug }, status: :created end |
#reset ⇒ Object
POST /api/auth/password/reset
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/rhino/controllers/auth_controller.rb', line 116 def reset errors = {} errors[:token] = ["The token field is required."] if params[:token].blank? errors[:email] = ["The email field is required."] if params[:email].blank? errors[:password] = ["The password field is required."] if params[:password].blank? if params[:password].present? && params[:password].length < 8 errors[:password] = ["The password must be at least 8 characters."] end if params[:password].present? && params[:password] != params[:password_confirmation] errors[:password_confirmation] = ["The password confirmation does not match."] end unless errors.empty? return render json: { errors: errors }, status: :unprocessable_entity end user_class = "User".safe_constantize user = user_class&.find_by(email: params[:email]) unless user return render json: { message: "Token is invalid or expired." }, status: :bad_request end # Verify token valid_token = user.respond_to?(:reset_password_token) && user.reset_password_token == params[:token] && user.respond_to?(:reset_password_sent_at) && user.reset_password_sent_at.present? && user.reset_password_sent_at > 1.hour.ago unless valid_token return render json: { message: "Token is invalid or expired." }, status: :bad_request end # Update password user.password = params[:password] user.reset_password_token = nil user.reset_password_sent_at = nil user.save! hook_response = run_hook(:after_password_reset, user, revoke_on_reject: false) return if hook_response render json: { message: "Password has been reset." }, status: :ok end |