API Rails : How to make a RESTFul API using Devise & JWT? - ruby-on-rails

I'm trying to build a RESTFul API with devise & jwt.
I can register, and login/logout using my jwt bear token, using Postman.
Now I have a problem when I want to POST an Article.
I dont understand why my console goes for a login after I POST an Article with Postman.
Also I dont understand why I get this 401 error. It's really hard to find some content with RESTFul + API + Devise + JWT.
Do you think it's better in the long term to run with or without Devise ? Cause there is actually some content without Devise.
What I try on Postman
Authorization : <Bearer token>
{
"title":"the title",
"content":"the content"
}
Returned ERROR message from the console when I post an Article with Postman ( and with a the same bear token as for login/logout
Started POST "/articles" for ::1 at 2021-09-01 18:07:41 +0200
Processing by ArticlesController#create as */*
Parameters: {"title"=>"the title", "content"=>"the content", "article"=>{"title"=>"the title", "content"=>"the content"}}
Completed 401 Unauthorized in 76ms (Allocations: 113)
Started GET "/api/login" for ::1 at 2021-09-01 18:07:41 +0200
Processing by SessionsController#new as JSON
Completed 200 OK in 71ms (Views: 2.5ms | Allocations: 179)
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_todo, only: [:show, :update, :destroy]
before_action :authenticate_user!
# GET /todos
def index
#articles = Article.all
json_response(#articles)
end
# POST /todos
def create
#article = Article.create!(article_params)
#article.user = current_user
end
# GET /todos/:id
def show
json_response(#article)
end
# PUT /todos/:id
def update
#article.update(article_params)
head :no_content
end
# DELETE /todos/:id
def destroy
#article.destroy
head :no_content
end
private
def article_params
# whitelist params
params.permit(:title, :content, :user_id)
end
def set_article
#article = Article.find(params[:id])
end
end
db/shema.rb
ActiveRecord::Schema.define(version: 2021_09_01_124211) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "articles", force: :cascade do |t|
t.string "title"
t.text "content"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "user_id", null: false
t.index ["user_id"], name: "index_articles_on_user_id"
end
create_table "jwt_denylist", force: :cascade do |t|
t.string "jti", null: false
t.datetime "expired_at", null: false
t.index ["jti"], name: "index_jwt_denylist_on_jti"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "articles", "users"
end
app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
end
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
has_many :articles
end

Related

Problem with the logout on Rails API + Devise + JWT

Im building a Rails API. All my devise routes working fine, execpt for one : the logout.
I followed this tutorial : https://jameschambers.co.uk/rails-api
I already tried few things, like :
tried the api/logout.json route
tried to change rb #111 for : config.skip_session_storage = [:http_auth, :params_auth] ( config/initializers/devise.rb )
tried to change rb #280 for : config.sign_out_via = :get ( config/initializers/devise.rb )
Im sending a DELETE request on this address : localhost:3000/api/logout, with a JWT/Bearer token.
error
"status": 500,
"error": "Internal Server Error",
"exception": "#<ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column jwt_denylist.exp does not exist\nLINE 1: ...jwt_denylist\" WHERE \"jwt_denylist\".\"jti\" = $1 AND \"jwt_denyl...
rails routes
destroy_user_session DELETE /api/logout(.:format) sessions#destroy {:format=>:json}
db/schema.rb
ActiveRecord::Schema.define(version: 2021_09_01_233823) do
create_table "jwt_denylist", force: :cascade do |t|
t.string "jti", null: false
t.datetime "expired_at", null: false
t.index ["jti"], name: "index_jwt_denylist_on_jti"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end
app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
private
def respond_with(resource, _opts = {})
render_jsonapi_response(resource)
end
def respond_to_on_destroy
head :no_content
end
end
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
end
config/routes.rb
Rails.application.routes.draw do
default_url_options :host => "http://localhost:3000"
namespace :api, defaults: { format: :json } do
resources :users, only: %w[show]
end
devise_for :users,
defaults: { format: :json },
path: '',
path_names: {
sign_in: 'api/login',
sign_out: 'api/logout',
registration: 'api/signup'
},
controllers: {
sessions: 'sessions',
registrations: 'registrations'
}
end
The error ERROR: column jwt_denylist.exp does not exist indicates it's looking for a column (exp) that doesn't exist on the jwt_denylist table.
Your schema shows you have an expired_at but no exp column.
From the jwt-denylist gem the schema it expects exp. Perhaps the tutorial is out of date? Here's what the gem's documentation recommends:
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
So i would rename the :expired_at column to :exp, e.g.
rails generate migration rename_expired_at_to_exp_on_jwt_denylist and make the change method something like:
def change
rename_column :jwt_denylist, :expired_at, :exp
end
It works, thanks Melcher !! :)
This is what the migration looked like
class CreateJwtDenylist < ActiveRecord::Migration[6.1]
def change
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :expired_at, null: false
end
add_index :jwt_denylist, :jti
end
end
This is what is looks like now
class CreateJwtDenylist < ActiveRecord::Migration[6.1]
def change
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
end
end

After being saved the foreign_key = nil - Solving error in db

I save the #booking with a user (called "booker"). Right after the #booking.save I can retrieve #booking.booker in the command line that display all the properties from the user (email, password, id, etc.). However After leaving the create method, impossible to retrieve it (for example from the show) : #booking.booker = nil .
I guess that commes from a mistake in my booking model : I have belongs_to and has_many_through. If the error comes from here, how to solve it without having to change all the db?
booking_controller.rb
class BookingsController < ApplicationController
before_action :set_booking, only: [:show, :edit, :update ]
before_action :set_booking_format, only: [:destroy ]
def index
end
def my_bookings
#bookings = BookingPolicy::Scope.new(current_user, Booking).scope.where(booker: current_user)
end
def show
authorize #booking
end
def new
#garden = Garden.find(params[:garden_id])
#booking = Booking.new
authorize #booking
end
def create
#garden = Garden.find params[:garden_id]
#booking = #garden.bookings.build(booker: current_user)
authorize #booking
if #booking.save
redirect_to garden_booking_path(#booking, current_user)
end
end
def update
end
private
def set_booking
#booking = Booking.find(params[:id])
end
def set_booking_format
#booking = Booking.find(params[:format])
end
def booking_params
params.require(:booking).permit(:garden_id, :booker_id, :date)
end
end
booking.rb
class Booking < ApplicationRecord
belongs_to :garden
belongs_to :booker, class_name: "User"
end
garden.rb
class Garden < ApplicationRecord
belongs_to :user
has_many :bookings, dependent: :destroy
end
user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :gardens
has_and_belongs_to_many :bookings
end
schema.rb
create_table "bookings", force: :cascade do |t|
t.date "date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "garden_id"
t.integer "booker_id"
t.index ["garden_id"], name: "index_bookings_on_garden_id"
end
create_table "gardens", force: :cascade do |t|
t.string "title"
t.text "details"
t.integer "surface"
t.text "address"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "availabilities"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "admin", default: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "bookings", "gardens"
end
In your model, user.rb:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :gardens
has_and_belongs_to_many :bookings
end
The :bookings association should be has_many. You aren't using a join table.
See: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
The belongs_to part of the habtm association is looking for a foreign key, which doesn't exist. You can retrieve #booking.booker before moving to a different controller action because you aren't hitting the database at all, you're just retrieving the instance variables' association.

NoMethodError (undefined method `posts' for nil:NilClass): app/controllers/posts_controller.rb:44:in `set_post'

I'm building a rails backend for an ember blog project. I'm working on authenticating user actions via rails so people can't delete things they didn't post. With my current setup I'm getting the error you see in the title when I go to delete.
I've included the post controller, the openreadcontroller that it inherits from, the models for posts, comments and users, because the relationships between them might be making "current_user" a nil value, and I included the schema, because there's always a chance I bungled a migration or something.
I suspect I'm failing to identify the user when it comes time to execute the destroy method. I have a before action that sets the #posts to current_user.posts and this is where the error occurs. If this is indeed the issue, how do I identify current_user?
Thanks in advance for the help!
class PostsController < OpenReadController
before_action :set_post, only: [:show, :update, :destroy]
before_action :authenticate, only: [:create, :update, :destroy]
# GET /posts
def index
#posts = Post.all
render json: { posts: Post.all, comments: Comment.all }
end
# GET /posts/1
def show
render json: { post: #post, comments: #post.comments }
end
# POST /posts
def create
#post = current_user.posts.build(post_params)
if #post.save
render json: { post: #post, comments: #post.comments }, methods: :comment_ids, status: :created, location: #post
else
render json: #post.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /posts/1
def update
if #post.update(post_params)
render json: #post
else
render json: #post.errors, status: :unprocessable_entity
end
end
# DELETE /posts/1
def destroy
#post.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
#post = current_user.posts.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def post_params
params.require(:post).permit( :title, :body)
end
end
```
# frozen_string_literal: true
# Inherit from this class to allow unauthenticate access to read actions
class OpenReadController < ProtectedController
READ_ACTIONS = [:index, :show].freeze
skip_before_action :authenticate, only: READ_ACTIONS
before_action :set_current_user, only: READ_ACTIONS
end
```
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
belongs_to :user
end
```
# frozen_string_literal: true
class User < ApplicationRecord
include Authentication
has_many :posts
has_many :comments, through: :posts
end
```
class Comment < ApplicationRecord
belongs_to :post
end
```
ActiveRecord::Schema.define(version: 20170616000945) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "comments", force: :cascade do |t|
t.string "author"
t.text "body"
t.integer "post_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["post_id"], name: "index_comments_on_post_id", using: :btree
end
create_table "examples", force: :cascade do |t|
t.text "text", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_examples_on_user_id", using: :btree
end
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "body"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["user_id"], name: "index_posts_on_user_id", using: :btree
end
create_table "user_coins", force: :cascade do |t|
t.string "name"
t.decimal "price"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_user_coins_on_user_id", using: :btree
end
create_table "users", force: :cascade do |t|
t.string "email", null: false
t.string "token", null: false
t.string "password_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
t.index ["token"], name: "index_users_on_token", unique: true, using: :btree
end
add_foreign_key "comments", "posts"
add_foreign_key "examples", "users"
add_foreign_key "posts", "users"
add_foreign_key "user_coins", "users"
end
```
Here is the chrome console data, though I'm not sure it's useful.
ember.debug.js:16905 Ember Data Request DELETE
http://localhost:4741/posts/32 returned a 500
Payload (application/json; charset=utf-8)
[object Object]
Error
at ErrorClass.AdapterError (http://localhost:7165/assets/vendor.js:88496:16)
at ErrorClass (http://localhost:7165/assets/vendor.js:88521:24)
at Class.handleResponse (http://localhost:7165/assets/vendor.js:89804:22)
at Class.handleResponse (http://localhost:7165/assets/vendor.js:73690:28)
at Class.superWrapper [as handleResponse] (http://localhost:7165/assets/vendor.js:50205:22)
at ajaxError (http://localhost:7165/assets/vendor.js:90169:25)
at Class.hash.error (http://localhost:7165/assets/vendor.js:89838:23)
at fire (http://localhost:7165/assets/vendor.js:3632:31)
at Object.fireWith [as rejectWith] (http://localhost:7165/assets/vendor.js:3762:7)
at done (http://localhost:7165/assets/vendor.js:9589:14)
```
And here for good measure is my server log.
Started DELETE "/posts/32" for 127.0.0.1 at 2017-06-17 00:57:01 -0400
Processing by PostsController#destroy as HTML
Parameters: {"id"=>"32"}
Completed 500 Internal Server Error in 3ms (ActiveRecord: 0.0ms)
NoMethodError (undefined method `posts' for nil:NilClass):
app/controllers/posts_controller.rb:44:in `set_post'
​
current_user nil in your set_post method. You are not setting current_user for destroy action in OpenReadController. So try change before_action :set_current_user, only: READ_ACTIONS to before_action :set_current_user, except: READ_ACTIONS.
# frozen_string_literal: true
# Inherit from this class to allow unauthenticate access to read actions
class OpenReadController < ProtectedController
READ_ACTIONS = [:index, :show].freeze
skip_before_action :authenticate, only: READ_ACTIONS
before_action :set_current_user, except: READ_ACTIONS
end

Ruby on Rails SQLite3::ConstraintException: NOT NULL constraint failed:

I am developing a simple app where a user can add a subject to a cart. Before I add the authentication I was able to add a subject to the cart but as I want a user to be able to has access to just his/her cart I used Devise to create User with authentication. Now, when I click on the button to add a subject to the cart I have the following error:
This is a snapshot of the error I get: 1
SQLite3::ConstraintException: NOT NULL constraint failed: carts.user_id: INSERT INTO "carts" ("created_at", "updated_at") VALUES (?, ?)
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
def current_cart
Cart.find(params[:user_id])
rescue ActiveRecord::RecordNotFound
cart = Cart.create
params[:user_id] = cart.id
cart
end
end
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_one :cart
end
class Cart < ActiveRecord::Base
belongs_to :user
has_many :line_items, dependent: :destroy
scope :user_carts, ->(user) { where(['user_id= ?', user.id]) }
end
class AddUsersToCarts < ActiveRecord::Migration
def up
add_reference :carts, :user, index: true
Cart.reset_column_information
user = User.first
Cart.all.each do |cart|
cart.user_id = user.id
cart.save!
end
change_column_null :carts, :user_id, false
add_foreign_key :carts, :users
end
def down
remove_foreign_key :carts, :users
remove_reference :carts, :user, index: true
end
end
Edit: I added the schema.rb below:
ActiveRecord::Schema.define(version: 20151210213408) do
create_table "carts", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
end
add_index "carts", ["user_id"], name: "index_carts_on_user_id"
create_table "line_items", force: :cascade do |t|
t.integer "subject_id"
t.integer "cart_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "subjects", force: :cascade do |t|
t.string "title", null: false
t.string "code", null: false
t.text "description", null: false
t.integer "credits", null: false
t.string "lecturer", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "subjects", ["title"], name: "index_subjects_on_title", unique: true
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
You current_cart method does not make much sense.
You cannot find the user's cart by calling Cart.find(params[:user_id]), because that looks for a cart by an id (not by an user_id).
Cart.create fails, because you do not provide an user_id that is required (your database migrations says that the filed cannot be null).
Furthermore, params[:user_id] = cart.id changes the params hash, but not the new cart.
Change that method to something like this (using find_or_create_by) and use the current_user.id instead of params[:user_id]:
def current_cart
Cart.find_or_create_by(user_id: current_user.id)
end

Issue with belongs_to and user (devise)

I'm new to RoR, and I would like to develop an app, but I have an issue with the belongs_to association. I am using devise for the authentication of my users, and I have an object called timesheet, I followed several tutorials and read a lot of forums but unfortunately user_id remains null in my db, so I do not know where does the problem come from.
If you can tell how to fix it, any links that can helps me, that would be great.
Schema.rb:
ActiveRecord::Schema.define(version: 20150128160116) do
create_table "timesheets", force: true do |t|
t.date "date"
t.time "working_start_time"
t.time "working_end_time"
t.integer "breaks"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "timesheets", ["user_id"], name: "index_timesheets_on_user_id"
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.integer "current_sign_in_ip"
t.integer "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
timesheets_controller.rb
class TimesheetsController < ApplicationController
layout 'application'
def show
#timesheet=Timesheet.find(params[:id])
end
def index
#timesheet = Timesheet.all
end
def new
#timesheet = Timesheet.new
end
def create
#timesheet = Timesheet.create(timesheet_params)
redirect_to new_timesheet_path
end
def edit
#timesheet=Timesheet.find(params[:id])
end
def update
#timesheet = Timesheet.find(params[:id])
#timesheet.update_attributes(timesheet_params)
redirect_to student_table_path
end
user.rb model
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :timesheets
end
Timesheet.rb model
class Timesheet < ActiveRecord::Base
belongs_to :user
validates :user_id, :presence => true
end
Thanks in advance.
It will stay null because you are not using it in the timesheetsController, your create action should be like this :
def create
#timesheet = current_user.timesheets.build(timesheet_params)
redirect_to new_timesheet_path
end
You have to use that build method to reference the current_user, so the timesheet will have the current_user in the user_id field.

Resources