Server side Rails model validation in a React form - ruby-on-rails

I am trying to validate User inputs on server side in a Rails Application with React as view. Basically I make axios calls to the Rails API like this:
const Script = props => {
const [script, setScript] = useState({})
const [scene, setScene] = useState({})
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const scriptID = props.match.params.id
const url = `/api/v1/scripts/${scriptID}`
axios.get(url)
.then( resp => {
setScript(resp.data)
setLoaded(true)
})
.catch(resp => console.log(resp))
}, [])
const handleChange = (e) => {
e.preventDefault()
setScene(Object.assign({}, scene, {[e.target.name]: e.target.value}))
}
const handleSubmit = (e) => {
e.preventDefault()
const csrfToken = document.querySelector('[name=csrf-token]').content
axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken
const script_id = script.data.id
axios.post('/api/v1/scenes', {scene, script_id})
.then(resp => {
const included = [...script.included, resp.data.data]
setScript({...script, included})
})
.catch(err => {
console.log(err.response.data.error)
})
.finally(() => {
setScene({name: '', description: ''})
})
}
All data gets passed into a react component with a form.
return (
<div className="wrapper">
{
loaded &&
<Fragment>
.
.
.
<SceneForm
handleChange={handleChange}
handleSubmit={handleSubmit}
attributes={script.data.attributes}
scene={scene}
/>
</Fragment>
}
</div>
)
In this form I have a name field and the corresponding name in Rails has a validation uniqueness: true. everything works fine if I enter a valid (unique) name.
I tried to implement a validation but I am not happy with the outcome. (It works in general: my no_errors? method does what is is supposed to do and I get a 403 status) This is the controller part:
def create
scene = script.scenes.new(scene_params)
if no_error?(scene)
if scene.save
render json: SceneSerializer.new(scene).serialized_json
else
render json: { error: scene.errors.messages }, status: 422
# render json: { error: scene.errors.messages[:name] }, status: 423
end
else
render json: { error: "name must be unique" }, status: 403
end
end
.
.
.
private
def no_error?(scene)
Scene.where(name: scene.name, script_id: scene.script_id).empty?
end
If I enter an existing name I get a console.log like this:
screenshot
Here is my concern: I am not happy with my approach of error handling in general. I do not want to get the 403 message logged to the console (I do want to avoid this message in the first place).
My idea is to take the "simple form" approach: Make the border of my field red and post an error message under the field, without any console output...
And on a side note: Is 403 the correct status? I was thinking about 422 but wasn't sure...
Thank you for your ideas in advance!

403 is the wrong status code. What you need is to return a 422 (unprocessable entity). 403 is more about policy and what you are authorized to do.
Then when you deal with http request it's a standard to have a request and status code printed in browser console. Not sur to get your issue here.
If it's about to display the error you could just have a function that colorize (or whatever fireworks you want) your input if the status code response is a 422.

Related

Value of ActionText body is always " "(empty string)

I am using Rails 6 API and React. I'm trying to build a Rich Text Editor with ActionText. When I send the RTE content from the Trix editor on the front end, it just doesn't set the ActionText body to the body I sent through with Axios.
I am sure that the body has come correctly to the controller because I used byebug and printed out the param value.
For example, it looked like this: <div><!--block-->test</div>
But whenever I try to view what it actually is by running announcement.details.to_s it returns " " for some reason.
I set the details field like this: has_rich_text :details in the Announcement model.
My controller which handles this looks like this:
module V1
class AnnouncementsController < ApplicationController
def create
announcement = Announcement.new(announcement_params)
announcement.author = #current_user
authorize announcement
if announcement.valid? && announcement.save
render json: { message: "Announcement successfully created! You can view it here." }, status: 201
else
render json: { messages: announcement.errors.full_messages }, status: 400
end
end
private
def announcement_params
params.require(:announcement).permit(:title, :details)
end
end
end
If it helps in any way, this is the React code:
const RTE = (props) => {
let trixInput = React.createRef()
useEffect(() => {
trixInput.current.addEventListener("trix-change", event => {
console.log("fired")
props.onChange(event.target.innerHTML)
})
}, [])
return (
<div>
<input
type="hidden"
id="trix"
value={props.value}
/>
<trix-editor
input="trix"
data-direct-upload-url={`${bURL}/rails/active_storage/direct_uploads`}
data-blob-url-template={`${bURL}/rails/active_storage/blobs/:signed_id/*filename`}
ref={trixInput}
className="trix-content"
></trix-editor>
</div>
);
}
And then I just normally pass it with Axios:
axios.post(`${bURL}/v1/announcements/create`, {
"announcement": {
"title": title,
"details": value
}
}, {
headers: {
'Authorization': `token goes here`
}
}).then(res => {
// success
}).catch(err => {
// error
})
If you need any more code snippets or information please comment.

How to read JSON error message React-Rails

Wandering into uncharted waters, I've been having some trouble figuring out how to access an error message I made in my Rails API thru my React Frontend. I've been reading through a good amount of sites and so far I haven't really been able to figure out where i'm going wrong. Is it on the Rails side or the React side?
TLDR; I want my response error to be: "Username or Password does not match.", but I am getting: "Request failed with status code 422"
Rails Controller
class Api::V1::SessionsController < ApplicationController
def create
user = User.find_by(email: params["user"]["email"]).try(:authenticate, params["user"]["password"])
if user
session[:user_id] = user.id
render json: {
status: 200,
logged_in: true,
user: user
}
else
// how can I reach this error message?
render json: { status: "error", message: "Username or Password does not match." }, status: :unprocessable_entity
end
end
...
React Component
handleLogin = (e) => {
e.preventDefault();
axios
.post(
'http://localhost:3001/api/v1/sessions',
{
user: { email: this.state.email, password: this.state.password }
},
{ withCredentials: true }
)
.then((response) => {
if (response.data.logged_in) {
this.handleSuccessfulAuth(response.data);
}
})
.catch((error) => {
// returns login error Request failed with status code 422
console.log('login error', error.message);
});
};
Your message property could be accessible like this:
.catch(error => {
console.log('login error', error.response.data.message);
});

Tying in the PayPal smart buttons with a Ruby on Rails form_for and controller/method?

I have the smart buttons "working" in sandbox but I can't think of any way to attach the smart buttons success to the order form which creates the order. With Stripe Elements, it's pretty plug and play because it's on the page and a part of the form itself, but with PayPal with the redirects, I can't seem to think of a way.
Does this require javascript or can I do this without it, aside from what's already there?
Form:
<%= form_for(#order, url: listing_orders_path([#listing, #listing_video]), html: {id: "payment_form-4"} ) do |form| %>
<%= form.label :name, "Your Name", class: "form-label" %>
<%= form.text_field :name, class: "form-control", required: true, placeholder: "John" %>
#stripe code here (not important)
<%= form.submit %>
<div id="paypal-button-container"></div>
<!-- Include the PayPal JavaScript SDK -->
<script src="https://www.paypal.com/sdk/js?client-id=sb&currency=USD"></script>
<script>
// Render the PayPal button into #paypal-button-container
paypal.Buttons({
// Set up the transaction
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
amount: {
value: <%= #listing.listing_video.price %>
}
}]
});
},
// Finalize the transaction
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
// Show a success message to the buyer
alert('Transaction completed by ' + details.payer.name.given_name + '!');
});
}
}).render('#paypal-button-container');
</script>
Create Method in Controller:
require 'paypal-checkout-sdk'
client_id = Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_id)
client_secret = Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_secret)
# Creating an environment
environment = PayPal::SandboxEnvironment.new(client_id, client_secret)
client = PayPal::PayPalHttpClient.new(environment)
#amount_paypal = (#listing.listing_video.price || #listing.listing_tweet.price)
request = PayPalCheckoutSdk::Orders::OrdersCreateRequest::new
request.request_body(
{
intent: 'AUTHORIZE',
purchase_units: [
{
amount: {
currency_code: 'USD',
value: "#{#amount_paypal}"
}
}
]
}
)
begin
# Call API with your client and get a response for your call
response = client.execute(request)
# If call returns body in response, you can get the deserialized version from the result attribute of the response
order = response.result
puts order
#order.paypal_authorization_token = response.id
rescue BraintreeHttp::HttpError => ioe
# Something went wrong server-side
puts ioe.status_code
puts ioe.headers['debug_id']
end
How can I tie in the PayPal smart buttons with the form so once the payment is completed, it creates an order if successful?
UPDATE:::::::
Created a PaypalPayments controller and model:
controller:
def create
#paypal_payment = PaypalPayment.new
#listing = Listing.find_by(params[:listing_id])
require 'paypal-checkout-sdk'
client_id = "#{Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_id)}"
client_secret = "#{Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_secret)}"
# Creating an environment
environment = PayPal::SandboxEnvironment.new(client_id, client_secret)
client = PayPal::PayPalHttpClient.new(environment)
#amount_paypal = #listing.listing_video.price
request = PayPalCheckoutSdk::Orders::OrdersCreateRequest::new
#paypal_payment = request.request_body({
intent: "AUTHORIZE",
purchase_units: [
{
amount: {
currency_code: "USD",
value: "#{#amount_paypal}"
}
}
]
})
begin
# Call API with your client and get a response for your call
response = client.execute(request)
# If call returns body in response, you can get the deserialized version from the result attribute of the response
order = response.result
puts order
# #order.paypal_authorization_token = response.id
rescue BraintreeHttp::HttpError => ioe
# Something went wrong server-side
puts ioe.status_code
puts ioe.headers["debug_id"]
end
# if #paypal_payment.create
# render json: {success: true}
# else
# render json: {success: false}
# end
end
Javascript in view:
paypal.Buttons({
createOrder: function() {
return fetch('/paypal_payments', {
method: 'post',
headers: {
'content-type': 'application/json'
}
}).then(function(res) {
return res.json();
}).then(function(data) {
return data.orderID;
});
},
onApprove: function(data) {
return fetch('/orders', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
orderID: data.orderID
})
}).then(function(res) {
return res.json();
}).then(function(details) {
alert('Authorization created for ' + details.payer_given_name);
});
},
}).render('#paypal-button-container');
With this, the paypal box appears but then goes away right after it loads with this in the CMD:
#<OpenStruct id="1Pxxxxxxx394U", links=[#<OpenStruct href="https://api.sandbox.paypal.com/v2/checkout/orders/1P0xxxxxxx394U", rel="self", method="GET">, #<OpenStruct href="https://www.sandbox.paypal.com/checkoutnow?token=1P07xxxxxxx94U", rel="approve", method="GET">, #<OpenStruct href="https://api.sandbox.paypal.com/v2/checkout/orders/1Pxxxxxxx4U", rel="update", method="PATCH">, #<OpenStruct href="https://api.sandbox.paypal.com/v2/checkout/orders/1P07xxxxxxx394U/authorize", rel="authorize", method="POST">], status="CREATED">
No template found for PaypalPaymentsController#create, rendering head :no_content
Completed 204 No Content in 2335ms (ActiveRecord: 15.8ms)
I have not used smart buttons. However, you should not have "a ton more code" in a create action. If you are following MVC and rails conventions. It would seem that you need a seperate controller action to handle the payment authorization separately from the create action. But if you can get to this point in your javascript, here is example of how you would send the data from paypal javascript back to your controller, this will need some work but hopefully it points you in the right direction:
// Finalize the transaction
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
// Show a success message to the buyer
alert('Transaction completed by ' + details.payer.name.given_name + '!');
// here is where you should send info to your controller action via ajax.
$.ajax({
type: "POST",
url: "/orders",
data: data,
success: function(data) {
alert(data); // or whatever you wanna do here
return false;
},
error: function(data) {
alert(data); // or something else
return false;
}
});
});
}
This is most likely far too late, but ill add what worked for me.
You need to return the response ID to the PayPal script as a json object. All you need to do is update your create function like so :
...
begin
# Call API with your client and get a response for your call
response = client.execute(request)
# If call returns body in response, you can get the deserialized version from the result attribute of the response
order = response.result
render json: { orderID: order.id }
# #order.paypal_authorization_token = response.id
rescue BraintreeHttp::HttpError => ioe
# Something went wrong server-side
puts ioe.status_code
puts ioe.headers["debug_id"]
end
...

Rails is attempting to render remotely... but not

I'm working on a rails application and am attempting to convert the event_calendar gem's "next month" link into an ajax response.
I set the link to remote:
def month_link(month_date)
link_to I18n.localize(month_date, :format => "%B"),
{:month => month_date.month, :year => month_date.year},
remote: true
end
told it to respond to js...
respond_to do |format|
format.html
format.js { render text: "help me!" }
end
And it works!
Started GET "/calendar/2012/6" for 127.0.0.1 at 2012-07-03 15:27:42 -0500
Processing by CalendarController#index as JS
Parameters: {"year"=>"2012", "month"=>"6"}
Event Load (0.3ms) SELECT "events".* FROM "events" WHERE (('2012-05-27 05:00:00.000000' <= end_at) AND (start_at< '2012-07-01 05:00:00.000000')) ORDER BY start_at ASC
Rendered text template (0.0ms)
Completed 200 OK in 14ms (Views: 0.7ms | ActiveRecord: 0.3ms)
well... except for the part where it doesn't actually render anything I pass it. If I just tell it to format.js w/o the render, it doesn't actually respond to a js file.
What could cause a render to not display?
Updates
I just noticed that if you access the url like so localhost:3000/calendar/2012/6.js It works as expected, So I would assume it's an issue with how the link is set up?
Ok, I got the js file working, but I have no clue why. I think I was miss-using render (although I could have sworn I had used it for debugging purposes once). I guess render only actually render an html page when responding to an html request. Would make sense since it passes json to javascript for ajax requests.
Another part of the issue was I was trying to use CoffeeScript with either index.js.coffee.erb or index.js.erb.coffee. I thought it was working for the longest time, but what was really happening, was it was using the original index.js.erb I had written first, even though I had already deleted it. Once I restarted the server, everything broke.
Try this:
def month_link(month_date)
link_to I18n.localize(month_date, :format => "%B"),
{:remote=>true, :month => month_date.month, :year => month_date.year}
end
The format of link_to you are wanting to use is:
link_to(body, url_options = {}, html_options = {})
The :remote=>true wants to be in the url_options. I'm not sure what the :month & :year keys are for, but if they are html options, you would want this:
def month_link(month_date)
link_to I18n.localize(month_date, :format => "%B"),
{:remote=>true},
{:month => month_date.month, :year => month_date.year}
end
It seems that by default the remote option ignores any attempts to render or redirect. Considering that the point of Ajax is to prevent both of these... I can see why.
For self reference here is what (to my knowledge) happens when you create a remote link_to:
line 51 of jquery_ujs.js
$.rails = rails = {
// Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',
following linkClickSelector we find this function at line 300
$(document).delegate(rails.linkClickSelector, 'click.rails', function(e) {
var link = $(this), method = link.data('method'), data = link.data('params');
if (!rails.allowAction(link)) return rails.stopEverything(e);
if (link.is(rails.linkDisableSelector)) rails.disableElement(link);
if (link.data('remote') !== undefined) {
if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; }
if (rails.handleRemote(link) === false) { rails.enableElement(link); }
return false;
} else if (link.data('method')) {
rails.handleMethod(link);
return false;
}
});
Assuming that handleRemote handles the AJAX we wind up at line 107 to find this monster
// Submits "remote" forms and links with ajax
handleRemote: function(element) {
var method, url, data, crossDomain, dataType, options;
if (rails.fire(element, 'ajax:before')) {
crossDomain = element.data('cross-domain') || null;
dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
if (element.is('form')) {
method = element.attr('method');
url = element.attr('action');
data = element.serializeArray();
// memoized value from clicked submit button
var button = element.data('ujs:submit-button');
if (button) {
data.push(button);
element.data('ujs:submit-button', null);
}
} else if (element.is(rails.inputChangeSelector)) {
method = element.data('method');
url = element.data('url');
data = element.serialize();
if (element.data('params')) data = data + "&" + element.data('params');
} else {
method = element.data('method');
url = rails.href(element);
data = element.data('params') || null;
}
options = {
type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain,
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
element.trigger('ajax:error', [xhr, status, error]);
}
};
// Only pass url to `ajax` options if not blank
if (url) { options.url = url; }
return rails.ajax(options);
} else {
return false;
}
},

Check username availability

I have a form to user login:
<%= form_tag(#action, :method => "post", :name => 'signup' ,:onSubmit => 'return validate();') do %>
<%= label_tag(:user, "Username:") %>
<%= text_field_tag(:user) %>
I want to check if there is the username in the database immediately after :user-field lost focus. I can override this event on the form with javascript, but I can not send Ruby-AJAX request from javascipt code.
Is there any way to check username without adding additional controls (buttons, links) on the form?
You can use some JavaScript (this one written with jQuery) for AJAX cheking:
$(function() {
$('[data-validate]').blur(function() {
$this = $(this);
$.get($this.data('validate'), {
user: $this.val()
}).success(function() {
$this.removeClass('field_with_errors');
}).error(function() {
$this.addClass('field_with_errors');
});
});
});
This JavaScript will look for any fields with attribute data-validate. Then it assings onBlur event handler (focus lost in JavaScript world). On blur handler will send AJAX request to the URL specified in data-validate attribute and pass parameter user with input value.
Next modify your view to add attribute data-validate with validation URL:
<%= text_field_tag(:user, :'data-validate' => '/users/checkname') %>
Next add route:
resources :users do
collection do
get 'checkname'
end
end
And last step create your validation:
class UsersController < ApplicationController
def checkname
if User.where('user = ?', params[:user]).count == 0
render :nothing => true, :status => 200
else
render :nothing => true, :status => 409
end
return
end
#... other controller stuff
end
For what reason can you not send an ajax request from javascript code?
The best way would be to send a GET ajax request when the focus is lost. The get request could then return true or false and your javascript could then reflect this on the page.
I answered this in another post.
It is a friendly way for validating forms if you do not want to write it all from scratch using an existing jquery plugin. Check it out and if you like it let me know!
Check username availability using jquery and Ajax in rails
The solution that #Viacheslav has, works fine and my answer is a combination of his and my own changes (especially JS) part.
We will be using Ajax in order to achieve this.
Lets first create our function in the controller
def checkname
if !User.find_by_display_name(params[:dn])
render json: {status: 200}
else
render json: {status: 409}
end
return
end
and then adding our routes in routes.rb
resources :yourcontroller do
collection do
get 'checkname'
end
end
Now lets gets our hand on the view. Below you'll see the input:
.field
= f.text_field :display_name, onblur: "checkDisplayName.validate(this.value)"
%p.error-name.disp-none username exists
And now by help of JSwe get the magic rolling. Blow JS has few functions. validate does the actually validation. getStatus is our Ajax call to get the status and we use showError & disableSubmitButton to our form a bit more production ready to show errors and disabling the submit button.
var checkDisplayName = {
validate: function(dn){
checkDisplayName.getStatus(dn).then(function(result) {
if (!!result){
if (result.status != 200){
checkDisplayName.disableSubmitButton(true);
checkDisplayName.showError();
} else{
checkDisplayName.disableSubmitButton(false);
}
}
});
return false;
},
getStatus: async (dn) => {
const data = await fetch("/pages/checkname?dn=" + dn)
.then(response => response.json())
.then(json => {
return json;
})
.catch(e => {
return false
});
return data;
},
showError: function() {
let errEl = document.getElementsByClassName('error-name')[0];
if (!!errEl) {
errEl.classList.remove("disp-none");
window.setTimeout(function() { errEl.classList.add("disp-none"); },3500);
}
},
disableSubmitButton: function(status){
let button = document.querySelector('[type="submit"]');
button.disabled = status;
}
};

Resources