- Overview
- Setup
- Testing
- API Documentation
- Authentication, User Roles, and Authorization
- Contributing
This app is a Rails backend API for a job tracking CRM tool.
Rails 7.1.5 Ruby 3.2.2
bundle install
rails db:create
rails db:migrate
rails db:seed
createuser -s postgres
psql -d template1 -c "ALTER USER postgres WITH PASSWORD 'trackercrm';"
This app will run on port 3001 locally.
This app uses RSpec for testing.
bundle exec rspec
New users require a unique email address and a matching password and password confirmation.
Request:
POST /api/v1/users
Body:
{
"name": "John Doe",
"email": "john.doe@example.com",
"password": "password",
"password_confirmation": "password"
}
Successful Response:
Status: 201 Created
Body: {
"data": {
"id": "4",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john.doe@example.com"
}
}
}
Error Responses:
Status: 400 Bad Request
Body: {
"message": "Email has already been taken",
"status": 400
}
Status: 400 Bad Request
Body: {
"message": "Password confirmation doesn't match Password",
"status": 400
}
Request:
GET /api/v1/users
Successful Response:
Status: 200 OK
Body: {
"data": [
{
"id": "1",
"type": "user",
"attributes": {
"name": "Danny DeVito",
"email": "danny_de_v"
}
},
...
{
"id": "4",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john.doe@example.com"
}
}
]
}
Request:
GET /api/v1/users/:id
Successful Response:
Status: 200 OK
Body: {
"data": {
"id": "3",
"type": "user",
"attributes": {
"name": "Lionel Messi",
"email": "futbol_geek"
}
}
}
Request:
PUT /api/v1/users/:id
Body:
{
"name": "John Doe",
"email": "john.doe@example.com",
"password": "password",
"password_confirmation": "password"
}
Successful Response:
Status: 200 OK
Body: {
"data": {
"id": "4",
"type": "user",
"attributes": {
"name": "Nathan Fillon",
"email": "firefly_captian"
}
}
}
Error Responses:
Status: 400 Bad Request
Body: {
"message": "Email has already been taken",
"status": 400
}
Status: 400 Bad Request
Body: {
"message": "Password confirmation doesn't match Password",
"status": 400
}
Request:
POST /api/v1/sessions
Body:
{
"email": "john.doe@example.com",
"password": "password"
}
Successful Response:
Status: 200 OK
Body: {
"data": {
"id": "4",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john.doe@example.com"
}
}
}
Error Response:
Status: 401 Unauthorized
Body: {
"message": "Invalid login credentials",
"status": 401
}
Request:
GET /api/v1/users/:user_id/job_applications
Headers:
{
"Authorization": "Bearer <your_token_here>"
}
Successful Response:
Status: 200
{
"data": [
{
"id": "1",
"type": "job_application",
"attributes": {
"position_title": "Jr. CTO",
"date_applied": "2024-10-31",
"status": 1,
"notes": "Fingers crossed!",
"job_description": "Looking for Turing grad/jr dev to be CTO",
"application_url": "www.example.com",
"contact_information": "boss@example.com",
"company_id": 1,
"company_name": "Google"
}
},
{
"id": "3",
"type": "job_application",
"attributes": {
"position_title": "Backend Developer",
"date_applied": "2024-08-20",
"status": 2,
"notes": "Had a technical interview, awaiting decision.",
"job_description": "Developing RESTful APIs and optimizing server performance.",
"application_url": "https://creativesolutions.com/careers/backend-developer",
"contact_information": "techlead@creativesolutions.com",
"company_id": 3,
"company_name": "Amazon"
}
}
]
}
Error Response if no token provided:
Status: 401 Unauthorized
Body: {
"message": "Invalid login credentials",
"status": 401
}
Request:
POST /api/v1/users/:user_id/job_applications
Headers:
{
"Authorization": "Bearer <your_token_here>"
}
Body
Body: {
{
position_title: "Jr. CTO",
date_applied: "2024-10-31",
status: 1,
notes: "Fingers crossed!",
job_description: "Looking for Turing grad/jr dev to be CTO",
application_url: "www.example.com",
contact_information: "boss@example.com",
company_id: id_1
}
}
Successful Response:
Status: 200
{:data=>
{:id=>"4",
:type=>"job_application",
:attributes=>
{:position_title=>"Jr. CTO",
:date_applied=>"2024-10-31",
:status=>1,
:notes=>"Fingers crossed!",
:job_description=>"Looking for Turing grad/jr dev to be CTO",
:application_url=>"www.example.com",
:contact_information=>"boss@example.com",
:company_id=>35}}
}
Unsuccessful Response:
{:message=>"Company must exist and Position title can't be blank", :status=>400}
Request:
GET /api/v1/users/:user_id/job_applications/:job_application_id
Successful Response:
{
"data": {
"id": "3",
"type": "job_application",
"attributes": {
"position_title": "Backend Developer",
"date_applied": "2024-08-20",
"status": 2,
"notes": "Had a technical interview, awaiting decision.",
"job_description": "Developing RESTful APIs and optimizing server performance.",
"application_url": "https://creativesolutions.com/careers/backend-developer",
"company_id": 3,
"company_name": "Creative Solutions Inc.",
"contacts": [
{
"id": 3,
"first_name": "Michael",
"last_name": "Johnson",
"email": "michael.johnson@example.com",
"phone_number": "123-555-9012",
"notes": "Hiring manager at Creative Solutions Inc."
}
]
}
}
}
Unsuccessful Response(job application does not exist OR belongs to another user):
{
"message": "Job application not found",
"status": 404
}
If the user is not authenticated:
{
"message": "Unauthorized request",
"status": 401
}
Unsuccessful Response(pre-existing application for user):
{:message=>"Application url You already have an application with this URL", :status=>400}
Request:
PATCH /api/v1/users/:user_id/job_applications
Headers:
{
"Authorization": "Bearer <your_token_here>"
}
Body
Body: {
{
position_title: "Jr. CTO",
date_applied: "2024-10-31",
status: 1,
notes: "Fingers crossed!",
job_description: "Looking for Turing grad/jr dev to be CTO",
application_url: "www.example.com",
contact_information: "boss@example.com",
company_id: id_1
}
}
Minimum of one attribute needs to be changed in the update for it to be successful
Successful Response:
Status: 200
{:data=>
{:id=>"4",
:type=>"job_application",
:attributes=>
{:position_title=>"Jr. CTO",
:date_applied=>"2024-10-31",
:status=>1,
:notes=>"Fingers crossed!",
:job_description=>"Looking for Turing grad/jr dev to be CTO",
:application_url=>"www.example.com",
:contact_information=>"boss@example.com",
:company_id=>35,
:updated_at=>"2025-01-07"
}
}
}
Unsuccessful Response:
{:message=>"No parameters provided", :status=>400}
No parameters were recognized by the application, check the request parameters to see that they match.
{:message=>"Job application not found", :status=>404}
Either the application doesn't exist or it doesn't belong to the current user. Verify that it exists and matches the user identity in the token.
Request:
POST "/api/v1/users/:userid/companies"
Headers:
{
"Authorization": "Bearer <your_token_here>"
}
raw json body:
{
"name": "New Company",
"website": "www.company.com",
"street_address": "123 Main St",
"city": "New York",
"state": "NY",
"zip_code": "10001",
"notes": "This is a new company."
}
Successful Response:
Status: 201 created
"data": {
"id": "1",
"type": "company",
"attributes": {
"name": "New Company",
"website": "www.company.com",
"street_address": "123 Main St",
"city": "New York",
"state": "NY",
"zip_code": "10001",
"notes": "This is a new company."
}
}
Error response - missing params
Request:
{
"name": "",
"website": "amazon.com",
"street_address": "410 Terry Ave N",
"city": "Seattle",
"state": "WA",
"zip_code": "98109",
"notes": "E-commerce"
}
Response:
{
"message": "Name can't be blank",
"status": 422
}
Request:
GET /api/v1/users/userid/companies
Authorization: Bearer Token - put in token for user
Successful Response:
Body:{
{
"data": [
{
"id": "1",
"type": "company",
"attributes": {
"name": "Google",
"website": "google.com",
"street_address": "1600 Amphitheatre Parkway",
"city": "Mountain View",
"state": "CA",
"zip_code": "94043",
"notes": "Search engine"
}
},
{
"id": "2",
"type": "company",
"attributes": {
"name": "New Company122",
"website": "www.company.com",
"street_address": "122 Main St",
"city": "New York11",
"state": "NY11",
"zip_code": "10001111",
"notes": "This is a new company111."
}
}
]
}
}
User with no companies:
{
"data": [],
"message": "No companies found"
}
No token or bad token response
{
"error": "Not authenticated"
}
Request:
GET /api/v1/users/:user_id/contacts
Authorization: Bearer Token - put in token for user
Successful Response:
{
"data": [
{
"id": "1",
"type": "contacts",
"attributes": {
"first_name": "John",
"last_name": "Smith",
"company": "Turing",
"email": "123@example.com",
"phone_number": "(123) 555-6789",
"notes": "Type notes here...",
"user_id": 4
}
},
{
"id": "2",
"type": "contacts",
"attributes": {
"first_name": "Jane",
"last_name": "Smith",
"company": "Turing",
"email": "123@example.com",
"phone_number": "(123) 555-6789",
"notes": "Type notes here...",
"user_id": 4
}
}
]
}
Successful response for users without saved contacts:
{
"data": [],
"message": "No contacts found"
}
New contacts require a unique first and last name. All other fields are optional.
Request:
POST /api/v1/users/:user_id/contacts
Authorization: Bearer Token - put in token for user
raw json body with all fields:
{
"contact": {
"first_name": "Jonny",
"last_name": "Smith",
"company_id": 1,
"email": "jonny@gmail.com",
"phone_number": "555-785-5555",
"notes": "Good contact for XYZ",
"user_id": 7
}
}
Successful Response:
Status: 201 created
{
"data": {
"id": "5",
"type": "contacts",
"attributes": {
"first_name": "Jonny",
"last_name": "Smith",
"company_id": 1,
"email": "jonny@gmail.com",
"phone_number": "555-785-5555",
"notes": "Good contact for XYZ",
"user_id": 7
}
}
}
New contacts with company name require a unique first and last name, and company ID in the URI. All other fields are optional.
Request:
POST api/v1/users/:user_id/companies/:company_id/contacts
Authorization: Bearer Token - put in token for user
raw json body with all fields:
{
"data": [
{
"id": "1",
"type": "contacts",
"attributes": {
"first_name": "Jane",
"last_name": "Doe",
"company_id": 2,
"email": "",
"phone_number": "",
"notes": "",
"user_id": 2,
"company": {
"id": 2,
"name": "Future Designs LLC",
"website": "https://futuredesigns.com",
"street_address": "456 Future Blvd",
"city": "Austin",
"state": "TX",
"zip_code": "73301",
"notes": "Submitted application for the UI Designer role."
}
}
}
]
},
Ensure you Create the Contact first for the user NOT company - 2 different Routes!
Request:
GET http://localhost:3001/api/v1/users/:user_id/contacts/:contact_id
Authorization: Bearer Token - put in token for user
Successful Response:
{
"data": {
"id": "1",
"type": "contacts",
"attributes": {
"first_name": "Josnny",
"last_name": "Smsith",
"company_id": 1,
"email": "jonny@gmail.com",
"phone_number": "555-785-5555",
"notes": "Good contact for XYZ",
"user_id": 1,
"company": {
"id": 1,
"name": "Tech Innovators",
"website": "https://techinnovators.com",
"street_address": "123 Innovation Way",
"city": "San Francisco",
"state": "CA",
"zip_code": "94107",
"notes": "Reached out on LinkedIn, awaiting response."
}
}
}
}
401 Error Response if no token provided:
Status: 401 Unauthorized
Body: {
"message": "Invalid login credentials",
"status": 401
}
422 Error Response Unprocessable Entity: Missing Required Fields If required fields like first_name or last_name are missing:
Request:
POST /api/v1/users/:user_id/contacts
Authorization: Bearer Token - put in token for user
raw json body:
{
"contact": {
"first_name": "Jonny",
"last_name": ""
}
}
Error response - 422 Unprocessable Entity
{
"error": "Last name can't be blank"
}
Error response - invalid email format
Request:
{
"contact": {
"first_name": "Johnny",
"last_name": "Smith",
"email": "invalid-email"
}
}
Response: 422 Unprocessable Entity
{
"error": "Email must be a valid email address"
}
Error response - invalid phone number format
Request:
{
"contact": {
"first_name": "Johnny",
"last_name": "Smith",
"email": "invalid-email"
}
}
Response: 422 Unprocessable Entity
{
"error": "Phone number must be in the format '555-555-5555'"
}
Get login credentials:
Refer to "Create a Session" above
Make sure to not only create/login a user, but to have that user also create a Company/Job Application/Contact for your Postman scripts. Refer to above endpoints to do so and make sure that user is the one creating the other resources
Request:
GET /api/v1/users/:user_id/dashboard
Authorization: Bearer Token - put in token for user
Successful Response:
{
"data": {
"id": "5",
"type": "dashboard",
"attributes": {
"id": 5,
"name": "Danny DeVito",
"email": "danny_de@email.com",
"dashboard": {
"weekly_summary": {
"job_applications": [
{
"id": 1,
"position_title": "Jr. CTO",
"date_applied": "2024-10-31",
"status": 1,
"notes": "Fingers crossed!",
"job_description": "Looking for Turing grad/jr dev to be CTO",
"application_url": "www.example.com",
"contact_information": "boss@example.com",
"created_at": "2024-12-14T17:20:41.979Z",
"updated_at": "2024-12-14T17:20:41.979Z",
"company_id": 1,
"user_id": 5
},
{
"id": 2,
"position_title": " CTO",
"date_applied": "2024-10-31",
"status": 2,
"notes": "Fingers crossed!",
"job_description": "Looking for Turing grad/jr dev to be CTO",
"application_url": "www.testexample.com",
"contact_information": "boss1@example.com",
"created_at": "2024-12-14T17:37:28.465Z",
"updated_at": "2024-12-14T17:37:28.465Z",
"company_id": 2,
"user_id": 5
}
],
"contacts": [
{
"id": 1,
"first_name": "Jonny",
"last_name": "Smith",
"email": "jonny@gmail.com",
"phone_number": "555-785-5555",
"notes": "Good contact for XYZ",
"created_at": "2024-12-14T17:55:21.875Z",
"updated_at": "2024-12-14T17:55:21.875Z",
"user_id": 5,
"company_id": 1
},
{
"id": 2,
"first_name": "Josnny",
"last_name": "Smsith",
"email": "jonny@gmail.com",
"phone_number": "555-785-5555",
"notes": "Good contact for XYZ",
"created_at": "2024-12-15T01:57:14.557Z",
"updated_at": "2024-12-15T01:57:14.557Z",
"user_id": 5,
"company_id": 1
}
],
"companies": [
{
"id": 1,
"user_id": 5,
"name": "New Company",
"website": "www.company.com",
"street_address": "123 Main St",
"city": "New York",
"state": "NY",
"zip_code": "10001",
"notes": "This is a new company.",
"created_at": "2024-12-14T17:20:10.909Z",
"updated_at": "2024-12-14T17:20:10.909Z"
},
{
"id": 2,
"user_id": 5,
"name": "New Company1",
"website": "www.company1.com",
"street_address": "1231 Main St",
"city": "New York",
"state": "NY",
"zip_code": "10001",
"notes": "This is a new company1.",
"created_at": "2024-12-14T17:37:24.153Z",
"updated_at": "2024-12-14T17:37:24.153Z"
}
]
}
}
}
}
}
-
Authentication is handled by the JWT(Json Web Token) Gem
-
User Roles is handled by the Rolify Gem
-
Authorization is enforced by the Pundit Gem
-
- Pull the latest changes to your branch
git pull origin main
resolve merge conflicts - Bundle Install and Migrate
bundle install
thenrails db:migrate
- Run
bundle exec rspec spec/
and note what requests tests are now failing - Refactor controllers, their associated _spec.rb files. Make policies for corresponding controllers (app/policies/_policy.rb) and test policies
- Use examples in UserPolicy, ApplicationPolicy, UsersController, ApplicationController and all corresponding spec files.
- Pull the latest changes to your branch
-
-
This branch introduces 2 new tables to our db schema - roles and users_roles that were generated by Rolify:
- roles table has name, resouce_type and resource_id. Name string is the name of the role and can either be ["admin"] OR ["user"]. Resource type string is an optional polymorphic association that specifies the type of resource the role applies to (company, job, contact).
- Whenever a user is created, they are automatically assigned a role[:name] of
["user"]
- Whenever a user is created, they are automatically assigned a role[:name] of
- users_roles is a join table for the many-to-many relationship between users and roles.
- roles table has name, resouce_type and resource_id. Name string is the name of the role and can either be ["admin"] OR ["user"]. Resource type string is an optional polymorphic association that specifies the type of resource the role applies to (company, job, contact).
-
-
Understanding JWT(Json Web Token) Gem
We now use JSON Web Tokens for user authentication. Here's what to know:
-
- Token-Based Authentication
- Upon login, a JWT is issued to the registered user.
- The token must be included in the Headers for every authenticated request in Postman.
- Authorization: Bearer <jwt_token>
- The token encodes a payload that contains:
- User's id
- User's role (either "admin" or "user")
- Tokens expiration time (24 hours after issue)
- Changes made in Codebase
- ApplicationController now has 2 new methods:
authenticate_user
ensures tokens validity and corresponds to a logged-in user.decoded_token(token)
to extract info from token- All controllers will inherit this logic.
- SessionsController
- Handles login and session creation
#create
action:- Verifies user's credentials
generate_token(payload)
generates an encoded JWT using above-mentioned payload- Sends the token back in the response
- ApplicationController now has 2 new methods:
- Token-Based Authentication
-
-
Understanding the Rolify Gem
Rolify simplifies the management of user roles by introducing a flexible, role-based system. This gem created the roles and users_roles tables.
-
- When a new user is created, they are automatically assigned the user role.
- Methods in user.rb created as querying and setting shortcuts:
assign_default_role
is called to set a user's role to:user
upon a new instance of User being created. All new users will get role :user.is_admin?
and/oris_user?
queries any user's roleset_role(role_name)
assigns a specific role to a user(e.guser1.set_role(:admin)
will set a user to the :admin role)
- For future scalability, polymorphic roles can be scoped to specific users. For example, we can add in a :moderator role in the future that would be an :admin for only the company table. Enabled by resource_type and resource_id fields in roles table.
-
- In your models where roles are needed (e.g., User, Company), use:
resourcify
at the top of the class(near validations/relations) to make the model "role-aware" and capable of interacting with Rolify - Use above-mentioned methods in user.rb to query and set user roles.
- In your models where roles are needed (e.g., User, Company), use:
-
- UsersController now leverages roles to restrict access. For example, only an :admin can view all users (subject to change, ofc)
- ApplicationPolicy Includes logic to ensure actions are authorized based on roles.
- Affects RSpec testing to test role-based permitted controller actions (see index_spec.rb ln:7 for an example)
-
-
Understanding the Pundit Gem
Pundit enforces authorization through policy classes and ensures that only authorized users can perform specific actions. To fully understand Pundit, you must understand what a policy is:
-
A policy is a PORO that defines the rules governing what actions a user is authorized to perform on a resource (e.g., User, Job, Company). Each policy corresponds to a model's controller and centralizes the access control logic for that resource.
-
I have set up UserPolicy for it's corresponding UsersController as a template for all to use. UserPolicy restricts which CRUD actions a user can use depending on that user's role. All of Users associated request spec files have also been refactored to pass and are a template for you.
-
- Policy Classes as mentioned above define an action a user can take
- stored in app/policies and inherit from ApplicationPolicy
- Policies are then passed to ApplicationController via
include Pundit::Authorization
ln:2. Remember that all controllers inherit from ApplicationController unless explicitly told not to.
- Authorization Logic
- Authorization is defined in methods corresponding to controller actions (e.g., create?, update?).
- ApplicationPolicy provides a default template where ALL actions are unauthorized by default.
- Use UserPolicy (and it's associated spec file for testing) as a template for making your controller's policies.
- Scoping
- Policies include a nested Scope class (see UserPolicy) to define which records a user can access when retrieving a collection.
- Policy Classes as mentioned above define an action a user can take
-
- Use the
authorize
method IN your controller action code blocks to enforce policy checks (see UsersController ln: 5, 15, 21, 28 for examples. Yes, it is THAT easy :)) - If a user is unauthorized, Pundit raises a
Pundit::NotAuthorizedError
in your console RSpec testing suite. - For testing, refer to user_policy_spec.rb as a template. Note that ln:3
subject { described_class }
subject == UserPolicy - Policies integrate with Rolify to check user roles
- Use the
- JWT secures the app by ensuring only authenticated users can access resources. - Rolify manages who can act (roles), while Pundit manages what they can do (authorization). - Refactoring controllers and policies is critical to aligning with this new system. - Ensure all tests are updated to reflect these changes, including controller specs and new policy tests. - It is up to you, as you develop to keep in mind what actions users are authorized to take. :Admin role users should be authorized to do anything.
-
Banks, Charles
Bloom, Stefan
Chirchirillo, Joe
Cirbo, Candice
Croy, Lito
De La Rosa, Melchor
Chirchirillo, Joe
Delaney, Kyle
Galvin, Shane
Hill, John
Hotaling, Marshall
Macur, Jim
Messersmith, Renee
O'Leary, Ryan
Pintozzi, Erin - (Project Manager)