Initial commit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
module Api
|
||||
module V1
|
||||
class AdminController < ApplicationController
|
||||
include AuthorizeRequest
|
||||
include AuthorizeRole
|
||||
|
||||
before_action :require_admin
|
||||
|
||||
# example admin-only endpoint
|
||||
# GET /api/v1/admin/dashboard
|
||||
def dashboard
|
||||
render json: {
|
||||
message: "Welcome to admin dashboard",
|
||||
stats: {
|
||||
total_users: User.count,
|
||||
total_admins: User.admin.count,
|
||||
total_moderators: User.moderator.count
|
||||
}
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
module Api
|
||||
module V1
|
||||
class AuthController < ApplicationController
|
||||
include AuthorizeRequest
|
||||
skip_before_action :authorize_request, only: %i[signup login refresh]
|
||||
|
||||
# post /signup -> signup user and return jwt + refresh token
|
||||
# I used strong params so no sql injection here (rails got your back)
|
||||
def signup
|
||||
user = User.new(user_params)
|
||||
if user.save
|
||||
tokens = generate_tokens(user)
|
||||
render json: { **tokens, user: user.as_json(only: %i[id email role]) }, status: :created
|
||||
else
|
||||
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# post /login -> login with email & password (keep it simple)
|
||||
# if it matches, it gives you both access token and refresh token
|
||||
def login
|
||||
user = User.find_by(email: params[:email])
|
||||
if user&.valid_password?(params[:password])
|
||||
tokens = generate_tokens(user)
|
||||
render json: { **tokens, user: user.as_json(only: %i[id email role]) }, status: :ok
|
||||
else
|
||||
render json: { error: "Invalid email or password" }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
# post /refresh -> exchange refresh token for new access token
|
||||
# keeps users logged in without re-entering credentials
|
||||
def refresh
|
||||
refresh_token = RefreshToken.find_by(token: params[:refresh_token])
|
||||
|
||||
if refresh_token&.active?
|
||||
user = refresh_token.user
|
||||
access_token = JsonWebToken.encode(user_id: user.id)
|
||||
render json: { access_token:, user: user.as_json(only: %i[id email role]) }, status: :ok
|
||||
else
|
||||
render json: { error: "Invalid or expired refresh token" }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
# post /logout -> NOW with real token blacklisting
|
||||
# adds the current token to blacklist so it can't be used again
|
||||
# authorize_request ensures @current_user and token are present
|
||||
def logout
|
||||
header = request.headers["Authorization"]
|
||||
token = header.split(" ").last if header
|
||||
|
||||
decoded = JsonWebToken.decode(token)
|
||||
BlacklistedToken.create!(
|
||||
jti: decoded[:jti],
|
||||
user_id: decoded[:user_id],
|
||||
exp: Time.at(decoded[:exp])
|
||||
)
|
||||
|
||||
# also revoke all refresh tokens for this user
|
||||
@current_user.refresh_tokens.update_all(revoked: true)
|
||||
|
||||
render json: { message: "Successfully logged out. Token blacklisted." }, status: :ok
|
||||
rescue StandardError => e
|
||||
render json: { error: "Logout failed: #{e.message}" }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# only allow what we actually need. nothing fancy, nothing extra. not need for require
|
||||
def user_params
|
||||
params.permit(:email, :password, :password_confirmation)
|
||||
end
|
||||
|
||||
# generate both access and refresh tokens
|
||||
def generate_tokens(user)
|
||||
access_token = JsonWebToken.encode(user_id: user.id)
|
||||
refresh_token = user.refresh_tokens.create!
|
||||
|
||||
{
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token.token,
|
||||
expires_in: 1.hour.to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
module Api
|
||||
module V1
|
||||
class ProfileController < ApplicationController
|
||||
include AuthorizeRequest
|
||||
|
||||
# get /profile
|
||||
# this is for getting current user who logged in
|
||||
def show
|
||||
render json: {
|
||||
id: @current_user.id,
|
||||
email: @current_user.email
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module Api
|
||||
module V1
|
||||
class ProtectedController < ApplicationController
|
||||
include AuthorizeRequest
|
||||
|
||||
def index
|
||||
render json: {
|
||||
message: "you are free to use this buddy",
|
||||
user: @current_user.as_json(only: %(id email))
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
class ApplicationController < ActionController::API
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# this concern checks the token and finds the user
|
||||
# if token is invalid or missing, we block the request
|
||||
# usage: just add `before_action :authorize_request` in any controller you wanna protect
|
||||
# I keep things simple, if you want to make it better it's you choice
|
||||
|
||||
module AuthorizeRequest
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authorize_request
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_request
|
||||
header = request.headers["Authorization"]
|
||||
token = header.split(" ").last if header
|
||||
|
||||
begin
|
||||
decoded = JsonWebToken.decode(token)
|
||||
|
||||
# check if token is blacklisted (logged out)
|
||||
if BlacklistedToken.blacklisted?(decoded[:jti])
|
||||
render json: { error: "Token has been revoked" }, status: :unauthorized
|
||||
return
|
||||
end
|
||||
|
||||
@current_user = User.find(decoded[:user_id])
|
||||
rescue ActiveRecord::RecordNotFound, StandardError => e
|
||||
render json: { error: "unauthorized: #{e.message}" }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# this concern provides role-based authorization
|
||||
# usage: add `before_action :require_admin` in controllers that need admin access
|
||||
# or use `authorize_role!(:admin, :moderator)` to check multiple roles
|
||||
|
||||
module AuthorizeRole
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
# check if current user has any of the specified roles
|
||||
def authorize_role!(*roles)
|
||||
unless @current_user && roles.map(&:to_s).include?(@current_user.role)
|
||||
render json: { error: "Forbidden: insufficient permissions" }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
# helper methods for specific roles
|
||||
def require_admin
|
||||
authorize_role!(:admin)
|
||||
end
|
||||
|
||||
def require_moderator
|
||||
authorize_role!(:admin, :moderator)
|
||||
end
|
||||
|
||||
def require_user
|
||||
authorize_role!(:user, :moderator, :admin)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user