I'm trying to get user data to save to my Rails DB for user info through a sign up form on a React front end. I've got my route and controller written properly (at least I think) and the fetch request below but I keep getting a 500 internal error through on submit. Please let me know what I'm missing, any help would be greatly appreciated!
My route:
resources :users, only: [:show, :create]
My create action in UsersController:
def create
user = User.create!(user_params)
if user.valid?
session[:user_id] = user.id # remembering who our user is
render json: user, status: :ok
else
render json: {error: user.errors.messages}
end
end
and lastly my fetch request from the Signup.js component on the React frontend, where I'm getting the error on the line that has 'fetch'
fetch(`/users`,{
method:'POST',
headers:{'Content-Type': 'application/json'},
body:JSON.stringify(user)
})
.then(res => {
if(res.ok){
res.json().then(user => {
history.push(`/users/${user.id}`)
})
This might only be part of your problem, but first creating, then asking for validity is backwards.
Do something like this instead:
def create
user = User.new(user_params)
if user.save # <-- will return false if the save fails
user.reload
session[:user_id] = user.id # remembering who our user is
render json: user, status: :ok
else
render json: {error: user.errors.messages}
end
end
If you really want to check validity explicitly:
def create
user = User.new(user_params)
if user.valid? # <-- will check validity
user.save
user.reload
session[:user_id] = user.id # remembering who our user is
render json: user, status: :ok
else
render json: {error: user.errors.messages}
end
end
My guess is your error might be coming from the fact that your user variable doesn't actually have an ID yet. You need to save the record, then refresh it to get an ID.
Related
I am getting this error trying to update a card for my application. Here's a look at the rails server log. The ID is coming back "nil". I can't figure out why. Here is the PATCH update block of code:
function handleEdit(e) {
e.preventDefault()
fetch(`/items/${item.id}`,{
method: "PATCH",
body: JSON.stringify({
item: id,
bottle: bottle,
size: size,
count: count,
}),
headers: {
"Content-Type": "application/json",
},
})
.then(res=>res.json())
.then((updatedItem)=>setItem(updatedItem))
}
Here are all the routes:
resources :items, only: [:index,:show,:update]
get '/me', to: 'couriers#show'
get "/home", to: 'couriers#show'
get '/courier', to: 'couriers#show'
get '/items', to: 'items#index'
get 'items/:id', to: 'items#update'
get '/items/:id', to: 'items#show'
patch '/items/:id', to: 'items#update'
post '/login', to: 'sessions#create'
post '/signupform', to: 'couriers#create'
delete '/logout', to: 'sessions#destroy'
Here is the error log:
Started PATCH "/items/undefined" for 127.0.0.1 at 2022-08-01 19:27:47 -0400
Processing by ItemsController#update as */*
Parameters: {"item"=>nil, "bottle"=>"Glass", "size"=>"400", "count"=>"4", "id"=>"undefined"}
Item Load (0.2ms) SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2 [["id", nil], ["LIMIT", 1]]
↳ app/controllers/items_controller.rb:19:in `update'
Completed 404 Not Found in 4ms (Views: 0.2ms | ActiveRecord: 0.9ms | Allocations: 1234)
Here is the code in the ItemsController:
class ItemsController < ApplicationController
skip_before_action :authorize, except: :index
def index
items = Item.all
render json: items, status: :ok
end
def show
item=Item.find_by(id:session[:id])
if item
render json: item
else
render json: {error: 'Not Found'}, status: :not_found
end
end
def update
item = Item.find_by(id:params[:id])
if item
item.update(item_params)
render json: item
else
render json: { error: "Item not found" }, status: :not_found
end
end
private
def item_params
params.permit(:id,:item,:bottle, :size, :count)
end
end
If anyone has an idea or resolution, please share. I'm sure it's something I may be doing wrong, so anything helps.
Not super sure how React apps work, but should that ${items.id} in the patch code be ${item.id} (singular)?
Out of curiosity, where is the item (or items) var actually set? Again, I'm not familiar with React, but that's the source of the 'undefined' text being passed to the controller. Whatever is rendered in the ${ } section is undefined. It is being turned into the literal text 'undefined' and that is being sent to your controller.
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
I am writing my react flask-rest site and and encountered unusual behavior ... of something. So when i create a get request at the address domain/api/users/1, where 1 = id of deleted db element in postman my response consist of one null element(as written in the code). But when i create this request in fetch i get error 410. I have a check for the existence of an element in the code, but this response does not allow my code to execute and all the logic of the program breaks. Moreover, when using a fetch, information about such a request does not even appear in the flask log (when using postman, everything is fine). So maybe I write a lot of unnecessary information, but I really dont understand what is wrong
python code:
def get(self, id):
u = User.query.filter_by(id=id).first()
if u:
return {
'id': u.id,
'username': u.username,
'email': u.email}
if id is not exist return null (works with IDs that never existed)
js-react code:
const [user, setUser] = useState({});
useEffect(() => {
fetch(`/api/users/${match.params.id}`)
.then((res) => res.json())
.then((data) => setUser(data))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!user) {
return <h1>Same user dont exists</h1>;
}
return (
<>
<h1 className="mb-3">{user.username}</h1>
<div>{user.email}</div>
</>
);
Not sure if this solves your problem, but you can try adding an else: block in case the user id does not exist and return an empty dictionary
def get(self, id):
u = User.query.filter_by(id=id).first()
if u:
return {
'id': u.id,
'username': u.username,
'email': u.email}
else:
return {}
# alternatively:
# return {"message": "User does not exist"}, 404
I dont know why this works for you one way but not the other, but it seems like not returning anything at all might break your code. Alternatively, return a 404 not found if the user doesnt exist, and handle the case separately based on the status code.
Also; I dont know why or how your server returns a 410 GONE. If I test a route with no explicit return, I get a 200 OK and the response body is "null". There might be more code involved than you posted in your question.
I've searched around a lot but have been unable to find a simple way to get flash messages from Express and render them in React.
I need to access the data on my Express server, but what is the best way of storing this and passing it down to React? I was thinking of passing an object down when the React index.html file is rendered, but I'm not sure how I can access this data, or send the correct data when certain events happen, for example a user enters the wrong password.
I solved the issue.
I simply have a variable in my session called flash which is set to false by default.
In the correct part of the passport flow I redefine this to a string, depending on the error. I have a React action and reducer to get this data and if it's truthy, render it to the screen. When the component unmounts or the site is refreshed I reset it to false.
EDIT: I have found a better solution
1. In the passport middleware set an optional message if something goes wrong.
return done(null, false, { message: 'Email not found' });
2. In the login route send this information as a response.
router.post('/login', (req, res, next) => {
passport.authenticate('local-login', (e, user, info) => {
if(e) return next(e);
if(info) return res.send(info);
req.logIn(user, e => {
if(e) return next(e);
return res.send(user);
});
})(req, res, next);
});
3. Handle the submission and response in a Redux action generator. If the user authenticates, then the message property will be undefined.
const res = await axios.post('/auth/login', { email, password });
dispatch({
type: 'FLASH',
payload: res.data.message
});
4. In the reducer, the state will be either a string or false:
return action.payload || false;
5. Then it's a question of rendering the state to the screen. Another action can be sent when the component unmounts to reset the state.
Hope this helps someone else out there.
expressjs/flash will place an array of flash objects onto res.locals. Per the docs: https://github.com/expressjs/flash#reslocalsflash
res.locals.flash
An array of flash messages of the form:
{
"type": "info",
"message": "message"
}
From my understanding, anything placed on res.locals is available in the global scope. In other words, you should be able to do window.flash which should return an Array of flash objects.
So you would simply loop over the array as you would normally in JavaScript. That is just my guess.
const makeFlashElement = ({type, message}) => {
return (
<div>
<h1>message</h1>
<h2>type</h2>
</div>
)
}
for (message in flash) {
makeFlashElement(message)
// ...
}
Typically you'd return a JSON response which React can easily digest.
See Karl Taylor's comment.
I am trying to set up a minimal layer of authentication between my Rails backend and my React front end, but I am running into some problems.
I cannot seem to find the cookie key value that the server passes down to my client. In the network tab, I see it in the response: Set-Cookie:_skillcoop_session=...., but when I use js-cookie to look for the above cookie, _skillcoop_session, I only see one called identity-token=... and its value is different from _skillcoop_session. How do I access _skillcoop_session in the browser?
What header key do I pass up to the server to signal to my backend to use 'this' header key to match up with the session it has stored off? In this post, Justin Weiss seems to suggest that I make the request to the server with a header like: Cookie: _skillcoop_session=....
Am I doing this all wrong? Would I be better off using a gem like devise?
Also in order to load the session in my other controllers, I have had to do something like session['init'] = true, and I learned to do this from this SO post. This seems hacky. Why do I have to manually reload the session in separate controller actions after I've set it previously in a different controller action in a different request?
I'm currently just stubbing out the user and the authentication -- all I want to do to get the plumping in place is set a session[:user_id] and be able to read that session data in other controller actions. For this I have two main files for consideration: UsersController and Transport.js. In UsersController I am just stubbing the session[:user_id] with the number 1 and in Transport.js I'd like to pass the cookie received from the server so that the backend can maintain a session between requests with a client.
Here is my controller:
class UsersController < ApplicationController
def create
session[:user_id] = 1
render json: user_stub, status: :ok
end
def show
puts "user id: #{session[:user_id]}"
# should return, 1, but is returning, nil...why?
render json: user_stub, status: :ok
end
private
def user_stub
{
id: 1,
email: params['email'] || 'fakeemail#gmail.com',
password: params['password'] || 'fake password'
}
end
end
Here is the main location of my app where I make my request to the server - it's in an abstraction I call Transport.js:
require('es6-promise').polyfill();
require('isomorphic-fetch');
var cookie = require('js-cookie');
const GET = 'GET';
const POST = 'POST';
function Transport() {
}
Transport.prototype.get = function(url, options = {}) {
return this.query(GET, url, null, options);
};
Transport.prototype.post = function(url, dataString, options = {}) {
return this.query(POST, url, dataString, options);
};
Transport.prototype.query = function(method, url, dataString, options = {}) {
var data;
if (dataString) {
data = JSON.parse(dataString);
}
switch(method) {
case GET:
return fetch(url, Object.assign({headers: {'Cookie': cookie.get('_skillcoop_session')}}, options, {
method: method
}));
case POST:
return fetch(url, Object.assign({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}, options, {
method: method
}));
default:
throw new Error("This HTTP Method is not supported.");
}
};
module.exports = Transport;
According to this SO post, one cannot access the Set-Cookie header in JS. Thus, I suppose my attempts to handle Set-Cookie in the response headers was a fools effort.
According to the NPM package that I'm using to make HTTP requests, I need to pass {credentials: 'same-origin'} key value pair in the second argument to fetch, which will 'automatically send cookies for the current domain'. That did the trick -- the session object is available and contains the user_id that was set in the session in the previous request in a different action.
Yes. I changed up how I approached this problem. I leaned very heavily on this Reddit post. In short, I use ruby-jwt on the backend and store the token in localStorage on the front end. Each request out to the server will include the token in a header AUTHORIZATION.
In following steps 1 and 2, it looks like I no longer have to 'reload the session'.