Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions aicruit-api/.env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# Frontend Domain
NEXT_DOMAIN=http://localhost:5173
# Python Domain
PYTHON_DOMAIN=http://localhost:8000
# Frontend Domain (Use lvh.me for local subdomain-based multitenancy testing)
NEXT_DOMAIN=http://joshsoftware.lvh.me:5173
# Python Domain (Internal container endpoint or local url)
PYTHON_DOMAIN=http://python-service:8000

# Database Configuration
DATABASE_USERNAME=xxxxxx
DATABASE_PASSWORD=xxxxxx
DATABASE_NAME=xxxxxx
DATABASE_HOST=xxxxx
DATABASE_PORT=xxxxx
# Database Configuration (Docker network default)
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=aicruit_development
DATABASE_HOST=db
DATABASE_PORT=5432

AWS_REGION=xxxxxx
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
AWS_BUCKET_NAME: xxxxxxxxxxxxxxxxxx
# AWS Storage Configurations
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=dummy_access_key
AWS_SECRET_ACCESS_KEY=dummy_secret_key
AWS_BUCKET_NAME=dummy_bucket

PYTHON_SERVICE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Python microservice auth and endpoints
PYTHON_SERVICE_API_KEY=dummy_python_api_key
PYTHON_JD_PARSE=/parse-job-description
PYTHON_RESUME_PARSE=/parse-resume

REDIS_URL=redis://localhost:6379/0
2 changes: 1 addition & 1 deletion aicruit-api/.ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby-3.2.0
3.3.2
26 changes: 26 additions & 0 deletions aicruit-api/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM ruby:3.3.2-slim

# Install system dependencies
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
build-essential \
git \
libpq-dev \
pkg-config \
curl \
postgresql-client && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives

WORKDIR /rails

# Install gems
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install

# Copy entrypoint script
COPY bin/docker-entrypoint /rails/bin/
RUN chmod +x /rails/bin/docker-entrypoint

ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 3000
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "3000"]
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# frozen_string_literal: true

class Api::V1::JobDescriptionsController < ApplicationController
skip_before_action :authenticate!, only: %i[published index show]
skip_before_action :authenticate!, only: %i[published index show download]
before_action :authenticate_optional!, only: %i[published index show download]

def create
authorize! :create, JobDescription
Expand Down Expand Up @@ -86,4 +87,22 @@ def upload
render json: result.to_h, status: :unprocessable_entity
end
end

def download
job_description = JobDescription.find(params[:id])
unless job_description.published?
authorize! :read, job_description
end

if job_description.file_url.present?
result = AwsService::S3Download.new(job_description.file_url).call
if result[:success]
send_data result[:data][:content], filename: result[:data][:filename], type: result[:data][:content_type], disposition: 'inline'
else
render json: result.to_h, status: :unprocessable_entity
end
else
render json: { success: false, message: "No file associated with this job description" }, status: :not_found
end
end
end
13 changes: 13 additions & 0 deletions aicruit-api/app/controllers/api/v1/resumes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,17 @@ def upload
end
end

def download
resume = Resume.find(params[:id])
if resume.link_to_file.present?
result = AwsService::S3Download.new(resume.link_to_file).call
if result[:success]
send_data result[:data][:content], filename: result[:data][:filename], type: result[:data][:content_type], disposition: 'inline'
else
render json: result.to_h, status: :unprocessable_entity
end
else
render json: { success: false, message: "No file associated with this resume" }, status: :not_found
end
end
end
20 changes: 19 additions & 1 deletion aicruit-api/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ def authenticate!
end
end

def authenticate_optional!
header = request.headers['Authorization']
header = header.split.last if header
return unless header.present?

if header == ENV['PYTHON_SERVICE_API_KEY']
@current_user = nil
@current_service = "python_service"
return
end

begin
jwt_payload(header)
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
# Do not render unauthorized or raise; just let guest access continue
end
end

def set_current_tenant
subdomain = request.subdomains.first
return unless subdomain.present?
Expand Down Expand Up @@ -69,7 +87,7 @@ def current_ability
private

def jwt_decode(token)
JWT.decode(token, Rails.application.credentials.secret_key_base)[0]
JWT.decode(token, Rails.application.secret_key_base)[0]
end

def handle_record_invalid(exception)
Expand Down
7 changes: 7 additions & 0 deletions aicruit-api/app/serializers/job_description_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ class JobDescriptionSerializer < ActiveModel::Serializer
attribute :total_applicants do
object.resumes.count
end

def file_url
if object.file_url.present?
api_url = ENV.fetch('RAILS_API_EXTERNAL_URL', 'http://joshsoftware.lvh.me:3000')
"#{api_url}/api/v1/job_descriptions/#{object.id}/download"
end
end
end
7 changes: 7 additions & 0 deletions aicruit-api/app/serializers/resume_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ class ResumeSerializer < ActiveModel::Serializer
attribute :referred_by, if: -> { object.referred_by.present? } do
"#{object.referred_by.first_name} #{object.referred_by.last_name}"
end

def link_to_file
if object.link_to_file.present?
api_url = ENV.fetch('RAILS_API_EXTERNAL_URL', 'http://joshsoftware.lvh.me:3000')
"#{api_url}/api/v1/resumes/#{object.id}/download"
end
end
end
60 changes: 60 additions & 0 deletions aicruit-api/app/services/aws_service/s3_download.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'aws-sdk-s3'

module AwsService
class S3Download < Base
attr_reader :s3_url, :bucket_name, :file_name

def initialize(s3_url)
super()
@s3_url = s3_url
@bucket_name, @file_name = parse_s3_url(s3_url)
end

def call
begin
options = {
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID', nil),
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil),
region: ENV.fetch('AWS_REGION', 'us-east-1')
}
if ENV['AWS_ENDPOINT'].present?
options[:endpoint] = ENV['AWS_ENDPOINT']
options[:force_path_style] = true
end

s3_client = Aws::S3::Client.new(options)

# Download the file content from S3
response = s3_client.get_object(
bucket: bucket_name,
key: file_name
)

content_type = response.content_type || 'application/octet-stream'
filename = File.basename(file_name)

success_response(
'File downloaded successfully',
{ content: response.body.read, filename: filename, content_type: content_type }
)
rescue Aws::S3::Errors::ServiceError => e
failure_response("S3 service error: #{e.message}")
rescue StandardError => e
failure_response("Error downloading file: #{e.message}")
end
end

private

def parse_s3_url(url)
unless url.to_s.start_with?('s3://')
raise ArgumentError, "Invalid S3 URL format: #{url}"
end

parts = url.to_s.gsub('s3://', '').split('/', 2)
[parts[0], parts[1]]
end
end
end
10 changes: 8 additions & 2 deletions aicruit-api/app/services/aws_service/s3_upload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ def initialize(file, service_type = nil, file_name = nil)

def call
begin
s3_client = Aws::S3::Client.new(
options = {
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID', nil),
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil),
region: ENV.fetch('AWS_REGION', 'us-east-1')
)
}
if ENV['AWS_ENDPOINT'].present?
options[:endpoint] = ENV['AWS_ENDPOINT']
options[:force_path_style] = true
end

s3_client = Aws::S3::Client.new(options)
# Upload the file to S3
response = s3_client.put_object(
bucket: bucket_name,
Expand Down
2 changes: 1 addition & 1 deletion aicruit-api/app/services/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def decode_hash(encoded_hash)
end

def jwt_encode(payload, exp = 1.days.from_now)
secret_key = Rails.application.credentials.secret_key_base
secret_key = Rails.application.secret_key_base
payload[:exp] = exp.to_i
JWT.encode(payload, secret_key)
end
Expand Down
2 changes: 1 addition & 1 deletion aicruit-api/config/initializers/cors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins %r{\Ahttps?://.*\.aicruit\.com(:5173)?\z}
origins %r{\Ahttps?://.*\.aicruit\.com(:5173)?\z}, %r{\Ahttps?://.*\.lvh\.me(:5173)?\z}

resource '*',
headers: :any,
Expand Down
2 changes: 2 additions & 0 deletions aicruit-api/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
delete :destroy
get :show
get :applicant_resumes
get :download
end
end

Expand All @@ -47,6 +48,7 @@
member do
get :show
put :update
get :download
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions aicruit-api/db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions app/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Base URL for the Rails API
NEXT_PUBLIC_RAILS_API_URL=XXXXXXXXXXXXX
# Base URL for the Rails API (Use joshsoftware.lvh.me to preserve subdomain-based tenancy)
NEXT_PUBLIC_RAILS_API_URL=http://joshsoftware.lvh.me:3000

# Base URL for the Python API
NEXT_PUBLIC_PYTHON_URL=XXXXXXXXXXXXXX
# Base URL for the Python API (Accessed from host browser)
NEXT_PUBLIC_PYTHON_URL=http://localhost:8000

# Domain for the API (used for CORS or cookies, etc.)
NEXT_PUBLIC_API_DOMAIN=XXXXXXXXXXXXXXX
# Domain for the API Accept header routing.
# IMPORTANT: This MUST be set to vnd.aicruit.com to match Rails routes versioning!
NEXT_PUBLIC_API_DOMAIN=vnd.aicruit.com
17 changes: 17 additions & 0 deletions app/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM node:18-slim

WORKDIR /app

# Install system dependencies if any are needed
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*

COPY package.json package-lock.json ./
RUN npm install

COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev"]
1 change: 1 addition & 0 deletions app/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum UserRoles {
}

export const UserRoutes: Record<string, string> = {
[UserRoles.SUPER_ADMIN]: "/job-description",
[UserRoles.HR_ADMIN]: "/job-description",
[UserRoles.HR]: "/job-description",
[UserRoles.CANDIDATE]: "/published-job-descriptions",
Expand Down
2 changes: 1 addition & 1 deletion app/src/services/AuthUser/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const useUser = () => {
toast.success(ValidationMessage.SIGNIN_SUCCESS);
// User role based navigation
const userRole = res?.data?.user.role_name;
router.push(UserRoutes[userRole]);
router.push(UserRoutes[userRole] || UserRoutes.DEFAULT || "/");
},
onError: (error) => handleErrorResponse(error),

Expand Down
Loading