Skip to content

rnters backend-challenge Tiago Santos #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
79 changes: 48 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,62 @@
# Rnters - Backend Code Challenge

If you are reading this, you probably have interviewed or chatted with someone on the team at Rnters. This is our standard "toy" project we normally like to work on together to see how you think about problems, model them, and make decisions. If you stumbled upon this project randomly and want to give it a shot, please feel free to fork the project and hack away. We would love to see what you come up with.

An initial version of this project should be doable in well under 2 hours, but has many facets that could be improved beyond that inital cut. And although we are providing you with most of the boilerplate so you can focus on the requested functionalities, it has many facets that could be improved beyond that initial cut.
## Creation of users

## Challenge
We are looking for an API-based application that exposes an API entry to create users with a first and last name. The twist - after 30seconds that user will grow a mustache, and the 🥸 emoji will be added to his last name. E.g. if I create the user "Quim Barreiros", after 30 seconds I will see that his name is "Quim Barreiros 🥸". Thats it!

#### Users
User table should have the following attributes:
- `first_name`
- `last_name`
- `admin`
I've created a migration in order to create the users table, with the attributes "first_name:string", "last_name:string", "admin:boolean" and the timestamps, which could be useful in a real-world application ("created_at:datetime" and "updated_at:datetime").

#### API
Should have the following endpoint:
- `POST /api/v1/users` with the params `first_name` and `last_name`
- Should return `202 (Created)` if successful and the appropriate error otherwise

The defined routes (resources: users) are inside the namespaces "api" and "v1"... The available routes are constructed to allow us creating, showing the all the records and showing specific records.

## How to run
You'll need to have [Docker installed](https://docs.docker.com/get-docker/). Fork and clone this repo into your computer.
It was necessary to create a model referring to the users, which was created under the name "User". The model has validations, with the attributes "first_name" and "last_name" not being allowed to be blank.

After that you can simply run the web version so you can use curl: `docker-compose up`
It also includes attr_accessible, defining which attributes are permitted to be defined when posting the data.

Or you can run the console version: `docker-compose run --rm console`
In the controller, the action "create" defines a new user, based upon the "user_params", which is a function that points to the model's attr_accessible and allows that specific attributes to be defined.

Íf the user is successfully saved, it will be rendered json, with the "created" status coming as a response.

## How to test
We provide you with a simple test by running:
```
$ docker-compose run --rm console
> rspec spec/request/user_spec.rb
```
Otherwise, the response will be a bad request (for example, a user like {first_name: '', last_name: ''} is a bad request).

You can also run the environment and curl against it
```
$ docker-compose up web
# in another terminal
$ curl -v -X POST 'http://127.0.0.1:8000/api/v1/users' -d '{"first_name": "Quim", "last_name": "Barreiros" }' -H "Content-Type: application/json"
```
## Getting all the users

You can also check sidekiq admin through:
`http://127.0.0.1:8000/sidekiq`
In order to test the creation and update after 30 seconds of all the users, I've created another endpoint, (get '/api/v1/users').


Inside the controller's action, all the users are gathered (by calling the function "get_users") and used to be rendered as JSON. The status is 200, if any records are found.


## Getting a specific user

This API also allows us to get a specific user, via the following endpoint (get '/api/v1/users/:id').

Inside the controller, the function "get_user" is called before any action occurs. It's not called individually inside "show" because, in a more extensive API (that would contain actions like updating and destroying records) it would also be used, so in this way we don't have the need to call it several times.




## Adding a moustache after 30 seconds
As it is a feature wanted to each record only one time, I felt I needed to do something after the creation of each instance. So, I've included in the model a function under the name "add_moustache" that is fired when a record is created (after_create). I've built an asynchrounous job, that waits 30 seconds after the each creation event to be performed. The job goes under the name "UpdateUserJob" and simply updates the user's last name, adding a moustache after it.


## Securing the API

I've added another class ("ApiKey") with the attribute "access_token:string" and the timestamps being the attributes added.

The objective was to build an authentication token, so I could secure the API in order to prevent access to everyone except those who have a valid token.

Inside the corresponding model, there is a private method named "generate_access_token", in which a random hexadecimal string is generated. It has a condition to stop the generation, that has the objective of avoiding repeated tokens.

Then, in the controller (commented) there is a function ("restrict_access") called before any action is taken. Inside the function, it is compared the header 'Authorization-token' with the registers in the database and then if the token doesn't exist in the DB, the sent status is 401 (unauthorized).


Another (simplistic) way of securing the API is also commented in the controller with the instruction "http_basic_authenticate_with name: 'rnters', password: 'sign€d'", which forces us to login as that user and to know that password in order to access the API functionalities.


## Notes

The templates for the user updating and destroying are also included in the controller, so if we wanted to add those actions we only would need to uncomment them.


The docker-compose file had a problem in the "worker" container, caused by the lack of a defined "working_dir".
74 changes: 74 additions & 0 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
class Api::V1::UsersController < ActionController::Base
#before_action :restrict_access
before_action :get_user, only: [:show]

#http_basic_authenticate_with name: 'rnters', password: 'sign€d'
# GET /users
def index
get_users
#poderia ser utilizado serializer
render json: @users, only: [:first_name, :last_name, :admin]

end

# GET /users/1
def show
render json: @user, only: [:first_name, :last_name, :admin]
end

# POST /users
def create
@user = User.new(user_params)


if @user.save
render json: @user, only: [:first_name, :last_name, :admin], status: :created
else
# status 422
render json: @user.errors, status: :bad_request
end
end

# PATCH/PUT /users/1
#def update
# if @user.update(user_params)
# render json: @user, only: [:first_name, :last_name]
#else
# render json: @user.errors, status: :bad_request
# end
#end

# DELETE /users/1
#def destroy
# @user.destroy

#head :no_content
#end

private
def get_user
@user = User.find_by(id: params[:id])

if @user.nil?
render json: {message: I18n.t('user_not_found')}, status: :not_found
end
end


# def restrict_access
# token = request.headers['Autorization-token']
# if !ApiKey.exists?(access_token: token)
# render json: {message: I18n.t('invalid_token')}, status: :unauthorized
# end
# end

# Only allow a list of trusted parameters through.
def user_params
params.permit(*User.attr_accessible)
end

def get_users

@users = User.all
end
end
7 changes: 7 additions & 0 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end
8 changes: 8 additions & 0 deletions app/jobs/update_user_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Job assíncrono para criar o moustache
class UpdateUserJob < ApplicationJob
queue_as :default

def perform(user)
user.update(last_name: "#{user.last_name} 🥸")
end
end
13 changes: 13 additions & 0 deletions app/models/api_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class ApiKey < ApplicationRecord
before_create :generate_access_token

private

def generate_access_token
begin
#gerar o token
self.access_token = SecureRandom.hex
#se for repetido, não criar um novo
end while self.class.exists?(access_token: access_token)
end
end
18 changes: 18 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class User < ApplicationRecord
# não permitir first_name = '' e last_name = ''
validates :first_name, presence: true, allow_blank: false
validates :last_name, presence: true, allow_blank: false
after_create :add_moustache

def self.attr_accessible
[
:first_name, :last_name, :admin
]
end

private

def add_moustache
UpdateUserJob.set(wait: 30.seconds).perform_later(self)
end
end
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@

en:
hi: "Hi"
user_not_found: "User not found"
no_access_token: "No access token"
no_records_found: "No records found"
invalid_token: "Invalid token"
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

mount Sidekiq::Web => '/sidekiq'

namespace :api, defaults: { format: 'json' }, constraints: { format: 'json' } do
namespace :api, defaults: { format: :json }, constraints: { format: :json } do
namespace :v1 do
# returns API status
# only used for tests atm
get 'ping', to: 'ping#ping'
resources :users, :only => [:index, :show, :create]
end
end
end
12 changes: 12 additions & 0 deletions db/migrate/20211115113315_create_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateUsers < ActiveRecord::Migration[6.1]
def change
if !table_exists? 'users'
create_table :users do |t|
t.string :first_name
t.string :last_name
t.boolean :admin, :default => false
t.timestamps
end
end
end
end
11 changes: 11 additions & 0 deletions db/migrate/20211118161645_create_api_keys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateApiKeys < ActiveRecord::Migration[6.1]
def change
if !table_exists? 'api_keys'
create_table :api_keys do |t|
t.string :access_token, :unique => true

t.timestamps
end
end
end
end
3 changes: 3 additions & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
ApiKey.create!

#User.create!(first_name: 'Tiago', last_name: 'Santos', admin: true)
5 changes: 5 additions & 0 deletions spec/factories/api_keys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FactoryBot.define do
factory :api_key do
access_token { "MyString" }
end
end
36 changes: 33 additions & 3 deletions spec/request/user_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
RSpec.describe 'User', type: :request do
describe "PUT /api/v1/questions/:id" do
describe "post /api/v1/users" do

it 'returns success' do
post "/api/v1/users", params: { first_name: 'Quim', last_name: 'Barreiros', admin: true }

#created
expect(response.status).to eq(201)
# ....

Expand All @@ -12,8 +12,38 @@
it 'returns error' do
post "/api/v1/users", params: { first_name: '', last_name: '', admin: true }

expect(response.status).to eq(422)
expect(response.status).to eq(400)
# ....
end
end

describe "get /api/v1/users/1" do

it 'returns success' do
get "/api/v1/users/1"

expect(response.status).to eq(200)
# ....

end

it 'returns error' do
get "/api/v1/users/1"

expect(response.status).to eq(404)
# ....
end
end


describe "get /api/v1/users" do

it 'returns success' do
get "/api/v1/users"

expect(response.status).to eq(200)
# ....

end
end
end
22 changes: 22 additions & 0 deletions spec/routing/users_routing_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require "rails_helper"

RSpec.describe Api::V1::UsersController, type: :routing do
describe "routing" do
it "routes to #index" do
expect(get: "api/v1/users").to route_to("api/v1/users#index", :format => :json)
end

it "routes to #show" do
expect(get: "api/v1/users/1").to route_to("api/v1/users#show", id: "1", :format => :json)
end


it "routes to #create" do
expect(post: "api/v1/users").to route_to("api/v1/users#create", :format => :json)
end

#it "routes to #destroy" do
# expect(delete: "/users/1").to route_to("users#destroy", id: "1")
#end
end
end