CSC/ECE 517 Spring 2025 - E2535 Reimplement User Profile Management (Frontend + Backend)

From Expertiza_Wiki
Jump to navigation Jump to search

Overview

The goal of this project is to reimplement the User Profile Management feature in the Expertiza system, replacing the legacy Ruby-based frontend with a modern, responsive interface built using React with TypeScript, while retaining and enhancing the existing Ruby on Rails backend.

This modernization effort addresses the challenges posed by outdated UI, limited form validation, and insecure session handling in the current implementation. By leveraging modern frameworks and best practices, the new system will allow users to securely update their profile details, manage email and notification preferences, and change passwords.

Problem Statement

From a security perspective, the current system does not follow best practices for handling sensitive operations such as password changes and session management. For instance:

  • Password updates do not log out other active sessions, leaving accounts vulnerable.
  • API responses may expose too much information, enabling account enumeration.
  • There is no rate limiting, making the system susceptible to brute-force attacks.

These limitations lead to:

  • Poor user experience (UX) and accessibility.
  • Increased technical debt and maintenance burden.
  • Security risks in managing user authentication and sessions.

As Expertiza evolves, it is essential to rebuild the profile management feature using modern frontend technologies and robust backend practices that improve usability, security, and maintainability.

Existing Issues

  • Inconsistent Form Validation:
    • Validation is not uniform, leading to unclear user feedback.
    • React Hook Form will be used for consistent validation.
  • Limited Security for Password Changes:
    • No session invalidation after password updates.
    • New implementation will log out all active sessions and invalidate old tokens.
  • Basic Error Handling:
    • Current error handling is minimal and vague.
    • Enhanced error messages and better handling for edge cases will be implemented.

Proposed Changes

Front-End

Enhance usability and data integrity through a tab-based layout that organizes the profile page into distinct sections, ensuring that users can update specific areas independently without risk of data loss.

Key Features

  • Tab-based Layout
    • The profile page will be restructured into multiple tabs, with each tab dedicated to a specific subsection (e.g., Personal Details, Password, Notification Settings).
    • This separation allows users to navigate between different sections without losing unsaved data from other areas.
  • Dedicated Forms for Each Section
    • Implement separate forms for each tab using React Hook Form which enables efficient state management and robust schema-driven validations.
    • Ensure that each form manages its own state and validation logic independently, avoiding interference between sections.
  • Enhanced User Experience
    • Users will benefit from clear, compartmentalized UI sections, reducing cognitive load and streamlining the update process.
    • Real-time validation and dedicated feedback per section will help prevent input errors and improve overall form usability.

Proposed Wireframes

Wireframe 1 Wireframe 2 Wireframe 3 Wireframe 4 Wireframe 5

Attached Wireframes are not to be mistaken as actual designs or the live application/website.

File Modifications

Back-End

The backend will be enhanced to support secure, RESTful profile and password updates, while incorporating session management and protection against attacks.

User Model Enhancements

Add/update methods to:

  • Safely update user profile fields (e.g., name, email)
  • Change passwords using bcrypt for hashing
  • Invalidate old sessions upon password update

New API Endpoints

There are some of the proposed endpoints for the reimplementation.

  • `GET /api/v1/profile` – Fetch current user details
  • `PUT /api/v1/profile` – Update user profile
  • `PUT /api/v1/profile/password` – Update password securely

Session Management

  • Log out all active sessions after a successful password change
  • Revoke previous tokens to ensure account safety

Security Measures

  • Implement rate limiting with `Rack::Attack` to prevent brute-force attacks
  • Return generic error messages for failed logins to avoid account enumeration

Database Changes

The existing `users` table currently includes all the necessary fields required for user profile management, such as name, email, and password. As of now, no immediate changes to the schema are required. However, during the implementation phase, if additional fields are identified as necessary, the schema will be updated accordingly. Any modifications to the schema will be thoroughly reviewed and documented to ensure they align with the project requirements and maintain data integrity.

Overview of Classes

UsersController (app/controllers/api/v1/users_controller.rb)

Will be extended to:

  • `GET /profile`: Return current user details (JSON)
  • `PUT /profile`: Update user attributes like name, email, notification preferences

PasswordsController (app/controllers/api/v1/passwords_controller.rb)

Will handle secure password changes via:

  • `PUT /password`:
    • Authenticate using current password
    • Update using bcrypt
    • Invalidate old sessions/tokens
    • Issue a new session token

User Model (app/models/user.rb)

Updated to include:

  • `update_profile(params)` method for secure profile updates
  • `change_password(current_password, new_password)` with proper validations
  • Session/token handling logic after password changes

AuthenticationTokenService / Session Handler

  • Issues new tokens upon password change
  • Invalidates previous sessions to prevent unauthorized access

ApplicationController

  • Handles token-based authentication for API access
  • Provides common methods for current user lookup and error handling

Rack::Attack Configuration (config/initializers/rack_attack.rb)

  • Implements rate limiting for profile and password update endpoints
  • Helps mitigate brute-force and enumeration attacks

Class Diagram

Class Diagram

Design Goals

Usability & User Experience (UX)

  • Intuitive Interface: The user profile page should be easy to navigate with clear sections for updating profile information, changing passwords, and managing notifications. Each form will include:
    • Informative tooltips and labels
    • Clear error messages and success notifications
    • Loading indicators to improve responsiveness
  • Responsive Design: The profile page must be fully responsive across various devices, from desktops to mobile phones, ensuring an optimized experience for all users.
  • Smooth Transitions: Using React Router, transitions between different sections (such as profile editing and password change) should feel seamless without page reloads, improving the overall user experience.

Security

  • Password Management
    • Use bcrypt to hash passwords securely before saving them to the database.
    • Implement token-based authentication for user sessions.
    • When the password is updated, invalidate all active sessions and issue a new authentication token to prevent unauthorized access.
  • Rate Limiting: To prevent brute-force attacks, rate limiting will be implemented on sensitive endpoints like the password change and login routes using Rack::Attack.
  • Account Enumeration Protection: Return generic error messages on failed login attempts and password changes to prevent attackers from determining whether a username/email is valid.
  • Session Management: When a password is successfully updated, ensure that all other active sessions are logged out and new tokens are issued to prevent session hijacking.

Maintainability & Scalability

  • Modular Codebase: The code will follow a modular approach by breaking down functionality into smaller, reusable components (both frontend and backend). For example, the frontend components will be encapsulated, and the backend logic will be implemented using service objects where appropriate.
  • RESTful API Structure: The backend will adhere to RESTful principles for API endpoints, ensuring that the architecture is scalable and easily extendable in the future. Common conventions like GET, PUT, and PATCH will be used for profile and password management.
  • Separation of Concerns: The frontend and backend will be clearly separated with well-defined API contracts. The frontend will handle the presentation and user interactions, while the backend will focus on business logic and data persistence.
  • Testing and Quality Assurance: Automated tests will be implemented to ensure the robustness of the system. RSpec will be used for backend testing, and Jest will be used for frontend testing.

Code Quality

  • Adhere to DRY Principles: The code will avoid repetition by creating reusable components and centralized validation logic for both frontend and backend. For example, validation logic for user input will be shared between the frontend (React Hook Form validation) and the backend (model validations).
  • Consistent API Responses: API responses will be standardized across endpoints to ensure a consistent interface for the frontend. This includes:
    • Returning relevant HTTP status codes
    • Structuring responses with consistent fields
  • Clear Documentation: The documentation will be maintained, detailing endpoint routes, required parameters, expected responses, and error handling procedures.

Implementation Plan

Frontend Implementation

  • Create new React components
  • Implement validation of form fields
  • Connect to backend endpoints
  • Add success/confirmation messages on profile updates
  • Add loading states and error handling
  • Implement responsive design

Backend Implementation

  • Update UserController with new endpoints
  • Implement password security measures
  • Add rate limiting functionality
  • Update user model validation
  • Implement session management

Sequence Diagram

Sequence Diagram


Implementation

Frontend Implementation

The new UI redesign replicates the functionality of the original page, now leveraging TypeScript for type safety and React for dynamic rendering. This approach enhances the user experience by providing a more responsive and accessible interface, while adhering to modern design principles. This feature redesigns the Edit Profile page, implementing a modern, form-based interface using React, TypeScript, and React Hook Form, with full backend integration for real-time data fetch and update. The new UI focuses on form validation, user experience, and code maintainability.

Key Features:

  • Form Management: Uses `react-hook-form` and `yup` for structured form handling and ensuring reliable input checks and robust form validation
  • Backend Integration: Profile data is fetched from the backend and is used to populate the form fields. On submission, this data is validated and triggers an API request to update profile and password (if the password is changed).
  • API Communication: Uses `axios` for making HTTP requests to fetch user profile data and submit updates to the backend, including GET, PATCH and POST requests with authentication headers.
  • UI Enhancements: Improved error handling and form usability. `Yup` validation checks data before it is sent to the backend, ensuring fast display of errors. The form also display success and error messages for API requests.

Form View - User can edit profile fields Success Message - Profile updated successfully Error Message - Displaying validation error

Backend Implementation

API Endpoints

  • GET /api/v1/users/:id/get_profile
    • Fetches user profile data and preferences for the frontend.
    • It returns basic attributes like full_name, email, and handle.
# GET /api/v1/users/:id/get_profile : Returns basic user profile information and email preferences
  def get_profile
    user = User.includes(:institution).find(params[:id])

    render json: {
      id: user.id,
      full_name: user.full_name,
      email: user.email,
      handle: user.handle || '',
      can_show_actions: user.can_show_actions,
      time_zone: user.time_zone || 'GMT-05:00',
      language: user.language || 'No Preference',
      email_on_review: user.email_on_review.nil? ? true : user.email_on_review,
      email_on_submission: user.email_on_submission.nil? ? true : user.email_on_submission,
      email_on_review_of_review: user.email_on_review_of_review.nil? ? true : user.email_on_review_of_review,
      institution: {
        id: user.institution&.id || 0,
        name: user.institution&.name || 'Other'
      }
    }, status: :ok
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'User not found' }, status: :not_found
  end
  • PATCH /api/v1/users/:id/update_profile
    • Updates editable user profile fields like full_name, email, handle, language and time_zone.
  # PATCH /users/:id
  def update_profile
    user = User.find(params[:id])
  
    if user.update(user_params)
      render json: {
        message: 'Profile updated successfully.',
        user: user.as_json(except: [:password_digest])
      }, status: :ok
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'User not found' }, status: :not_found
  end

  • POST /api/v1/users/:id/update_password
    • Securely allows users to change their passwords by comparing password and confirmPassword.
    • If the password is successfully updated, it issues a new JWT with updated jwt_version to invalidate old tokens.
  # POST /users/:id/update_password
  def update_password
    user = User.find(params[:id])
  
    password = params[:password]
    confirm_password = params[:confirmPassword]

    if password.blank? || confirm_password.blank?
      return render json: { error: 'Both password and confirmPassword are required' }, status: :bad_request
    end
  
    if password != confirm_password
      return render json: { error: 'Passwords do not match' }, status: :unprocessable_entity
    end

    if user.update(password: password, password_confirmation: confirm_password)
      # update jwt_version and issue new token
      user.update(jwt_version: SecureRandom.uuid)
  
      payload = {
        id: user.id,
        name: user.name,
        full_name: user.full_name,
        role: user.role.name,
        institution_id: user.institution.id,
        jwt_version: user.jwt_version
      }

      new_token = JsonWebToken.encode(payload, 24.hours.from_now)

      render json: { 
        message: 'Password updated successfully',
        token: new_token,
        user: user.as_json(except: [:password_digest])
        }, status: :ok
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end
  • Update routes.rb
        member do
          get :get_profile           # GET /api/v1/users/:id/get_profile
          patch :update_profile           # PATCH /api/v1/users/:id/update_profile
          post  :update_password          # POST  /api/v1/users/:id/update_password
        end
  • Compliance with Design Requirements:
    • update_profile restricts changes to non-sensitive fields only.
    • update_password securely updates the password, and a new token is issued.
    • All endpoints return appropriate HTTP status codes.

Session Management for Password Updates

  • This implementation ensures secure session handling whenever a user updates their password. The goal is to prevent unauthorized access using old tokens and provide a new authenticated session for the user.
    • Log out all active sessions upon password change.
    • Issue a new authentication token after a successful password reset.
    • Invalidate old session tokens to enhance security.
    # Invalidate token if jwt_version no longer matches
    if auth_token[:jwt_version] != @current_user.jwt_version
      render json: { error: 'Token has been invalidated. Please login again.' }, status: :unauthorized
      return
    end

Security Enhancements

Application Level Security:

File: app/controllers/authentication_controller.rb

Changes:

  • Introduced random delay (0.1 – 0.3 seconds) after each login attempt to mitigate timing attacks
 def login
   # Add a small random delay to prevent timing attacks
   sleep(rand(0.1..0.3))
   # ... rest of login logic
 end
  • Used a generic error message Invalid credentials to prevent user enumeration
 def login
   # ... authentication logic
   render json: { error: 'Invalid credentials' }, status: :unauthorized
 end
  • Ensured proper status code usage:
    • 401 Unauthorized for failed login attempts
    • 429 Too Many Requests when rate limit is exceeded
 Rack::Attack.throttled_responder = lambda do |env|
   [
     429, # status
     { 'Content-Type' => 'application/json' }, # headers
     [{ error: 'Rate limit exceeded. Try again later.' }.to_json] # body
   ]
 end

Network/Request Level Security:

File: config/initializers/rack_attack.rb

Changes:

  • Implemented in-memory rate limiting using a hash to track login attempts
 Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
  • Set maximum number of attempts to 5 within a 20-second window
 throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
   if req.path == '/login' && req.post?
     req.ip
   end
 end
  • Applied rate limiting based on both IP Address and Username
 # IP-based throttling
 throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
   if req.path == '/login' && req.post?
     req.ip
   end
 end
 # Username-based throttling
 throttle('logins/username', limit: 5, period: 20.seconds) do |req|
   if req.path == '/login' && req.post?
     req.params['user_name'].to_s.downcase
   end
 end
  • Added conditional handling to allow unrestricted access from localhost (127.0.0.1 and ::1) for testing
 safelist('allow-localhost') do |req|
   '127.0.0.1' == req.ip || '::1' == req.ip
 end

Testing

R Spec Tests

  • We have added an R spec test for the users_controller.rb in the file users_spec.rb. This test mainly checks the three methods we have implemented in this project.

Manual Testing for Sessions

  • In order to test the token generation and invalidation, we can use the web browser console to test the API endpoint. The following commands are used for this testing:

fetch("http://localhost:3002/login", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    user_name: "admin",  // add username
    password: "password123"    // add password
  })
})
.then(res => res.json())
.then(data => {
  console.log("Token A:", data.token);
  window.tokenA = data.token;

  // Decode token to get user ID
  const base64Url = data.token.split('.')[1];
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  const jsonPayload = decodeURIComponent(atob(base64).split('').map(c =>
    '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
  ).join(''));
  const payload = JSON.parse(jsonPayload);
  window.userId = payload.id;

  console.log("User ID:", window.userId);
});







NEXT:



 fetch(`http://localhost:3002/api/v1/users/${window.userId}`, {
  method: "GET",
  headers: {
    "Authorization": "Bearer " + window.tokenA
  }
})
.then(res => res.json())
.then(data => console.log(" User info with tokenA:", data));



NEXT: 



fetch(`http://localhost:3002/api/v1/users/${window.userId}/update_password`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + window.tokenA
  },
  body: JSON.stringify({
    password: "newpass123",             //  New password
    confirmPassword: "newpass123"
  })
})
.then(res => res.json())
.then(data => {
  console.log(" Password updated!");
  console.log(" Token B:", data.token);
  window.tokenB = data.token;
});

NEXT:




fetch(`http://localhost:3002/api/v1/users/${window.userId}`, {
  method: "GET",
  headers: {
    "Authorization": "Bearer " + window.tokenB
  }
})
.then(res => res.json())
.then(data => console.log(" User info with tokenB:", data));



NEXT: 



fetch(`http://localhost:3002/api/v1/users/${window.userId}`, {
  method: "GET",
  headers: {
    "Authorization": "Bearer " + window.tokenA
  }
})
.then(res => res.json())
.then(data => console.log(" Token A Response:", data))
.catch(err => console.error("Error:", err));

Testing Implementation for Security Enhancements

File: spec/requests/rack_attack_spec.rb

Changes:

  • Added comprehensive test coverage for login rate limiting
    • Verified successful login scenario
    • Verified rate limiting triggered after too many failed login attempts from the same IP address
    • Verified rate limiting triggered after too many failed attempts using the same username
    • Verified localhost IPs are exempt from rate limiting

Manual Testing Script

File: test_rate_limiting.sh

Changes:

  • Created shell script to simulate real-world usage patterns
  • Verified rate limiting behavior using multiple incorrect login attempts
  • Confirmed general login rate limiting functionality

Mentor

Team Members

References