Skip to content

Controllers Overview

Keeping Controllers Focused and Models Robust

This application follows a structured approach to maintain a clean and modular codebase. Controllers are designed to remain focused, handling only request processing, data retrieval, and response rendering. Business logic, data manipulations, and validations are encapsulated within models to ensure clarity, maintainability, and reusability. To prevent models from becoming too complex, service objects, concerns, and decorators are utilized where necessary.

Namespacing

The controllers are organized under the api/v1 namespace and are further divided into sub-namespaces such as library, license, and school, which correspond to different modules within the application.

School - api/v1/school

The api/v1/school namespace contains controllers that manage school-related functionalities. These controllers handle actions related to students, classrooms, assessments, and other school-specific operations.

Library - api/v1/library

The api/v1/library namespace contains controllers that manage library-related functionalities. These controllers handle actions related to educational content, domains, goals, curriculums and other library-specific operations.

Users - api/v1/users

The api/v1/users namespace contains controllers that manage user-related functionalities. These controllers handle actions related to user authentication, profiles, and access management.

Authentication Mechanism

The controllers use various authentication mechanisms to ensure that only authorized users can access the endpoints. The before_action :authenticate_user! callback is commonly used to enforce user authentication. This callback ensures that the user is logged in before they can access the controller actions. Additionally, some controllers use custom authentication methods, such as authenticate_user_or_shared_entity! and authenticate_client_basic!, to handle specific authentication scenarios.

Authentication Methods

authenticate_client_basic!

This method uses HTTP Basic Authentication to authenticate clients. It verifies the client credentials (UID and secret) against the Doorkeeper::Application records. If the credentials are valid, the client is authenticated; otherwise, an unauthorized response is returned.

authenticate_user!

This method uses the Doorkeeper gem to authenticate users. It verifies the access token and sets the current user based on the token's resource owner. If the token is invalid or missing, an unauthorized response is returned.

authenticate_user_or_shared_entity!

This method checks for the presence of an X-Share-Token header. If the token is present, it attempts to authenticate the user based on the shared entity token. If the token is not present, it falls back to the authenticate_user! method.

Headers Used

  • X-School-Id: This header is used to specify the school context for the request. It helps in multi-tenant scenarios where the application needs to differentiate between different schools.
  • X-Profile-Id: This header is used to specify the profile context for the request. It helps in identifying the specific profile of the user making the request.
  • X-Share-Token: This header is used to authenticate users based on a shared entity token. It allows for temporary access to resources without requiring full user authentication.

Authorization Mechanism

Authorization is handled using the Pundit gem, which provides a simple and flexible way to manage authorization policies. The authorize and policy_scope methods are used to enforce authorization rules. These methods ensure that users have the necessary permissions to perform actions on specific resources. The authorize_resource method is also used to check if the user is authorized to access a particular resource.

Pundit Policies

Pundit policies are defined in the app/policies directory. Each policy corresponds to a specific model and contains methods that define the authorization rules for various actions (e.g., show?, create?, update?, destroy?). These methods return a boolean value indicating whether the user is authorized to perform the action.

Standard Response Structure

The controllers follow a standard response structure to ensure consistency in the API responses. The response_as method is used to format the responses based on the status of the request. Common response formats include response_as(:success, data: ...) for successful requests and response_as(:error, message: ..., code: ...) for error responses. This standardized response structure helps in maintaining a consistent API and makes it easier for clients to handle the responses. See api_response.rb for more details.

Examples

Success

response = ActiveModels::ApiResponse.success(data: { id: 1, name: "Example" })
usually data is derived from resource's json_attributes method as part of resources controller.

{
  "status": "success",
  "data": {
    "id": 1,
    "name": "Example"
  }
}

Failure

response = ActiveModels::ApiResponse.failure(
  message: "Validation failed",
  errors: [
    { attribute: "name", errors: ["can't be blank"] }
  ]
)
{
  "status": "failure",
  "data": {
    "id": 1,
    "name": "Example"
  },
  "message": "Validation failed",
  "errors": [
    {
      "attribute": "name",
      "errors": ["can't be blank"]
    }
  ]
}

Error

response = ActiveModels::ApiResponse.error(
  data: { id: 1, name: "Example" },
  message: "An error occurred",
  code: "error-code",
  errors: [
    { attribute: "name", errors: ["can't be blank"] }
  ]
)
{
  "status": "error",
  "data": {
    "id": 1,
    "name": "Example"
  },
  "message": "An error occurred",
  "code": "error-code",
  "errors": [
    {
      "attribute": "name",
      "errors": ["can't be blank"]
    }
  ]
}

Paginated

scope = Tag.page(1).per(10)
response = ActiveModels::ApiResponse.paginated(scope, { only: [:id, :name] })
{
  "status": "success",
  "data": [
    { "id": 1, "name": "Tag 1" },
    { "id": 2, "name": "Tag 2" }
    // ... more tags
  ],
  "pagination": {
    "total_count": 100,
    "current_page": 1,
    "total_pages": 10,
    "window": 4,
    "breakdown": [
      { "kind": "prev", "url": "/tags?page=0", "page": 0 },
      { "kind": "inner", "url": "/tags?page=1", "page": 1 },
      { "kind": "inner", "url": "/tags?page=2", "page": 2 },
      { "kind": "inner", "url": "/tags?page=3", "page": 3 },
      { "kind": "inner", "url": "/tags?page=4", "page": 4 },
      { "kind": "gap", "url": null, "page": 5 },
      { "kind": "inner", "url": "/tags?page=6", "page": 6 },
      { "kind": "inner", "url": "/tags?page=7", "page": 7 },
      { "kind": "inner", "url": "/tags?page=8", "page": 8 },
      { "kind": "inner", "url": "/tags?page=9", "page": 9 },
      { "kind": "next", "url": "/tags?page=2", "page": 2 }
    ]
  }
}