Rails - POST request: Error 500 (Internal Server Error) - reactjs

I'm a beginner coder and I'm trying to create an application using React as my front end and Ruby on Rails for my back end. Anytime I press the sign up button and send the request to server I'm receiving an error and I cannot figure out what it is. Help!
Console:
SignUp.js:17
POST http://localhost:4000/users 500 (Internal Server Error)
SignUp.js:26
Response {type: 'basic', url: 'http://localhost:4000/users', redirected: false, status: 500, ok: false, …} body: (...) body Used: true headers: Headers {} ok: false redirected: false status: 500 statusText: "Internal Server Error" type: "basic" url: "http://localhost:4000/users" [[Prototype]]: Response
When trying to access localhost
GET http://localhost:3000/me 500 (Internal Server Error)
favicon.ico:1 GET http://localhost:3000/favicon.ico 500 (Internal Server Error)
Rails Server:
ArgumentError (wrong number of arguments (given 0, expected 1..2)):
app/controllers/users_controller.rb:2:in `<class:UsersController>'
app/controllers/users_controller.rb:1:in `<main>'
Started POST "/users" for 127.0.0.1 at 2022-11-02 23:09:07 -0400
ArgumentError (wrong number of arguments (given 0, expected 1..2)):
app/controllers/users_controller.rb:2:in `<class:UsersController>'
app/controllers/users_controller.rb:1:in `<main>'
React front end
import React from 'react';
import { useState } from "react";
function SignUp() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
function handleSignUp(e) {
e.preventDefault()
const user = {
username,
password
}
fetch("/users",{
method: "POST",
header: {
"Content-Type" : "application/json"
},
body: JSON.stringify(user)
}).then(r => {
r.json()
console.log(r)})
}
return (
<div>
<form onSubmit={handleSignUp}>
<p>Username</p>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<p>Password</p>
<input
type="text"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Sign Up</button>
</form>
</div>
);
}
export default SignUp;
Routes
Rails.application.routes.draw do
# Routing logic: fallback requests for React Router.
# Leave this here to help deploy your app later!
post "/login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
get "/me", to: "users#show"
post "/users", to: "users#create"
get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? }
end
Controller
class UsersController < ApplicationController
wrap_parameters
rescue_from ActiveRecord:RecordInvalid, with: :record_invalid_response
def create
user = User.create!(user_params)
render json: user, status: :created
rescue ActiveRecord::RecordInvalid => invalid
end
def show
user = User.find(session[:user_id])
if user
render json: user
else
render json: { error: "Not authorized" }, status: :unauthorized
end
end
private
def user_params
params.permit(:username, :password)
# :first_name, :last_name, :phone_number, :email
end
def record_invalid_response(user)
render json: {error: user.errors.full_messages}, status: :unprocessable_entity
end
end

:The error comes from the fact that wrap_parameters expects some arguments, see https://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html.
Now, for a JSON request, you don't need to call this function as Rails does it by default. But then I suppose Rails wraps your params with a user key and so you need to change your params processing, something like this should work:
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordInvalid, with: :record_invalid_response
def create
user = User.create!(user_params)
render json: user, status: :created
end
def show
user = User.find(session[:user_id])
if user
render json: user
else
render json: { error: "Not authorized" }, status: :unauthorized
end
end
private
def user_params
params.require(:user).permit(:username, :password)
# :first_name, :last_name, :phone_number, :email
end
def record_invalid_response(user)
render json: {error: user.errors.full_messages}, status: :unprocessable_entity
end
end
Note: I also removed the rescue from the create action as it would prevent your controller-level rescue_from from being used

Related

How to use Active Storage to add profile picture for User

Recently, I tried using active storage in Ruby on Rails to store profile picture for logged in user. I been following this documentation closely but, it didn't work out https://edgeguides.rubyonrails.org/active_storage_overview.html.
What I want is to allow users to register with only first name, last name, username, password, and email. Then, users can login and they can upload their own profile picture from profile page. After uploading, I want users to see their avatar in their profile.
My User.rb Model is looks like this:
class User < ApplicationRecord
has_secure_password
has_many :games, dependent: :destroy
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true
validates :password, presence: true
has_one_attached :avatar
end
I added "has_one_attached :avatar" like from the rails documentation and I'm using "set_avatar" method to attach avatar to an existing user.
My users_controller is looks like this:
class UsersController < ApplicationController
skip_before_action :authorized
wrap_parameters format: []
def index
users = User.all
render json: users
end
def find_user
user = User.find_by(id: params[:id])
if user
render json: user, status: :not_found
else
render json: { error: "Not found" }, status: :not_found
end
end
def show
user = User.find_by(id: session[:user_id])
if user
render json: user
else
render json: { error: "Not authorized" }, status: :unauthorized
end
end
def create
user = User.create(user_params)
if user.valid?
session[:user_id] ||= user.id
render json: user, status: :created
else
render json: {error: user.errors.full_messages }, status: :unprocessable_entity
end
end
def update
currentUser = User.find_by(id: params[:id])
if currentUser
currentUser.update(user_update_params)
render json: currentUser, status: :accepted
else
render json: {error: currentUser.errors.full_messages }, status: :unprocessable_entity
end
end
def destroy
user = User.find_by(id: params[:id])
if user
user.destroy
head :no_content
else
render json: {error: "user not found"}, status: :not_found
end
end
def set_avatar
user = User.find_by(id: params[:id])
if user
user.avatar.attach(params[:avatar])
else
render json: {error: "Profile image upload failed"}
end
end
private
def user_params
params.permit(:first_name, :last_name, :email, :username, :password, :avatar)
end
def user_update_params
params.permit(:first_name, :last_name, :email, :username, :password)
end
end
My routes:
Rails.application.routes.draw do
# resources :game_memos
# resources :memos
# resources :games
# resources :users
resources :sessions
#--------------USER------------------------------
# Create Session
get '/login', to: "sessions#create"
# Show all users
get '/users', to: "users#index"
# Register new user
post '/users', to: "users#create"
# Login User
post "/login", to: "sessions#create"
# Logout user
delete '/logout', to: "sessions#destroy"
# Update User profile
patch '/users/:id', to: "users#update"
# Keep user logged in
get '/me', to: "users#show"
# Get request for find user based on User id
get '/users/:id', to: "users#find_user"
# Save avatar
post '/users/:id', to: "users#set_avatar"
# -------------GAMES Routes----------------------
get '/games', to: 'games#index'
# Show all the games that belongs to logged in user
get '/users/:id/games', to: "games#show"
# Create a new wishlist
post '/games', to: "games#create"
# Delete a game from wishlist
delete '/users/:id/games/:id', to: "games#destroy"
#-------------Memo Routes------------------------
# Show memos that belongs to a game
get '/games/:id/memos', to: "memos#show"
# Post memos to a game
post '/games/:id/memos', to: "memos#create"
end
I'm uploading my avatar from this component:
import React, { useState } from "react";
// I'm getting default image from this location just for now
import IMAGES from '../images/Image';
export default function ProfilePicture({currentUser}){
const[profileAvatar, setAvatar] = useState([])
const fileTypes = [
"image/apng",
"image/bmp",
"image/gif",
"image/jpeg",
"image/pjpeg",
"image/png",
"image/svg+xml",
"image/tiff",
"image/webp",
"image/x-icon"
];
function handleSubmit(e){
// fetch(`/users/${currentUser.id}` ,{
// method: "POST",
// headers: {"Content-Type": "application/json"},
// body: JSON.stringify({avatar:profileAvatar})
// })
// .then((r) => r.json())
// .then(data => {
// if (data.errors) {
// alert(data.errors)
// }
// else {
// setAvatar(data)
// }
// })
}
return(
<div>
<img src={IMAGES.defaultProfile} alt="default_profile_image" className="profile_avatar"/>
<form onSubmit={handleSubmit}>
<input type="file" id="avatar" name="avatar" accept={fileTypes} onChange={(e)=>setAvatar(e.target.value)}/>
<button type="submit">Submit</button>
</form>
</div>
)
}
Lastly, my Users migration file:
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
t.string :username
t.string :password_digest
t.string :avatar
t.timestamps
end
end
end
I wasn't sure where to ask this kind of question. I never used Active Storage before and this is my first time using it. Please help.
Firstly you don't need t.string :avatar. ActiveStorage attachments are not columns of owner table but records in associated tables
ActiveStorage provides Direct Upload if you use JS frameworks like React
From docs:
If you want to use the Direct Upload feature from a JavaScript framework, or you want to integrate custom drag and drop solutions, you can use the DirectUpload class for this purpose. Upon receiving a file from your library of choice, instantiate a DirectUpload and call its create method. Create takes a callback to invoke when the upload completes.
import { DirectUpload } from "#rails/activestorage"
const input = document.querySelector('input[type=file]')
// Bind to file drop - use the ondrop on a parent element or use a
// library like Dropzone
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// Bind to normal file selection
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// you might clear the selected files from the input
input.value = null
})
const uploadFile = (file) => {
// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// Add an appropriately-named hidden input to the form with a
// value of blob.signed_id so that the blob ids will be
// transmitted in the normal upload flow
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
Pay attention to const url = input.dataset.directUploadUrl. You need to set data-direct-upload-url attribute as your upload url, by default it is /rails/active_storage/direct_uploads
hiddenField.setAttribute("value", blob.signed_id) from this example is a Rails magic
When the file is uploaded, Rails return blob with signed_id to frontend. Using it, you can attach file to the record
In your case
fetch(`/users/${currentUser.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({avatar: blob.signed_id}) // blob.signed_id we get after file uploading
})
On backend
def set_avatar
user = User.find(params[:id])
user.update(avatar: params[:avatar])
render json: { avatar: rails_blob_path(user.avatar) }
end
So pass blob.signed_id and avatar will be attached automatically
Probably this tutorial will be helpful for you
Like Mechnicov's answer you don't need t.string :avatar
in the schema the
has_one_attached :avatar is already taking care of that.
If you want to attach the photo through the backend and not direct upload try using this
user.avatar.attach(io: file, filename: 'photo.png', content_type: 'image/png')
and make sure that the client is sending the photo through formdata and not through the body
Step 1: Create a FormData instance
let formData = new FormData();
Step 2: Append the data in it
formData.append('file_to_upload', fileRef.files[0]); // fileRef is the input file reference

Sending data properly from React to Ruby on Rails API Endpoint

I'm working on posting data from a React Form to a Ruby on Rails API, about the React part, if just send the first item from an array using this code:
const submitOrderHandler = async (userData) => {
setIsSubmitting(true);
await fetch("http://localhost:3000/ordertemps", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cartCtx.items[0]),//please include user: userData
});
setIsSubmitting(false);
setDidSubmit(true);
cartCtx.clearCart();
};
The Ruby on Rails API manage it and store it in the table, this is the output:
However, I need to store all the data selected by the user, so, to accomplish this task I updated my code like this:
const submitOrderHandler = async (userData) => {
const dataSelected = JSON.stringify(cartCtx.items);
console.log(dataSelected);
setIsSubmitting(true);
await fetch("http://localhost:3000/ordertemps", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cartCtx.items),//please include user: userData
});
setIsSubmitting(false);
setDidSubmit(true);
cartCtx.clearCart();
};
The problem is I'm getting a 400 Status, so this is how the data looks from the FrontEnd:
This is the output from the Ruby on Rails Endpoint:
the source code of the Controller in charge to manage and store the data is this:
#POST /ordertemps
def create
#ordertemp = Ordertemp.new(ordertemp_params)
if #ordertemp.save
render json: #ordertemp
else
render error: { error: 'Unable to create an order'}, status: 400
end
end
private
def ordertemp_params
#params.require(:ordertemp).permit( :clientName, :clientId, :amount, :mealid, :name, :price)
params.require(:ordertemp).permit(:name, :amount, :price)
end
So, I'm assuming these facts:
the data is properly formatted from the FrontEnd.
For some reason my Ruby on Rails'Controller can't manage more than one element sent by the front end.
My question is: what am I missing in my controller to store all the data sent by the FrontEnd?
Thanks a lot for your comments
Update your controller code as below:
#POST /ordertemps
def create
begin
params['_json'].each do |ordertemp_params|
#ordertemp = Ordertemp.new(ordertemp_params)
#ordertemp.save
end
head :no_content
rescue => e
render json: { error: unable to create orders }, status: 400
end
end
Hope this will help you.

Trying to upload image from react through a rails API renders Unpermitted parameter: :image error

I am trying to upload an image from React using rails active storage. The problem is I am receiving the following error when making the request,
ArgumentError (Could not find or build blob: expected attachable, got <ActionController::Parameters {} permitted: false>):
The controller with the create action.
def create
micropost = #current_user.microposts.build(micropost_params)
micropost.image.attach(params[:micropost][:image])
binding.pry
if micropost.save
render json: micropost
else
render json: { errors: micropost.errors }
end
end
I am using strong parameters.
def micropost_params
params.require(:micropost).permit(:content, :image)
end
This is how the form looks like in the frontend
<input
type="file"
accept="image/*"
multiple={false}
onChange={(event) => setImage(event.target.files[0])}
/>
The request I make in the frontend.
export async function createMicropost(content, image) {
console.log(image);
return request({
url: "/micropost/new",
method: "POST",
data: {
micropost: {
image: image,
content: content,
},
},
});
}
I have also tried replacing :image with image: {} in micropost_params, it stopped rendering the Unpermitted parameter: :image error when I used binding.pry but the 500 still remained with the same error as in the first image.

How do i hookup a React button to authenticate with Devise Token Auth + OmniAuth

I'm having trouble connecting my Devise Token Auth with a token I get back from google in react.
I'm using this package for the button:
https://www.npmjs.com/package/react-google-login
This is the auth I'm trying to set up:
https://github.com/lynndylanhurley/devise_token_auth/blob/master/docs/config/omniauth.md
I'm getting a response from google with the react button but I have no idea how that information has to translate to go back to the devise auth.
Information online is severely lacking between these 2 technologies. What it comes down to is how to translate this ruby tag into react:
<%= link_to "Sign in with Google", user_google_oauth2_omniauth_authorize_path, method: :post %>
I know this is old but here are my 2 cents.
I have used this gem OmniAuth Google OAuth2. The information is pretty clear. In my project, I manage my token using JWT while still storing the access and refresh tokens from Google.
Backend
# config/initializers/devise.rb
config.omniauth :google_oauth2,
ENV['GOOGLE_CLIENT_ID'],
ENV['GOOGLE_CLIENT_SECRET'],
{ scope: "userinfo.email, userinfo.profile",
prompt: 'select_account',
image_aspect_ratio: 'square',
provider_ignores_state: true,
}
# controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
#user = User.from_omniauth(request.env['omniauth.auth'])
if #user.persisted?
# My personal token
token = issue_token(#user)
render json: { success: true,
user: #user,
token: token,
google_token: #user.access_token,
message: "Logged in successfully." }
else
render json: { success: false,
error: #user.errors.full_messages.join("\n"),
message: 'Google sign in unsuccessful.' }
end
end
def failure
set_flash_message! :alert, :failure, kind: OmniAuth::Utils.camelize(failed_strategy.name), reason: failure_message
render json: { success: false, message: 'Google authentication failed.', reason: failure_message, kind: OmniAuth::Utils.camelize(failed_strategy.name) }
end
private
end
# In User.rb
def self.from_omniauth(auth)
user = User.where(email: auth.info.email).first
if user
user = User.update(id: user.id,
refresh_token: auth.credentials.refresh_token,
access_token: auth.credentials.token,
uid: auth.uid,
)
else
# Create a username from names, append incremental if username already exists
username ||= auth.info.first_name + auth.info.last_name
username = username.delete('^a-zA-Z0-9_').downcase
num = 1
until User.find_by(username: username).nil?
username = "#{username}#{num}"
num += 1
end
user = User.create(email: auth.info.email,
uid: auth.uid,
refresh_token: auth.credentials.refresh_token,
access_token: auth.credentials.token,
provider: auth.provider,
password: Devise.friendly_token[0, 20],
firstname: auth.info.first_name,
lastname: auth.info.last_name,
username: username,
)
end
user
end
# routes.rb
# User Routes: Devise
devise_for :users,
path_names: {
sign_in: 'login',
sign_out: 'logout',
# sign_up: 'register'
},
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations',
omniauth_callbacks: 'users/omniauth_callbacks'
}
Above routes translations
user_google_oauth2_omniauth_authorize_path GET|POST /api/users/auth/google_oauth2(.:format)
users/omniauth_callbacks#passthru
user_google_oauth2_omniauth_callback_path GET|POST /api/users/auth/google_oauth2/callback(.:format)
users/omniauth_callbacks#google_oauth2
Here is the front end
<!-- index.html -->
<head>
<script src="https://apis.google.com/js/platform.js?onload=init" async defer></script>
</head>
Do not worry about defining the gapi function, it is loaded script in the above
// RegisterContent.js
const RegisterContent = function RegisterContent() {
function handleResponse(response) {
// Save user to redux store and all the tokens to cookies
}
// callback
function signInCallback(authResult) {
if (authResult.code) {
const params = { code: authResult.code }
const path = "localhost:3000/api/users/auth/google_oauth2/callback";
// This just handdles posting with axios
postResource(path, params, handleResponse);
}
}
// This will prompt opening the google window and returns a callback upon success
const googleHandler = () => {
googleAuth.grantOfflineAccess().then(signInCallback);
};
useEffect(() => {
// Initialize the GoogleAuth object
gapi.load("auth2", function foo() {
const auth = gapi.auth2.init({
client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
scope: "email profile",
});
setGoogleAuth(auth);
console.log("Init");
});
}, []);
return (
<Button onclick={googleHandler}>
Continue with Google
</Button>
);
}
A few resources to help Google Sign-In JavaScript client reference,
How to integrate Google API into your React app and that's it.
It's in the end just a post request to that endpoint but I have encountered the same problems as you are.
What you need to do is to create a form like this:
<form action="<%=user_google_oauth2_omniauth_authorize_path %>" method="post">
<input type="hidden" name="authenticity_token" value="XX">
<button type="submit">Connect Google</button>
</form>
My trials failed when I haven't passed the auth token or added a "skip_before_action :verify_autneticity_token" to the callback controller. You need to fill the correct authenticity token, then it works.
Authenticity token information can be added to your html page's head section, via <%= csrf_meta_tags %>. Then you will need to parse the dom for the meta fields to fill them correctly.

empty POST request with react/axios

I'm new to node.js and react. I've built a back end api to expose CRUD endpoints for a Products model with three fields: Title, Description and Price. I've used node/express for the server with an SQLite db file.
I've tested the endpoints using postman and can successfully access all the products, retrieve a single product, add a new product (but only with the header set to application/x-www-form-urlencoded), and delete a product.
I'm loosely following a tutorial building the front end with react and using axios to make http requests. I can read without issue so get all and get one by ID is working.
However, I've hit a wall with my post request. If I send a post request I get this error backend in the api server console -
Unhandled rejection SequelizeValidationError: notNull Violation: product
And on the front end after about a couple of minutes from sending the request I get:
XHR failed loading: POST "http://localhost:3000/api/products/new".
dispatchXhrRequest # xhr.js:178
xhrAdapter # xhr.js:12
dispatchRequest # dispatchRequest.js:59
Promise resolved (async)
request # Axios.js:51
Axios.(anonymous function) # Axios.js:71
wrap # bind.js:9
NewProduct._this.createHandler # ProductNew.js:25
callCallback # react-dom.development.js:542
invokeGuardedCallbackDev # react-dom.development.js:581
invokeGuardedCallback # react-dom.development.js:438
invokeGuardedCallbackAndCatchFirstError # react-dom.development.js:452
executeDispatch # react-dom.development.js:836
executeDispatchesInOrder # react-dom.development.js:858
executeDispatchesAndRelease # react-dom.development.js:956
executeDispatchesAndReleaseTopLevel # react-dom.development.js:967
forEachAccumulated # react-dom.development.js:935
processEventQueue # react-dom.development.js:1112
runEventQueueInBatch # react-dom.development.js:3607
handleTopLevel # react-dom.development.js:3616
handleTopLevelImpl # react-dom.development.js:3347
batchedUpdates # react-dom.development.js:11082
batchedUpdates # react-dom.development.js:2330
dispatchEvent # react-dom.development.js:3421
09:59:59.293 ProductNew.js:30
Error: Network Error
at createError (createError.js:16)
at XMLHttpRequest.handleError (xhr.js:87)
If I remove the validation a new product is successfully added to the database, but all the values are null.
This is my ProductNew.js file:
import React from 'react';
import axios from'axios';
import ButtonAdd from '../components/Buttons/ButtonAdd';
class NewProduct extends React.Component {
state = {
title: '',
description: '',
price: ''
}
createHandler = () => {
const data = {
Title: this.state.title,
Description: this.state.description,
Price: this.state.price
};
console.log('Raw data is: ' + Object.entries(data));
// => Raw data is: Title,burger,Description,bun,Price,5
const header = {
ContentType: 'application/x-www-form-urlencoded',
Accept: 'application/json'
};
const querystring = require('querystring');
console.log(querystring.stringify(data));
// => Title=burger&Description=bun&Price=5
console.log('Data is:' + JSON.stringify(data));
// => Data is:{"Title":"burger","Description":"bun","Price":"5"}
axios.post('/api/products/new', querystring.stringify(data), header)
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
}
render () {
return (
<div className='NewPost'>
<h1>Add a Product</h1>
<label>Title</label>
<textarea rows='4' value={this.state.title} onChange={(event) => this.setState({title: event.target.value})} />
<label>Description</label>
<textarea rows='6' value={this.state.description} onChange={(event) => this.setState({description: event.target.value})} />
<label>Price</label>
<input type='number' value={this.state.price} onChange={(event) => this.setState({price: event.target.value})} />
// ButtonAdd.js onClick={props.create} added to <Button> tag
<ButtonAdd create={this.createHandler}>Add New Product</ButtonAdd>
</div>
);
}
}
export default NewProduct;
I found this issue on github which I hoped would have solved my problem. I've added various console.log to follow what is happening with the state data through the script but I can't see why my POST request is empty. I'd appreciate any pointers as to what I'm doing wrong.
Maybe it will help you somehow. I think you have a problem with your axios request structure.
Docs say: axios.post(url[, data[, config]])
Instead of putting headers directly, as the last parameter:
axios.post('/api/products/new', querystring.stringify(data), header)
you should have a config object that contains headers and then pass it as the last parameter:
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
};
axios.post('/api/products/new', querystring.stringify(data), config)
It appears that you may be using an incorrect Content type. The proper content type should be application/json.
The following answer has an example of the differing formats that are passed by the various content types, which may be contributing to your issues: differences in application/json and application/x-www-form-urlencoded

Resources