Skip to content

Latest commit

 

History

History
811 lines (698 loc) · 19.8 KB

README.md

File metadata and controls

811 lines (698 loc) · 19.8 KB

Tracker CRM by Turing School of Software and Design

Overview

This app is a Rails backend API for a job tracking CRM tool.

Setup

Rails 7.1.5 Ruby 3.2.2

bundle install

rails db:create
rails db:migrate
rails db:seed

This app will run on port 3001 locally.

Testing

This app uses RSpec for testing.

bundle exec rspec

API Documentation

Users

Create a User

New users require a unique email address and a matching password and password confirmation.

Request:

POST /api/v1/users

Body:
{
  "name": "John Doe",
  "email": "[email protected]",
  "password": "password",
  "password_confirmation": "password"
}

Successful Response:

Status: 201 Created
Body: {
  "data": {
    "id": "4",
    "type": "user",
    "attributes": {
      "name": "John Doe",
      "email": "[email protected]"
    }
  }
}

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
}

Get All Users

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": "[email protected]"
      }
    }
  ]
}

Get a User

Request:

GET /api/v1/users/:id

Successful Response:

Status: 200 OK
Body: {
  "data": {
    "id": "3",
    "type": "user",
    "attributes": {
      "name": "Lionel Messi",
      "email": "futbol_geek"
    }
  }
}

Update a user

Request:

PUT /api/v1/users/:id

Body:
{
  "name": "John Doe",
  "email": "[email protected]",
  "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
}

Create a Session (Login)

Request:

POST /api/v1/sessions

Body:
{
  "email": "[email protected]",
  "password": "password"
}

Successful Response:

Status: 200 OK

Body: {
    "data": {
        "id": "4",
        "type": "user",
        "attributes": {
            "name": "John Doe",
            "email": "[email protected]"
        }
    }
}

Error Response:

Status: 401 Unauthorized

Body: {
    "message": "Invalid login credentials",
    "status": 401
}

Job Applications

Fetch all Job Applications for a user

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": "[email protected]",
                "company_id": 1
            }
        },
        {
            "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": "[email protected]",
                "company_id": 3
            }
        }
    ]
}

Error Response if no token provided:

Status: 401 Unauthorized

Body: {
    "message": "Invalid login credentials",
    "status": 401
}

Create a Job Application

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: "[email protected]",
        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=>"[email protected]",
      :company_id=>35}}
}

Unsuccessful Response:

{:message=>"Company must exist and Position title can't be blank", :status=>400}

Get a Job Application

Request:

GET /api/v1/users/:user_id/job_applications/:job_application_id

Successful Response:

Status: 200 OK
{: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=>"[email protected]",
      :company_id=>35}}
}

Unsuccessful Response(job application does not exist OR belongs to another user):

{:message=>"Job application not found", :status=>404}

Unsuccessful Response(missing job application ID param):

{:message=>"Job application ID is missing", :status=>400}

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}

Companies

Get login credentials

Request:

POST /api/v1/sessions

Body:
{
  "email": "[email protected]",
  "password": "password"
}

Successful Response:

Status: 200 OK

This response will have a token like this:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MzM0MzUzMDJ9.O6FtfoVjcobUiBHfKmZNovtt57061ktlPx-UgIZFGaQ

Body: {
    "data": {
        "id": "4",
        "type": "user",
        "attributes": {
            "name": "John Doe",
            "email": "[email protected]"
        }
    }
}

Error Response if no token provided:

Status: 401 Unauthorized

Body: {
    "message": "Invalid login credentials",
    "status": 401
}

Create a company

Request:

post "/api/v1/users/userid/companies" 

Add the bearer token to the auth tab in postman and will be able to create a company now for that specific user. Make sure to have the token for that user.

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
}

Get all companies

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"
}

Contacts

Get login credentials:
Refer to Companies "Get login credentials" above

Get all contacts for a user

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": "[email protected]",
        "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": "[email protected]",
      "phone_number": "(123) 555-6789",
      "notes": "Type notes here...",
      "user_id": 4
      }
    }
  ]
}

Successful response for users without saved contacts:

{
  "data": [],
  "message": "No contacts found"
}

Create a contact with required and optional fields.

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": "[email protected]",
    "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": "[email protected]",
            "phone_number": "555-785-5555",
            "notes": "Good contact for XYZ",
            "user_id": 7
        }
    }
}

Contact Errors

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'"
}

Authentication, User Roles, and Authorization

  • 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

  • Installation

    1. Pull the latest changes to your branch git pull origin main resolve merge conflicts
    2. Bundle Install and Migrate bundle install then rails db:migrate
    3. Run bundle exec rspec spec/ and note what requests tests are now failing
    4. Refactor controllers, their associated _spec.rb files. Make policies for corresponding controllers (app/policies/_policy.rb) and test policies
    5. Use examples in UserPolicy, ApplicationPolicy, UsersController, ApplicationController and all corresponding spec files.
  • Important Schema Note

    • 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"]
      • users_roles is a join table for the many-to-many relationship between users and roles.
  • We now use JSON Web Tokens for user authentication. Here's what to know:

    • Key Concepts

      • 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
  • 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.

    • Key Concepts

      • 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/or is_user? queries any user's role
        • set_role(role_name) assigns a specific role to a user(e.g user1.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.
    • Usage in Codebase

      • 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.
    • How this affected the Codebase

      • 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.

    • Key Concepts

      • 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.
    • Usage in Codebase

      • 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

    In conclusion

      - 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.