From 060c3162b200583ffbe8e3c545cc3ff440f0bf1e Mon Sep 17 00:00:00 2001 From: ZHONGYU1111 <2922833288@qq.com> Date: Fri, 17 May 2024 07:40:29 +1000 Subject: [PATCH 1/2] Add documentation for Rails backend and architecture diagram --- public/diagram.png | Bin 0 -> 67349 bytes .../Documentation-Backlog-Rails-Backend.md | 1389 +++++++++++++++++ 2 files changed, 1389 insertions(+) create mode 100644 public/diagram.png create mode 100644 src/content/docs/products/ontrack/documentation/Documentation_Backlog_for_rails/Documentation-Backlog-Rails-Backend.md diff --git a/public/diagram.png b/public/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..872ae906bba5d935b0c723dcbf8c6f1a9004ddd1 GIT binary patch literal 67349 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�AlADP`jtM$vm#RU#8fAj0Ez*sUmSZEb~@ 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 + + + ``` + +- **Request Method**: POST +- **URL**: `/users/sign_in` +- **Parameters**: + ```json + { + "user": { + "email": "user@example.com", + "password": "password" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Signed in successfully." + } + ``` + +- **Request Method**: DELETE +- **URL**: `/users/sign_out` +- **Parameters**: None +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Signed out successfully." + } + ``` + +### Password Management +- **Request Method**: GET +- **URL**: `/users/password/new` +- **Parameters**: None +- **Response Format**: HTML +- **Example**: + ```html +
+ +
+ ``` + +- **Request Method**: GET +- **URL**: `/users/password/edit` +- **Parameters**: None +- **Response Format**: HTML +- **Example**: + ```html +
+ +
+ ``` + +- **Request Method**: PATCH/PUT/POST +- **URL**: `/users/password` +- **Parameters**: + ```json + { + "user": { + "email": "user@example.com", + "password": "new_password", + "password_confirmation": "new_password" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Password updated successfully." + } + ``` + +### User Registration +- **Request Method**: GET +- **URL**: `/users/sign_up` +- **Parameters**: None +- **Response Format**: HTML +- **Example**: + ```html +
+ +
+ ``` + +- **Request Method**: GET +- **URL**: `/users/edit` +- **Parameters**: None +- **Response Format**: HTML +- **Example**: + ```html +
+ +
+ ``` + +- **Request Method**: PATCH/PUT/DELETE/POST +- **URL**: `/users` +- **Parameters**: + ```json + { + "user": { + "email": "user@example.com", + "password": "password", + "password_confirmation": "password" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "User registered/updated successfully." + } + ``` + +## File Downloads + +### Download Course Resources +- **Request Method**: GET +- **URL**: `/api/submission/unit/:id/portfolio` +- **Parameters**: + ```json + { + "id": "unit_id" + } + ``` +- **Response Format**: File +- **Example**: + ```json + { + "file": "portfolio.pdf" + } + ``` + +- **Request Method**: GET +- **URL**: `/api/submission/unit/:id/task_definitions/:task_def_id/download_submissions` +- **Parameters**: + ```json + { + "id": "unit_id", + "task_def_id": "task_definition_id" + } + ``` +- **Response Format**: File +- **Example**: + ```json + { + "file": "submissions.zip" + } + ``` + +- **Request Method**: GET +- **URL**: `/api/submission/unit/:id/task_definitions/:task_def_id/student_pdfs` +- **Parameters**: + ```json + { + "id": "unit_id", + "task_def_id": "task_definition_id" + } + ``` +- **Response Format**: File +- **Example**: + ```json + { + "file": "student_pdfs.zip" + } + ``` + +- **Request Method**: GET +- **URL**: `/api/units/:id/all_resources` +- **Parameters**: + ```json + { + "id": "unit_id" + } + ``` +- **Response Format**: File +- **Example**: + ```json + { + "file": "resources.zip" + } + ``` + +## API Root and Documentation + +### API Root +- **URL**: `/` +- **Parameters**: None +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "API Root" + } + ``` + +### Swagger API Documentation +- **URL**: `/api/docs` +- **Parameters**: None +- **Response Format**: HTML +- **Example**: + ```html + + +

Swagger API Documentation

+ + + ``` + +## Sidekiq + +### Sidekiq Management Interface +- **URL**: `/sidekiq` +- **Parameters**: None +- **Response Format**: HTML +- **Example**: + ```html + + +

Sidekiq Management

+ + + ``` + +## Action Mailbox + +### Email Handling +- **Request Method**: POST +- **URL**: `/rails/action_mailbox/postmark/inbound_emails` +- **Parameters**: + ```json + { + "email": { + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Email received via Postmark" + } + ``` + +- **Request Method**: POST +- **URL**: `/rails/action_mailbox/relay/inbound_emails` +- **Parameters**: + ```json + { + "email": { + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Email received via Relay" + } + ``` + +- **Request Method**: POST +- **URL**: `/rails/action_mailbox/sendgrid/inbound_emails` +- **Parameters**: + ```json + { + "email": { + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Email received via Sendgrid" + } + ``` + +- **Request Method**: GET +- **URL**: `/rails/action_mailbox/mandrill/inbound_emails` +- **Parameters**: None +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Mandrill health check passed" + } + ``` + +- **Request Method**: POST +- **URL**: `/rails/action_mailbox/mandrill/inbound_emails` +- **Parameters**: + ```json + { + "email": { + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Email received via Mandrill" + } + ``` + +- **Request Method**: POST +- **URL**: `/rails/action_mailbox/mailgun/inbound_emails/mime` +- **Parameters**: + ```json + { + "email": { + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + } + ``` +- **Response Format**: JSON +- **Example**: + ```json + { + "message": "Email received via Mailgun" + } + ``` + +## Action Mailbox Conductor + +### Conductor Management Interface +- **Request Method**: GET +- **URL**: `/rails/conductor/action_mailbox/inbound_emails` +- **Parameters**: None +- **Response Format**: JSON +- **Example**: + ```json + { + "inbound_emails": [ + { + "id": "1", + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + ] + } + ``` + +# Environment Configurations + +This section documents the configurations found in the `config/environments` directory of the Rails backend for Ontrack. Each file configures the application behavior for different environments such as development, production, staging, and testing. + +## 1. Development Environment (`development.rb`) + +### Purpose +The `development.rb` file contains settings specific to the development environment. It prioritizes ease of development and debugging. + +### Key Configurations +- **Code Reloading:** + - `config.cache_classes = false`: Disables class caching, allowing code changes to be reflected without restarting the server. +- **Eager Loading:** + - `config.eager_load = false`: Disables eager loading of code on boot. +- **Error Reporting:** + - `config.consider_all_requests_local = true`: Shows full error reports. +- **Caching:** + - Conditional caching based on environment variables or presence of a file. + - `config.action_controller.perform_caching = true/false`: Enables or disables caching. + - `config.cache_store = :memory_store / :redis_cache_store / :null_store`: Configures the cache store based on environment variables. +- **File Storage:** + - `config.active_storage.service = :local`: Stores uploaded files on the local file system. +- **Action Mailer:** + - `config.action_mailer.raise_delivery_errors = false`: Ignores delivery errors. + - `config.action_mailer.perform_caching = false`: Disables mailer caching. + - `config.action_mailer.delivery_method = :file`: Writes emails to file instead of sending them. +- **Deprecation Notices:** + - `config.active_support.deprecation = :log`: Logs deprecation warnings. + - `config.active_support.disallowed_deprecation = :raise`: Raises exceptions for disallowed deprecations. + - `config.active_support.disallowed_deprecation_warnings = []`: Specifies which deprecation warnings are disallowed. +- **Database:** + - `config.active_record.migration_error = :page_load`: Raises an error on page load if there are pending migrations. + - `config.active_record.verbose_query_logs = true`: Highlights code that triggered database queries in logs. +- **File Watcher:** + - `config.file_watcher = ActiveSupport::EventedFileUpdateChecker`: Uses an evented file watcher to asynchronously detect changes. +- **Logging:** + - `config.log_level = :debug`: Sets the logging level to debug. +- **Miscellaneous:** + - `config.action_dispatch.best_standards_support = :builtin`: Uses best-standards-support built into browsers. + - `Faker::Config.random = Random.new(77)`: Sets deterministic randomness for Faker. + - `config.pdfgen_quiet = false`: Sets the verbosity of pdfgen logs. + - `config.active_record.encryption.key_derivation_salt = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'default_salt'`: Configures ActiveRecord encryption keys. + +## 2. Production Environment (`production.rb`) + +### Purpose +The `production.rb` file contains settings specific to the production environment, optimized for performance and security. + +### Key Configurations +- **Code Caching:** + - `config.cache_classes = true`: Enables class caching for better performance. +- **Caching:** + - `config.cache_store = :redis_cache_store`: Uses Redis for caching. + - Configures Redis connection and error handling. +- **Error Reporting:** + - `config.consider_all_requests_local = false`: Disables full error reports. +- **Static Files:** + - `config.serve_static_files = true`: Enables serving of static files. +- **Eager Loading:** + - `config.eager_load = true`: Enables eager loading of code on boot. +- **I18n:** + - `config.i18n.fallbacks = true`: Enables locale fallbacks for I18n. +- **Deprecation Notices:** + - `config.active_support.deprecation = :notify`: Sends deprecation notices to registered listeners. +- **Middleware:** + - `config.middleware.delete Rack::Runtime`: Removes runtime middleware to harden against timing attacks. +- **Logging:** + - `config.log_level = :info`: Sets the logging level to info. +- **Action Mailer:** + - `config.action_mailer.perform_deliveries = (ENV['DF_MAIL_PERFORM_DELIVERIES'] || 'yes') == 'yes'`: Configures whether to perform email deliveries. + - `config.action_mailer.delivery_method = :smtp`: Uses SMTP for email delivery. + - Configures SMTP settings based on environment variables. +- **ActiveRecord Encryption:** + - Configures ActiveRecord encryption keys using environment variables. + +## 3. Staging Environment (`staging.rb`) + +### Purpose +The `staging.rb` file configures settings for the staging environment, which mirrors the production environment with minor changes. + +### Key Configurations +- **SSL:** + - `config.force_ssl = false`: Disables forcing SSL. +- **Logging:** + - `config.log_level = :info`: Sets the logging level to info. +- **Deterministic Randomness:** + - `Faker::Config.random = Random.new(77)`: Sets deterministic randomness for Faker. + +## 4. Test Environment (`test.rb`) + +### Purpose +The `test.rb` file contains settings specific to the test environment, optimized for running the application's test suite. + +### Key Configurations +- **Code Caching:** + - `config.cache_classes = true`: Enables class caching for tests. +- **Static Files:** + - `config.serve_static_files = true`: Configures static asset server for tests. + - `config.static_cache_control = 'public, max-age=3600'`: Sets Cache-Control headers. +- **Eager Loading:** + - `config.eager_load = false`: Disables eager loading. +- **Error Reporting:** + - `config.consider_all_requests_local = true`: Shows full error reports. +- **Caching:** + - `config.action_controller.perform_caching = false`: Disables caching. +- **Exception Handling:** + - `config.action_dispatch.show_exceptions = false`: Raises exceptions instead of rendering templates. +- **Request Forgery Protection:** + - `config.action_controller.allow_forgery_protection = false`: Disables request forgery protection. +- **Action Mailer:** + - `config.action_mailer.delivery_method = :test`: Uses the test delivery method for emails. +- **Deprecation Notices:** + - `config.active_support.deprecation = :stderr`: Prints deprecation notices to stderr. +- **Logging:** + - `config.log_level = :warn`: Sets the logging level to warn. +- **ActiveRecord Encryption:** + - Configures ActiveRecord encryption keys. +- **Environment Variables:** + - Sets specific environment variables for the test environment. + +# Initializer Configurations + +This section documents the configurations found in the `config/initializers` directory of the Rails backend for Ontrack. Each file configures the application behavior during the initialization phase. + +## 1. Backtrace Silencer (`backtrace_silencers.rb`) + +### Purpose +This file allows you to add or remove backtrace silencers for libraries you don't wish to see in your backtraces. + +### Key Configurations +- **Add Silencer**: + ```ruby + # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + ``` +- **Remove All Silencers**: + ```ruby + # Rails.backtrace_cleaner.remove_silencers! + ``` + +## 2. Devise Configuration (`devise.rb`) + +### Purpose +This file is used to configure Devise, a flexible authentication solution for Rails. + +### Key Configurations +- **Mailer Configuration**: + ```ruby + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + ``` + +- **ORM Configuration**: + ```ruby + require 'devise/orm/active_record' + ``` + +- **Authentication Keys**: + ```ruby + config.authentication_keys = [:username] + ``` + +- **Case-Insensitive Keys**: + ```ruby + config.case_insensitive_keys = [:email, :username] + ``` + +- **Whitespace Stripped Keys**: + ```ruby + config.strip_whitespace_keys = [:email, :username] + ``` + +- **Session Storage**: + ```ruby + config.skip_session_storage = [:http_auth] + ``` + +- **Password Length**: + ```ruby + config.password_length = 8..128 + ``` + +- **Token Expiry**: + ```ruby + config.reset_password_within = 6.hours + ``` + +- **Sign Out Method**: + ```ruby + config.sign_out_via = :delete + ``` + +- **Navigational Formats**: + ```ruby + config.navigational_formats = ['*/*', :json] + ``` + +- **Secret Key**: + ```ruby + config.secret_key = Doubtfire::Application.secrets.secret_key_devise if Rails.env.production? + ``` + +- **LDAP Configuration**: + ```ruby + config.ldap_use_admin_to_bind = ENV.fetch('DF_LDAP_USE_ADMIN_TO_BIND', 'false').to_s.downcase != 'false' + ``` + +- **Responder Configuration**: + ```ruby + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + ``` + +## 3. Inflections (`inflections.rb`) + +### Purpose +This file is used to add new inflection rules for pluralization and singularization of words. + +### Key Configurations +- **Irregular Inflections**: + ```ruby + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'campus', 'campuses' + end + ``` + +## 4. Log Initializer (`log_initializer.rb`) + +### Purpose +This file configures the logging output and format. + +### Key Configurations +- **Log Output to STDOUT**: + ```ruby + unless Rails.env.test? + Rails.logger.broadcast_to(ActiveSupport::Logger.new($stdout)) + end + ``` + +- **Custom Log Formatter**: + ```ruby + class FormatifFormatter < Logger::Formatter + include ActiveSupport::TaggedLogging::Formatter + + def call(severity, timestamp, _progname, msg) + remote_ip = Thread.current.thread_variable_get(:ip) || 'unknown' + "#{timestamp},#{remote_ip},#{severity}: #{msg.to_s.gsub(/\n/, '\n')}\n" + end + end + + Rails.logger.formatter = FormatifFormatter.new + ``` + +## 5. MIME Types (`mime_types.rb`) + +### Purpose +This file allows you to add new MIME types for use in respond_to blocks. + +### Key Configurations +- **Add New MIME Types**: + ```ruby + # Mime::Type.register "text/richtext", :rtf + # Mime::Type.register_alias "text/html", :iphone + ``` + +## 6. Session Store (`session_store.rb`) + +### Purpose +This file configures how session data is stored. + +### Key Configurations +- **Cookie Store**: + ```ruby + Doubtfire::Application.config.session_store :cookie_store, key: '_doubtfire_session' + ``` + +- **ActiveRecord Store**: + ```ruby + # Doubtfire::Application.config.session_store :active_record_store + ``` + +## 7. Sidekiq Configuration (`sidekiq.rb`) + +### Purpose +This file configures Sidekiq, a background job processing library. + +### Key Configurations +- **Server Configuration**: + ```ruby + Sidekiq.configure_server do |config| + config.redis = { url: ENV.fetch('DF_REDIS_SIDEKIQ_URL', 'redis://localhost:6379/1') } + config.logger = Rails.logger + end + ``` + +- **Client Configuration**: + ```ruby + Sidekiq.configure_client do |config| + config.redis = { url: ENV.fetch('DF_REDIS_SIDEKIQ_URL', 'redis://localhost:6379/1') } + config.logger = Rails.logger + end + ``` + +## 8. Swagger Configuration (`swagger.rb`) + +### Purpose +This file configures Swagger, a tool for documenting APIs. + +### Key Configurations +- **Swagger URL and App URL**: + ```ruby + GrapeSwaggerRails.options.url = '/api/swagger_doc' + GrapeSwaggerRails.options.before_action do + GrapeSwaggerRails.options.app_url = request.protocol + request.host_with_port + end + + GrapeSwaggerRails.options.before_filter_proc = proc { + GrapeSwaggerRails.options.app_url = request.protocol + request.host_with_port + } + ``` + +## 9. TurnItIn Initializer (`turn_it_in_initializer.rb`) + +### Purpose +This file initializes the TurnItIn API configuration. + +### Key Configurations +- **Load Configuration**: + ```ruby + require_relative '../../app/helpers/turn_it_in' + config = Doubtfire::Application.config + + TurnItIn.load_config(config) + ``` + +- **Background Jobs**: + ```ruby + if config.tii_enabled + require 'tca_client' + config.logger = Rails.logger + + unless Rails.env.test? + config.after_initialize do + TurnItIn.launch_tii(with_webhooks: Rails.env.production?) + end + end + + if Rails.env.development? + TCAClient.configure do |tii_config| + tii_config.debugging = true + end + end + end + ``` + +## 10. Wrap Parameters (`wrap_parameters.rb`) + +### Purpose +This file configures parameter wrapping for JSON requests. + +### Key Configurations +- **Enable Parameter Wrapping for JSON**: + ```ruby + ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] + end + ``` + +- **Disable Root Element in JSON**: + ```ruby + ActiveSupport.on_load(:active_record) do + self.include_root_in_json = false + end + ``` + +# Locale Configurations + +This section documents the configurations found in the `config/locales` directory of the Rails backend for Ontrack. Each file contains translations and localization settings for the application. + +## 1. Devise Locale (`devise.en.yml`) + +### Purpose +This file contains the English translations for error messages and notifications used by the Devise authentication library. + +### Key Configurations + +- **Error Messages**: + ```yaml + en: + errors: + messages: + expired: "has expired, please request a new one" + not_found: "not found" + already_confirmed: "was already confirmed, please try signing in" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" + ``` + +- **Devise Notifications**: + ```yaml + en: + devise: + failure: + already_authenticated: 'You are already signed in.' + unauthenticated: 'You need to sign in before continuing.' + unconfirmed: 'You have to confirm your account before continuing.' + locked: 'Your account is locked.' + invalid: 'Invalid username or password.' + invalid_token: 'Invalid authentication token.' + timeout: 'Your session expired, please sign in again to continue.' + inactive: 'Your account was not activated yet.' + sessions: + signed_in: 'Signed in successfully.' + signed_out: 'Signed out successfully.' + passwords: + send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' + updated: 'Your password was changed successfully. You are now signed in.' + updated_not_active: 'Your password was changed successfully.' + send_paranoid_instructions: "If your e-mail exists on our database, you will receive a password recovery link on your e-mail" + confirmations: + send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' + send_paranoid_instructions: 'If your e-mail exists on our database, you will receive an email with instructions about how to confirm your account in a few minutes.' + confirmed: 'Your account was successfully confirmed. You are now signed in.' + registrations: + signed_up: 'Welcome! You have signed up successfully.' + inactive_signed_up: 'You have signed up successfully. However, we could not sign you in because your account is %{reason}.' + updated: 'You updated your account successfully.' + destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' + reasons: + inactive: 'inactive' + unconfirmed: 'unconfirmed' + locked: 'locked' + unlocks: + send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' + unlocked: 'Your account was successfully unlocked. You are now signed in.' + send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.' + omniauth_callbacks: + success: 'Successfully authorized from %{kind} account.' + failure: 'Could not authorize you from %{kind} because "%{reason}".' + mailer: + confirmation_instructions: + subject: 'Confirmation instructions' + reset_password_instructions: + subject: 'Reset password instructions' + unlock_instructions: + subject: 'Unlock Instructions' + ``` + +## 2. Bootstrap Locale (`en.bootstrap.yml`) + +### Purpose +This file contains English translations for common actions and labels used in the Bootstrap framework. + +### Key Configurations + +- **Helper Actions and Links**: + ```yaml + en: + helpers: + actions: "Actions" + links: + back: "Back" + cancel: "Cancel" + confirm: "Are you sure?" + destroy: "Delete" + new: "New" + edit: "Edit" + titles: + edit: "Edit" + save: "Save" + new: "New" + delete: "Delete" + ``` + +## 3. General Locale (`en.yml`) + +### Purpose +This file contains general English translations used in the application. + +### Key Configurations + +- **General Translations**: + ```yaml + en: + hello: "Hello world" + ``` + +## 4. Simple Form Locale (`simple_form.en.yml`) + +### Purpose +This file contains English translations for the Simple Form gem, which is used to create forms in Rails applications. + +### Key Configurations + +- **Form Labels and Hints**: + ```yaml + en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + error_notification: + default_message: "Please review the problems below:" + # Labels and hints examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + ``` + +# Additional Configurations + +This section documents various configuration files found in the `config` directory of the Rails backend for Ontrack. These files include core application settings, database configurations, institution-specific settings, and more. + +## 1. Application Configuration (`application.rb`) + +### Purpose +This file contains the core configuration settings for the Rails application. + +### Key Configurations + +- **Load Defaults**: + ```ruby + config.load_defaults 7.0 + ``` + +- **Environment Variables**: + ```ruby + Dotenv::Railtie.load + ``` + +- **Authentication Method**: + ```ruby + config.auth_method = (ENV['DF_AUTH_METHOD'] || :database).to_sym + ``` + +- **Student Work Directory**: + ```ruby + config.student_work_dir = ENV['DF_STUDENT_WORK_DIR'] || "#{Rails.root}/student_work" + ``` + +- **Credentials**: + ```ruby + credentials.secret_key_base = ENV.fetch('DF_SECRET_KEY_BASE', Rails.env.production? ? nil : 'default_secret_key_base') + credentials.secret_key_attr = ENV.fetch('DF_SECRET_KEY_ATTR', Rails.env.production? ? nil : 'default_secret_key_attr') + credentials.secret_key_devise = ENV.fetch('DF_SECRET_KEY_DEVISE', Rails.env.production? ? nil : 'default_secret_key_devise') + credentials.secret_key_aaf = ENV.fetch('DF_SECRET_KEY_AAF', Rails.env.production? ? nil : 'secret_key_aaf') + credentials.secret_key_moss = ENV.fetch('DF_SECRET_KEY_MOSS', nil) + ``` + +- **Institution Settings**: + ```ruby + config.institution = YAML.load_file("#{Rails.root}/config/institution.yml").with_indifferent_access + ``` + +- **SAML Authentication**: + ```ruby + if config.auth_method == :saml + config.saml = HashWithIndifferentAccess.new + config.saml[:SAML_metadata_url] = ENV.fetch('DF_SAML_METADATA_URL', nil) + config.saml[:assertion_consumer_service_url] = ENV.fetch('DF_SAML_CONSUMER_SERVICE_URL', nil) + config.saml[:entity_id] = ENV.fetch('DF_SAML_SP_ENTITY_ID', nil) + config.saml[:idp_sso_target_url] = ENV.fetch('DF_SAML_IDP_TARGET_URL', nil) + config.saml[:idp_sso_signout_url] = ENV.fetch('DF_SAML_IDP_SIGNOUT_URL', nil) + config.saml[:idp_sso_cert] = ENV.fetch('DF_SAML_IDP_CERT', nil) if config.saml[:SAML_metadata_url].nil? + config.saml[:idp_name_identifier_format] = ENV['DF_SAML_IDP_SAML_NAME_IDENTIFIER_FORMAT'] || "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + end + ``` + +- **AAF Authentication**: + ```ruby + if config.auth_method == :aaf + config.aaf = HashWithIndifferentAccess.new + config.aaf[:issuer_url] = ENV['DF_AAF_ISSUER_URL'] || 'https://rapid.test.aaf.edu.au' + config.aaf[:audience_url] = ENV.fetch('DF_AAF_AUDIENCE_URL', nil) + config.aaf[:callback_url] = ENV.fetch('DF_AAF_CALLBACK_URL', nil) + config.aaf[:redirect_url] = ENV.fetch('DF_AAF_UNIQUE_URL', nil) + config.aaf[:identity_provider_url] = ENV.fetch('DF_AAF_IDENTITY_PROVIDER_URL', nil) + config.aaf[:auth_signout_url] = ENV.fetch('DF_AAF_AUTH_SIGNOUT_URL', nil) + end + ``` + +- **Localization**: + ```ruby + config.i18n.enforce_available_locales = true + ``` + +- **Parameter Filtering**: + ```ruby + config.filter_parameters += %i(auth_token password password_confirmation) + ``` + +- **Autoload Paths**: + ```ruby + config.autoload_paths += Dir[Rails.root.join('app')] + config.eager_load_paths += Dir[Rails.root.join('app')] + ``` + +- **CORS Configuration**: + ```ruby + config.middleware.insert_before Warden::Manager, Rack::Cors do + allow do + origins '*' + resource '*', headers: :any, methods: %i(get post put delete options) + end + end + ``` + +## 2. Boot Configuration (`boot.rb`) + +### Purpose +This file sets up the gems listed in the Gemfile and speeds up boot time by caching expensive operations. + +### Key Configurations + +- **Bundler Setup**: + ```ruby + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + ``` + +- **Bootsnap Setup**: + ```ruby + require 'bootsnap/setup' + ``` + +## 3. Database Configuration (`database.yml`) + +### Purpose +This file contains the database connection settings for different environments. + +### Key Configurations + +- **Development Environment**: + ```yaml + development: + adapter: <%= ENV['DF_DEV_DB_ADAPTER'] %> + database: <%= ENV['DF_DEV_DB_DATABASE'] %> + username: <%= ENV['DF_DEV_DB_USERNAME'] %> + password: <%= ENV['DF_DEV_DB_PASSWORD'] %> + host: <%= ENV['DF_DEV_DB_HOST'] %> + min_messages: warning + ``` + +- **Test Environment**: + ```yaml + test: + adapter: <%= ENV['DF_TEST_DB_ADAPTER'] %> + database: <%= ENV['DF_TEST_DB_DATABASE'] %> + username: <%= ENV['DF_TEST_DB_USERNAME'] %> + password: <%= ENV['DF_TEST_DB_PASSWORD'] %> + host: <%= ENV['DF_TEST_DB_HOST'] %> + min_messages: warning + ``` + +- **Staging Environment**: + ```yaml + staging: + adapter: <%= ENV['DF_STAGING_DB_ADAPTER'] %> + host: <%= ENV['DF_STAGING_DB_HOST'] %> + database: <%= ENV['DF_STAGING_DB_DATABASE'] %> + username: <%= ENV['DF_STAGING_DB_USERNAME'] %> + password: <%= ENV['DF_STAGING_DB_PASSWORD'] %> + ``` + +- **Production Environment**: + ```yaml + production: + adapter: <%= ENV['DF_PRODUCTION_DB_ADAPTER'] %> + host: <%= ENV['DF_PRODUCTION_DB_HOST'] %> + database: <%= ENV['DF_PRODUCTION_DB_DATABASE'] %> + username: <%= ENV['DF_PRODUCTION_DB_USERNAME'] %> + password: <%= ENV['DF_PRODUCTION_DB_PASSWORD'] %> + ``` + +## 4. Deakin Institution Settings (`deakin.rb`) + +### Purpose +This file contains custom settings and methods for importing users into units for Deakin University. + +### Key Configurations + +- **Initialization**: + ```ruby + def initialize() + @base_url = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_BASE_URL', nil) + @client_id = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_CLIENT_ID', nil) + @client_secret = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_CLIENT_SECRET', nil) + @star_url = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_STAR_URL', nil) + @star_user = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_STAR_USER', nil) + @star_secret = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_STAR_SECRET', nil) + end + ``` + +- **Methods for User Import**: + ```ruby + def user_import_settings_for(headers) + if are_callista_headers?(headers) + { + missing_headers_lambda: ->(row) { missing_headers(row, ["person id", "surname", "given names", "unit code", "student attempt status", "email", "preferred given name", "campus"]) }, + fetch_row_data_lambda: ->(row, unit) { fetch_callista_row(row, unit) }, + replace_existing_tutorial: false + } + else + { + missing_headers_lambda: ->(row) { missing_headers(row, ["student_code", "first_name", "last_name", "email_address", "preferred_name", "subject_code", "activity_code", "campus", "day_of_week", "start_time", "location", "campus"]) }, + fetch_row_data_lambda: ->(row, unit) { fetch_star_row(row, unit) }, + replace_existing_tutorial: true + } + end + end + ``` + +- **Synchronization Methods**: + ```ruby + def sync_enrolments(unit) + # Implementation for synchronizing enrolments + end + + def fetch_timetable_data(unit) + # Implementation for fetching timetable data + end + ``` + +## 5. Environment Initialization (`environment.rb`) + +### Purpose +This file loads and initializes the Rails application. + +### Key Configurations + +- **Application Initialization**: + ```ruby + require_relative "application" + Doubtfire::Application.initialize! + ``` + +## 6. Institution Configuration (`institution.yml`) + +### Purpose +This file contains institution-specific settings and information. + +### Key Configurations + +- ** + +Institution Details**: + ```yaml + name: Doubtfire University + email_domain: doubtfire.com + host: localhost:3000 + product_name: Doubtfire + settings: no_institution_setting.rb + privacy: Privacy statement text... + plagiarism: Plagiarism policy text... + ``` + +## 7. LDAP Configuration (`ldap.yml`) + +### Purpose +This file contains LDAP server settings for different environments. + +### Key Configurations + +- **Development Environment**: + ```yaml + development: + host: <%= ENV['DF_LDAP_HOST'] %> + port: <%= ENV['DF_LDAP_PORT'] %> + attribute: <%= ENV['DF_LDAP_ATTRIBUTE'] %> + base: <%= ENV['DF_LDAP_BASE'] %> + admin_user: <%= ENV['DF_LDAP_ADMIN_USER'] %> + admin_password: <%= ENV['DF_LDAP_ADMIN_PWD'] %> + ssl: <%= ENV['DF_LDAP_SSL'].to_s.downcase != "false" %> + ``` + +- **Test Environment**: + ```yaml + test: + host: <%= ENV['DF_LDAP_HOST'] %> + port: <%= ENV['DF_LDAP_PORT'] %> + attribute: <%= ENV['DF_LDAP_ATTRIBUTE'] %> + base: <%= ENV['DF_LDAP_BASE'] %> + admin_user: <%= ENV['DF_LDAP_ADMIN_USER'] %> + admin_password: <%= ENV['DF_LDAP_ADMIN_PWD'] %> + ssl: <%= ENV['DF_LDAP_SSL'].to_s.downcase != "false" %> + ``` + +- **Production Environment**: + ```yaml + production: + host: <%= ENV['DF_LDAP_HOST'] %> + port: <%= ENV['DF_LDAP_PORT'] %> + attribute: <%= ENV['DF_LDAP_ATTRIBUTE'] %> + base: <%= ENV['DF_LDAP_BASE'] %> + admin_user: <%= ENV['DF_LDAP_ADMIN_USER'] %> + admin_password: <%= ENV['DF_LDAP_ADMIN_PWD'] %> + ssl: <%= ENV['DF_LDAP_SSL'].to_s.downcase != "false" %> + ``` + +## 8. No Institution Setting (`no_institution_setting.rb`) + +### Purpose +This file contains default institution settings when no specific institution settings are provided. + +### Key Configurations + +- **Default Methods**: + ```ruby + def are_headers_institution_users?(headers) + false + end + + def extract_user_from_row(row) + { unit_code: nil, username: nil, student_id: nil, first_name: nil, last_name: nil, email: nil, tutorials: nil } + end + + def sync_enrolments(unit) + puts 'Unit sync not enabled' + end + + def details_for_next_tutorial_stream(unit, activity_type) + counter = 1 + begin + name = "#{activity_type.name} #{counter}" + abbreviation = "#{activity_type.abbreviation} #{counter}" + counter += 1 + end while unit.tutorial_streams.where("abbreviation = :abbr OR name = :name", abbr: abbreviation, name: name).present? + [name, abbreviation] + end + ``` + +## 9. Puma Configuration (`puma.rb`) + +### Purpose +This file contains configuration settings for the Puma web server. + +### Key Configurations + +- **Thread Configuration**: + ```ruby + max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } + min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } + threads min_threads_count, max_threads_count + ``` + +- **Port Configuration**: + ```ruby + port ENV.fetch("PORT") { 3000 } + ``` + +- **Environment Configuration**: + ```ruby + environment ENV.fetch("RAILS_ENV") { "development" } + ``` + +- **PID File**: + ```ruby + pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + ``` + +- **Plugin**: + ```ruby + plugin :tmp_restart + ``` + +## 10. Routes Configuration (`routes.rb`) + +### Purpose +This file defines the routes for the Rails application. + +### Key Configurations + +- **Devise Routes**: + ```ruby + devise_for :users + ``` + +- **API Routes**: + ```ruby + get 'api/submission/unit/:id/portfolio', to: 'portfolio_downloads#index' + get 'api/submission/unit/:id/task_definitions/:task_def_id/download_submissions', to: 'task_downloads#index' + get 'api/submission/unit/:id/task_definitions/:task_def_id/student_pdfs', to: 'task_submission_pdfs#index' + get 'api/units/:id/all_resources', to: 'lecture_resource_downloads#index' + ``` + +- **Mounting Engines**: + ```ruby + mount ApiRoot => '/' + mount GrapeSwaggerRails::Engine => '/api/docs' + mount Sidekiq::Web => "/sidekiq" + ``` + +## 11. Schedule Configuration (`schedule.rb`) + +### Purpose +This file contains cron job schedules for the application. + +### Key Configurations + +- **Daily Update Task**: + ```ruby + set :output, "#{path}/log/cron.log" + every 1.day, at: '3:00 am' do + rake 'db:update_temporal' + end + ``` + +## 12. Schedule Configuration (`schedule.yml`) + +### Purpose +This file contains job schedules for background tasks. + +### Key Configurations + +- **Register Webhooks**: + ```yaml + register_webhooks: + cron: "every day at 5" + class: "TiiRegisterWebHookJob" + ``` + +- **Progress TurnItIn Jobs**: + ```yaml + progress_turn_it_in_jobs: + cron: "every 30 minutes" + class: "TiiCheckProgressJob" + ``` + +## 13. Storage Configuration (`storage.yml`) + +### Purpose +This file contains configurations for storage services. Note that ActiveStorage is not used in this application. + +### Key Configurations + +- **Sample Storage Configuration**: + ```yaml + # local: + # service: Disk + # root: <%= Rails.root.join("storage") %> + ``` + +- **Amazon S3 Configuration**: + ```yaml + # amazon: + # service: S3 + # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> + # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> + # region: us-east-1 + # bucket: your_own_bucket + ``` + +- **Google Cloud Storage Configuration**: + ```yaml + # google: + # service: GCS + # project: your_project + # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> + # bucket: your_own_bucket + ``` + +# Architecture Diagram of the Doubtfire API + +The architecture diagram of the Doubtfire API illustrates the following components and their interactions: + +## Components + +1. **Client:** + - Represents the user interface, typically a web browser or a mobile app. + - Sends HTTP requests to the Web Server. + +2. **Web Server:** + - Handles incoming HTTP requests from the client. + - Serves static content (HTML, CSS, JavaScript). + - Forwards API requests to the Application Server via REST API. + +3. **Application Server:** + - Processes all business logic and data operations. + - Contains all API endpoints such as user authentication, file downloads, and email handling. + - Interacts with the Database using SQL. + - Communicates with Third-party Services via API. + +4. **Database:** + - Stores application data, including user information, files, and emails. + - The Application Server interacts with the Database through SQL queries. + +5. **Third-party Services:** + - Includes external services used by the application, such as payment gateways and email services (Postmark, SendGrid, Mandrill, Mailgun). + - The Application Server communicates with these services through APIs. + +![figure_1](/diagram.png) From 7ba72ee71e1b1a388f864bb81d97052693eebce5 Mon Sep 17 00:00:00 2001 From: ZHONGYU1111 <2922833288@qq.com> Date: Fri, 17 May 2024 07:54:29 +1000 Subject: [PATCH 2/2] formatted with prettier --- .../Documentation-Backlog-Rails-Backend.md | 374 +++++++++++++----- 1 file changed, 274 insertions(+), 100 deletions(-) diff --git a/src/content/docs/products/ontrack/documentation/Documentation_Backlog_for_rails/Documentation-Backlog-Rails-Backend.md b/src/content/docs/products/ontrack/documentation/Documentation_Backlog_for_rails/Documentation-Backlog-Rails-Backend.md index 3e8e858f..5c82b883 100644 --- a/src/content/docs/products/ontrack/documentation/Documentation_Backlog_for_rails/Documentation-Backlog-Rails-Backend.md +++ b/src/content/docs/products/ontrack/documentation/Documentation_Backlog_for_rails/Documentation-Backlog-Rails-Backend.md @@ -1,17 +1,23 @@ --- -Title: Documentation Backlog - Rails Backend +title: Documentation Backlog - Rails Backend --- # Doubtfire API +## Student Name: Zhongyu Zhang + +## Student ID: 222076406 + ## User Authentication and Management (Devise) ### Sign In + - **Request Method**: GET - **URL**: `/users/sign_in` - **Parameters**: None - **Response Format**: HTML - **Example**: + ```html
@@ -31,6 +37,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: JSON - **Example**: + ```json { "message": "Signed in successfully." @@ -49,11 +56,13 @@ Title: Documentation Backlog - Rails Backend ``` ### Password Management + - **Request Method**: GET - **URL**: `/users/password/new` - **Parameters**: None - **Response Format**: HTML - **Example**: + ```html @@ -65,6 +74,7 @@ Title: Documentation Backlog - Rails Backend - **Parameters**: None - **Response Format**: HTML - **Example**: + ```html @@ -92,11 +102,13 @@ Title: Documentation Backlog - Rails Backend ``` ### User Registration + - **Request Method**: GET - **URL**: `/users/sign_up` - **Parameters**: None - **Response Format**: HTML - **Example**: + ```html @@ -108,6 +120,7 @@ Title: Documentation Backlog - Rails Backend - **Parameters**: None - **Response Format**: HTML - **Example**: + ```html @@ -137,6 +150,7 @@ Title: Documentation Backlog - Rails Backend ## File Downloads ### Download Course Resources + - **Request Method**: GET - **URL**: `/api/submission/unit/:id/portfolio` - **Parameters**: @@ -147,6 +161,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: File - **Example**: + ```json { "file": "portfolio.pdf" @@ -164,6 +179,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: File - **Example**: + ```json { "file": "submissions.zip" @@ -181,6 +197,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: File - **Example**: + ```json { "file": "student_pdfs.zip" @@ -206,6 +223,7 @@ Title: Documentation Backlog - Rails Backend ## API Root and Documentation ### API Root + - **URL**: `/` - **Parameters**: None - **Response Format**: JSON @@ -217,6 +235,7 @@ Title: Documentation Backlog - Rails Backend ``` ### Swagger API Documentation + - **URL**: `/api/docs` - **Parameters**: None - **Response Format**: HTML @@ -232,6 +251,7 @@ Title: Documentation Backlog - Rails Backend ## Sidekiq ### Sidekiq Management Interface + - **URL**: `/sidekiq` - **Parameters**: None - **Response Format**: HTML @@ -247,6 +267,7 @@ Title: Documentation Backlog - Rails Backend ## Action Mailbox ### Email Handling + - **Request Method**: POST - **URL**: `/rails/action_mailbox/postmark/inbound_emails` - **Parameters**: @@ -262,6 +283,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: JSON - **Example**: + ```json { "message": "Email received via Postmark" @@ -283,6 +305,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: JSON - **Example**: + ```json { "message": "Email received via Relay" @@ -304,6 +327,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: JSON - **Example**: + ```json { "message": "Email received via Sendgrid" @@ -315,6 +339,7 @@ Title: Documentation Backlog - Rails Backend - **Parameters**: None - **Response Format**: JSON - **Example**: + ```json { "message": "Mandrill health check passed" @@ -336,6 +361,7 @@ Title: Documentation Backlog - Rails Backend ``` - **Response Format**: JSON - **Example**: + ```json { "message": "Email received via Mandrill" @@ -366,6 +392,7 @@ Title: Documentation Backlog - Rails Backend ## Action Mailbox Conductor ### Conductor Management Interface + - **Request Method**: GET - **URL**: `/rails/conductor/action_mailbox/inbound_emails` - **Parameters**: None @@ -373,30 +400,36 @@ Title: Documentation Backlog - Rails Backend - **Example**: ```json { - "inbound_emails": [ - { - "id": "1", - "to": "example@domain.com", - "from": "sender@domain.com", - "subject": "Hello", - "body": "Email body" - } - ] + "inbound_emails": [ + { + "id": "1", + "to": "example@domain.com", + "from": "sender@domain.com", + "subject": "Hello", + "body": "Email body" + } + ] } ``` # Environment Configurations -This section documents the configurations found in the `config/environments` directory of the Rails backend for Ontrack. Each file configures the application behavior for different environments such as development, production, staging, and testing. +This section documents the configurations found in the `config/environments` directory of the Rails +backend for Ontrack. Each file configures the application behavior for different environments such +as development, production, staging, and testing. ## 1. Development Environment (`development.rb`) ### Purpose -The `development.rb` file contains settings specific to the development environment. It prioritizes ease of development and debugging. + +The `development.rb` file contains settings specific to the development environment. It prioritizes +ease of development and debugging. ### Key Configurations + - **Code Reloading:** - - `config.cache_classes = false`: Disables class caching, allowing code changes to be reflected without restarting the server. + - `config.cache_classes = false`: Disables class caching, allowing code changes to be reflected + without restarting the server. - **Eager Loading:** - `config.eager_load = false`: Disables eager loading of code on boot. - **Error Reporting:** @@ -404,7 +437,8 @@ The `development.rb` file contains settings specific to the development environm - **Caching:** - Conditional caching based on environment variables or presence of a file. - `config.action_controller.perform_caching = true/false`: Enables or disables caching. - - `config.cache_store = :memory_store / :redis_cache_store / :null_store`: Configures the cache store based on environment variables. + - `config.cache_store = :memory_store / :redis_cache_store / :null_store`: Configures the cache + store based on environment variables. - **File Storage:** - `config.active_storage.service = :local`: Stores uploaded files on the local file system. - **Action Mailer:** @@ -413,27 +447,37 @@ The `development.rb` file contains settings specific to the development environm - `config.action_mailer.delivery_method = :file`: Writes emails to file instead of sending them. - **Deprecation Notices:** - `config.active_support.deprecation = :log`: Logs deprecation warnings. - - `config.active_support.disallowed_deprecation = :raise`: Raises exceptions for disallowed deprecations. - - `config.active_support.disallowed_deprecation_warnings = []`: Specifies which deprecation warnings are disallowed. + - `config.active_support.disallowed_deprecation = :raise`: Raises exceptions for disallowed + deprecations. + - `config.active_support.disallowed_deprecation_warnings = []`: Specifies which deprecation + warnings are disallowed. - **Database:** - - `config.active_record.migration_error = :page_load`: Raises an error on page load if there are pending migrations. - - `config.active_record.verbose_query_logs = true`: Highlights code that triggered database queries in logs. + - `config.active_record.migration_error = :page_load`: Raises an error on page load if there are + pending migrations. + - `config.active_record.verbose_query_logs = true`: Highlights code that triggered database + queries in logs. - **File Watcher:** - - `config.file_watcher = ActiveSupport::EventedFileUpdateChecker`: Uses an evented file watcher to asynchronously detect changes. + - `config.file_watcher = ActiveSupport::EventedFileUpdateChecker`: Uses an evented file watcher to + asynchronously detect changes. - **Logging:** - `config.log_level = :debug`: Sets the logging level to debug. - **Miscellaneous:** - - `config.action_dispatch.best_standards_support = :builtin`: Uses best-standards-support built into browsers. + - `config.action_dispatch.best_standards_support = :builtin`: Uses best-standards-support built + into browsers. - `Faker::Config.random = Random.new(77)`: Sets deterministic randomness for Faker. - `config.pdfgen_quiet = false`: Sets the verbosity of pdfgen logs. - - `config.active_record.encryption.key_derivation_salt = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'default_salt'`: Configures ActiveRecord encryption keys. + - `config.active_record.encryption.key_derivation_salt = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'default_salt'`: + Configures ActiveRecord encryption keys. ## 2. Production Environment (`production.rb`) ### Purpose -The `production.rb` file contains settings specific to the production environment, optimized for performance and security. + +The `production.rb` file contains settings specific to the production environment, optimized for +performance and security. ### Key Configurations + - **Code Caching:** - `config.cache_classes = true`: Enables class caching for better performance. - **Caching:** @@ -448,13 +492,16 @@ The `production.rb` file contains settings specific to the production environmen - **I18n:** - `config.i18n.fallbacks = true`: Enables locale fallbacks for I18n. - **Deprecation Notices:** - - `config.active_support.deprecation = :notify`: Sends deprecation notices to registered listeners. + - `config.active_support.deprecation = :notify`: Sends deprecation notices to registered + listeners. - **Middleware:** - - `config.middleware.delete Rack::Runtime`: Removes runtime middleware to harden against timing attacks. + - `config.middleware.delete Rack::Runtime`: Removes runtime middleware to harden against timing + attacks. - **Logging:** - `config.log_level = :info`: Sets the logging level to info. - **Action Mailer:** - - `config.action_mailer.perform_deliveries = (ENV['DF_MAIL_PERFORM_DELIVERIES'] || 'yes') == 'yes'`: Configures whether to perform email deliveries. + - `config.action_mailer.perform_deliveries = (ENV['DF_MAIL_PERFORM_DELIVERIES'] || 'yes') == 'yes'`: + Configures whether to perform email deliveries. - `config.action_mailer.delivery_method = :smtp`: Uses SMTP for email delivery. - Configures SMTP settings based on environment variables. - **ActiveRecord Encryption:** @@ -463,9 +510,12 @@ The `production.rb` file contains settings specific to the production environmen ## 3. Staging Environment (`staging.rb`) ### Purpose -The `staging.rb` file configures settings for the staging environment, which mirrors the production environment with minor changes. + +The `staging.rb` file configures settings for the staging environment, which mirrors the production +environment with minor changes. ### Key Configurations + - **SSL:** - `config.force_ssl = false`: Disables forcing SSL. - **Logging:** @@ -476,9 +526,12 @@ The `staging.rb` file configures settings for the staging environment, which mir ## 4. Test Environment (`test.rb`) ### Purpose -The `test.rb` file contains settings specific to the test environment, optimized for running the application's test suite. + +The `test.rb` file contains settings specific to the test environment, optimized for running the +application's test suite. ### Key Configurations + - **Code Caching:** - `config.cache_classes = true`: Enables class caching for tests. - **Static Files:** @@ -491,9 +544,11 @@ The `test.rb` file contains settings specific to the test environment, optimized - **Caching:** - `config.action_controller.perform_caching = false`: Disables caching. - **Exception Handling:** - - `config.action_dispatch.show_exceptions = false`: Raises exceptions instead of rendering templates. + - `config.action_dispatch.show_exceptions = false`: Raises exceptions instead of rendering + templates. - **Request Forgery Protection:** - - `config.action_controller.allow_forgery_protection = false`: Disables request forgery protection. + - `config.action_controller.allow_forgery_protection = false`: Disables request forgery + protection. - **Action Mailer:** - `config.action_mailer.delivery_method = :test`: Uses the test delivery method for emails. - **Deprecation Notices:** @@ -507,14 +562,18 @@ The `test.rb` file contains settings specific to the test environment, optimized # Initializer Configurations -This section documents the configurations found in the `config/initializers` directory of the Rails backend for Ontrack. Each file configures the application behavior during the initialization phase. +This section documents the configurations found in the `config/initializers` directory of the Rails +backend for Ontrack. Each file configures the application behavior during the initialization phase. ## 1. Backtrace Silencer (`backtrace_silencers.rb`) ### Purpose -This file allows you to add or remove backtrace silencers for libraries you don't wish to see in your backtraces. + +This file allows you to add or remove backtrace silencers for libraries you don't wish to see in +your backtraces. ### Key Configurations + - **Add Silencer**: ```ruby # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } @@ -527,65 +586,79 @@ This file allows you to add or remove backtrace silencers for libraries you don' ## 2. Devise Configuration (`devise.rb`) ### Purpose + This file is used to configure Devise, a flexible authentication solution for Rails. ### Key Configurations + - **Mailer Configuration**: + ```ruby config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' ``` - **ORM Configuration**: + ```ruby require 'devise/orm/active_record' ``` - **Authentication Keys**: + ```ruby config.authentication_keys = [:username] ``` - **Case-Insensitive Keys**: + ```ruby config.case_insensitive_keys = [:email, :username] ``` - **Whitespace Stripped Keys**: + ```ruby config.strip_whitespace_keys = [:email, :username] ``` - **Session Storage**: + ```ruby config.skip_session_storage = [:http_auth] ``` - **Password Length**: + ```ruby config.password_length = 8..128 ``` - **Token Expiry**: + ```ruby config.reset_password_within = 6.hours ``` - **Sign Out Method**: + ```ruby config.sign_out_via = :delete ``` - **Navigational Formats**: + ```ruby config.navigational_formats = ['*/*', :json] ``` - **Secret Key**: + ```ruby config.secret_key = Doubtfire::Application.secrets.secret_key_devise if Rails.env.production? ``` - **LDAP Configuration**: + ```ruby config.ldap_use_admin_to_bind = ENV.fetch('DF_LDAP_USE_ADMIN_TO_BIND', 'false').to_s.downcase != 'false' ``` @@ -599,9 +672,11 @@ This file is used to configure Devise, a flexible authentication solution for Ra ## 3. Inflections (`inflections.rb`) ### Purpose + This file is used to add new inflection rules for pluralization and singularization of words. ### Key Configurations + - **Irregular Inflections**: ```ruby ActiveSupport::Inflector.inflections do |inflect| @@ -612,10 +687,13 @@ This file is used to add new inflection rules for pluralization and singularizat ## 4. Log Initializer (`log_initializer.rb`) ### Purpose + This file configures the logging output and format. ### Key Configurations + - **Log Output to STDOUT**: + ```ruby unless Rails.env.test? Rails.logger.broadcast_to(ActiveSupport::Logger.new($stdout)) @@ -623,6 +701,7 @@ This file configures the logging output and format. ``` - **Custom Log Formatter**: + ```ruby class FormatifFormatter < Logger::Formatter include ActiveSupport::TaggedLogging::Formatter @@ -639,9 +718,11 @@ This file configures the logging output and format. ## 5. MIME Types (`mime_types.rb`) ### Purpose + This file allows you to add new MIME types for use in respond_to blocks. ### Key Configurations + - **Add New MIME Types**: ```ruby # Mime::Type.register "text/richtext", :rtf @@ -651,10 +732,13 @@ This file allows you to add new MIME types for use in respond_to blocks. ## 6. Session Store (`session_store.rb`) ### Purpose + This file configures how session data is stored. ### Key Configurations + - **Cookie Store**: + ```ruby Doubtfire::Application.config.session_store :cookie_store, key: '_doubtfire_session' ``` @@ -667,10 +751,13 @@ This file configures how session data is stored. ## 7. Sidekiq Configuration (`sidekiq.rb`) ### Purpose + This file configures Sidekiq, a background job processing library. ### Key Configurations + - **Server Configuration**: + ```ruby Sidekiq.configure_server do |config| config.redis = { url: ENV.fetch('DF_REDIS_SIDEKIQ_URL', 'redis://localhost:6379/1') } @@ -689,10 +776,13 @@ This file configures Sidekiq, a background job processing library. ## 8. Swagger Configuration (`swagger.rb`) ### Purpose + This file configures Swagger, a tool for documenting APIs. ### Key Configurations + - **Swagger URL and App URL**: + ```ruby GrapeSwaggerRails.options.url = '/api/swagger_doc' GrapeSwaggerRails.options.before_action do @@ -707,10 +797,13 @@ This file configures Swagger, a tool for documenting APIs. ## 9. TurnItIn Initializer (`turn_it_in_initializer.rb`) ### Purpose + This file initializes the TurnItIn API configuration. ### Key Configurations + - **Load Configuration**: + ```ruby require_relative '../../app/helpers/turn_it_in' config = Doubtfire::Application.config @@ -719,6 +812,7 @@ This file initializes the TurnItIn API configuration. ``` - **Background Jobs**: + ```ruby if config.tii_enabled require 'tca_client' @@ -741,10 +835,13 @@ This file initializes the TurnItIn API configuration. ## 10. Wrap Parameters (`wrap_parameters.rb`) ### Purpose + This file configures parameter wrapping for JSON requests. ### Key Configurations + - **Enable Parameter Wrapping for JSON**: + ```ruby ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] @@ -760,16 +857,20 @@ This file configures parameter wrapping for JSON requests. # Locale Configurations -This section documents the configurations found in the `config/locales` directory of the Rails backend for Ontrack. Each file contains translations and localization settings for the application. +This section documents the configurations found in the `config/locales` directory of the Rails +backend for Ontrack. Each file contains translations and localization settings for the application. ## 1. Devise Locale (`devise.en.yml`) ### Purpose -This file contains the English translations for error messages and notifications used by the Devise authentication library. + +This file contains the English translations for error messages and notifications used by the Devise +authentication library. ### Key Configurations - **Error Messages**: + ```yaml en: errors: @@ -788,55 +889,71 @@ This file contains the English translations for error messages and notifications en: devise: failure: - already_authenticated: 'You are already signed in.' - unauthenticated: 'You need to sign in before continuing.' - unconfirmed: 'You have to confirm your account before continuing.' - locked: 'Your account is locked.' - invalid: 'Invalid username or password.' - invalid_token: 'Invalid authentication token.' - timeout: 'Your session expired, please sign in again to continue.' - inactive: 'Your account was not activated yet.' + already_authenticated: "You are already signed in." + unauthenticated: "You need to sign in before continuing." + unconfirmed: "You have to confirm your account before continuing." + locked: "Your account is locked." + invalid: "Invalid username or password." + invalid_token: "Invalid authentication token." + timeout: "Your session expired, please sign in again to continue." + inactive: "Your account was not activated yet." sessions: - signed_in: 'Signed in successfully.' - signed_out: 'Signed out successfully.' + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." passwords: - send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' - updated: 'Your password was changed successfully. You are now signed in.' - updated_not_active: 'Your password was changed successfully.' - send_paranoid_instructions: "If your e-mail exists on our database, you will receive a password recovery link on your e-mail" + send_instructions: + "You will receive an email with instructions about how to reset your password in a few + minutes." + updated: "Your password was changed successfully. You are now signed in." + updated_not_active: "Your password was changed successfully." + send_paranoid_instructions: + "If your e-mail exists on our database, you will receive a password recovery link on your + e-mail" confirmations: - send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' - send_paranoid_instructions: 'If your e-mail exists on our database, you will receive an email with instructions about how to confirm your account in a few minutes.' - confirmed: 'Your account was successfully confirmed. You are now signed in.' + send_instructions: + "You will receive an email with instructions about how to confirm your account in a few + minutes." + send_paranoid_instructions: + "If your e-mail exists on our database, you will receive an email with instructions about + how to confirm your account in a few minutes." + confirmed: "Your account was successfully confirmed. You are now signed in." registrations: - signed_up: 'Welcome! You have signed up successfully.' - inactive_signed_up: 'You have signed up successfully. However, we could not sign you in because your account is %{reason}.' - updated: 'You updated your account successfully.' - destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' + signed_up: "Welcome! You have signed up successfully." + inactive_signed_up: + "You have signed up successfully. However, we could not sign you in because your account + is %{reason}." + updated: "You updated your account successfully." + destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." reasons: - inactive: 'inactive' - unconfirmed: 'unconfirmed' - locked: 'locked' + inactive: "inactive" + unconfirmed: "unconfirmed" + locked: "locked" unlocks: - send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' - unlocked: 'Your account was successfully unlocked. You are now signed in.' - send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.' + send_instructions: + "You will receive an email with instructions about how to unlock your account in a few + minutes." + unlocked: "Your account was successfully unlocked. You are now signed in." + send_paranoid_instructions: + "If your account exists, you will receive an email with instructions about how to unlock + it in a few minutes." omniauth_callbacks: - success: 'Successfully authorized from %{kind} account.' + success: "Successfully authorized from %{kind} account." failure: 'Could not authorize you from %{kind} because "%{reason}".' mailer: confirmation_instructions: - subject: 'Confirmation instructions' + subject: "Confirmation instructions" reset_password_instructions: - subject: 'Reset password instructions' + subject: "Reset password instructions" unlock_instructions: - subject: 'Unlock Instructions' + subject: "Unlock Instructions" ``` ## 2. Bootstrap Locale (`en.bootstrap.yml`) ### Purpose -This file contains English translations for common actions and labels used in the Bootstrap framework. + +This file contains English translations for common actions and labels used in the Bootstrap +framework. ### Key Configurations @@ -862,6 +979,7 @@ This file contains English translations for common actions and labels used in th ## 3. General Locale (`en.yml`) ### Purpose + This file contains general English translations used in the application. ### Key Configurations @@ -875,7 +993,9 @@ This file contains general English translations used in the application. ## 4. Simple Form Locale (`simple_form.en.yml`) ### Purpose -This file contains English translations for the Simple Form gem, which is used to create forms in Rails applications. + +This file contains English translations for the Simple Form gem, which is used to create forms in +Rails applications. ### Key Configurations @@ -883,11 +1003,11 @@ This file contains English translations for the Simple Form gem, which is used t ```yaml en: simple_form: - "yes": 'Yes' - "no": 'No' + "yes": "Yes" + "no": "No" required: - text: 'required' - mark: '*' + text: "required" + mark: "*" error_notification: default_message: "Please review the problems below:" # Labels and hints examples @@ -907,36 +1027,44 @@ This file contains English translations for the Simple Form gem, which is used t # Additional Configurations -This section documents various configuration files found in the `config` directory of the Rails backend for Ontrack. These files include core application settings, database configurations, institution-specific settings, and more. +This section documents various configuration files found in the `config` directory of the Rails +backend for Ontrack. These files include core application settings, database configurations, +institution-specific settings, and more. ## 1. Application Configuration (`application.rb`) ### Purpose + This file contains the core configuration settings for the Rails application. ### Key Configurations - **Load Defaults**: + ```ruby config.load_defaults 7.0 ``` - **Environment Variables**: + ```ruby Dotenv::Railtie.load ``` - **Authentication Method**: + ```ruby config.auth_method = (ENV['DF_AUTH_METHOD'] || :database).to_sym ``` - **Student Work Directory**: + ```ruby config.student_work_dir = ENV['DF_STUDENT_WORK_DIR'] || "#{Rails.root}/student_work" ``` - **Credentials**: + ```ruby credentials.secret_key_base = ENV.fetch('DF_SECRET_KEY_BASE', Rails.env.production? ? nil : 'default_secret_key_base') credentials.secret_key_attr = ENV.fetch('DF_SECRET_KEY_ATTR', Rails.env.production? ? nil : 'default_secret_key_attr') @@ -946,11 +1074,13 @@ This file contains the core configuration settings for the Rails application. ``` - **Institution Settings**: + ```ruby config.institution = YAML.load_file("#{Rails.root}/config/institution.yml").with_indifferent_access ``` - **SAML Authentication**: + ```ruby if config.auth_method == :saml config.saml = HashWithIndifferentAccess.new @@ -965,6 +1095,7 @@ This file contains the core configuration settings for the Rails application. ``` - **AAF Authentication**: + ```ruby if config.auth_method == :aaf config.aaf = HashWithIndifferentAccess.new @@ -978,16 +1109,19 @@ This file contains the core configuration settings for the Rails application. ``` - **Localization**: + ```ruby config.i18n.enforce_available_locales = true ``` - **Parameter Filtering**: + ```ruby config.filter_parameters += %i(auth_token password password_confirmation) ``` - **Autoload Paths**: + ```ruby config.autoload_paths += Dir[Rails.root.join('app')] config.eager_load_paths += Dir[Rails.root.join('app')] @@ -1006,11 +1140,14 @@ This file contains the core configuration settings for the Rails application. ## 2. Boot Configuration (`boot.rb`) ### Purpose -This file sets up the gems listed in the Gemfile and speeds up boot time by caching expensive operations. + +This file sets up the gems listed in the Gemfile and speeds up boot time by caching expensive +operations. ### Key Configurations - **Bundler Setup**: + ```ruby ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) @@ -1024,37 +1161,41 @@ This file sets up the gems listed in the Gemfile and speeds up boot time by cach ## 3. Database Configuration (`database.yml`) ### Purpose + This file contains the database connection settings for different environments. ### Key Configurations - **Development Environment**: + ```yaml development: - adapter: <%= ENV['DF_DEV_DB_ADAPTER'] %> - database: <%= ENV['DF_DEV_DB_DATABASE'] %> - username: <%= ENV['DF_DEV_DB_USERNAME'] %> - password: <%= ENV['DF_DEV_DB_PASSWORD'] %> - host: <%= ENV['DF_DEV_DB_HOST'] %> + adapter: <%= ENV['DF_DEV_DB_ADAPTER'] %> + database: <%= ENV['DF_DEV_DB_DATABASE'] %> + username: <%= ENV['DF_DEV_DB_USERNAME'] %> + password: <%= ENV['DF_DEV_DB_PASSWORD'] %> + host: <%= ENV['DF_DEV_DB_HOST'] %> min_messages: warning ``` - **Test Environment**: + ```yaml test: - adapter: <%= ENV['DF_TEST_DB_ADAPTER'] %> - database: <%= ENV['DF_TEST_DB_DATABASE'] %> - username: <%= ENV['DF_TEST_DB_USERNAME'] %> - password: <%= ENV['DF_TEST_DB_PASSWORD'] %> - host: <%= ENV['DF_TEST_DB_HOST'] %> + adapter: <%= ENV['DF_TEST_DB_ADAPTER'] %> + database: <%= ENV['DF_TEST_DB_DATABASE'] %> + username: <%= ENV['DF_TEST_DB_USERNAME'] %> + password: <%= ENV['DF_TEST_DB_PASSWORD'] %> + host: <%= ENV['DF_TEST_DB_HOST'] %> min_messages: warning ``` - **Staging Environment**: + ```yaml staging: - adapter: <%= ENV['DF_STAGING_DB_ADAPTER'] %> - host: <%= ENV['DF_STAGING_DB_HOST'] %> + adapter: <%= ENV['DF_STAGING_DB_ADAPTER'] %> + host: <%= ENV['DF_STAGING_DB_HOST'] %> database: <%= ENV['DF_STAGING_DB_DATABASE'] %> username: <%= ENV['DF_STAGING_DB_USERNAME'] %> password: <%= ENV['DF_STAGING_DB_PASSWORD'] %> @@ -1063,8 +1204,8 @@ This file contains the database connection settings for different environments. - **Production Environment**: ```yaml production: - adapter: <%= ENV['DF_PRODUCTION_DB_ADAPTER'] %> - host: <%= ENV['DF_PRODUCTION_DB_HOST'] %> + adapter: <%= ENV['DF_PRODUCTION_DB_ADAPTER'] %> + host: <%= ENV['DF_PRODUCTION_DB_HOST'] %> database: <%= ENV['DF_PRODUCTION_DB_DATABASE'] %> username: <%= ENV['DF_PRODUCTION_DB_USERNAME'] %> password: <%= ENV['DF_PRODUCTION_DB_PASSWORD'] %> @@ -1073,11 +1214,13 @@ This file contains the database connection settings for different environments. ## 4. Deakin Institution Settings (`deakin.rb`) ### Purpose + This file contains custom settings and methods for importing users into units for Deakin University. ### Key Configurations - **Initialization**: + ```ruby def initialize() @base_url = ENV.fetch('DF_INSTITUTION_SETTINGS_SYNC_BASE_URL', nil) @@ -1090,6 +1233,7 @@ This file contains custom settings and methods for importing users into units fo ``` - **Methods for User Import**: + ```ruby def user_import_settings_for(headers) if are_callista_headers?(headers) @@ -1109,6 +1253,7 @@ This file contains custom settings and methods for importing users into units fo ``` - **Synchronization Methods**: + ```ruby def sync_enrolments(unit) # Implementation for synchronizing enrolments @@ -1122,6 +1267,7 @@ This file contains custom settings and methods for importing users into units fo ## 5. Environment Initialization (`environment.rb`) ### Purpose + This file loads and initializes the Rails application. ### Key Configurations @@ -1135,31 +1281,35 @@ This file loads and initializes the Rails application. ## 6. Institution Configuration (`institution.yml`) ### Purpose + This file contains institution-specific settings and information. ### Key Configurations -- ** +- \*\* -Institution Details**: - ```yaml - name: Doubtfire University - email_domain: doubtfire.com - host: localhost:3000 - product_name: Doubtfire - settings: no_institution_setting.rb - privacy: Privacy statement text... - plagiarism: Plagiarism policy text... - ``` +Institution Details\*\*: + +```yaml +name: Doubtfire University +email_domain: doubtfire.com +host: localhost:3000 +product_name: Doubtfire +settings: no_institution_setting.rb +privacy: Privacy statement text... +plagiarism: Plagiarism policy text... +``` ## 7. LDAP Configuration (`ldap.yml`) ### Purpose + This file contains LDAP server settings for different environments. ### Key Configurations - **Development Environment**: + ```yaml development: host: <%= ENV['DF_LDAP_HOST'] %> @@ -1172,6 +1322,7 @@ This file contains LDAP server settings for different environments. ``` - **Test Environment**: + ```yaml test: host: <%= ENV['DF_LDAP_HOST'] %> @@ -1198,11 +1349,13 @@ This file contains LDAP server settings for different environments. ## 8. No Institution Setting (`no_institution_setting.rb`) ### Purpose + This file contains default institution settings when no specific institution settings are provided. ### Key Configurations - **Default Methods**: + ```ruby def are_headers_institution_users?(headers) false @@ -1230,11 +1383,13 @@ This file contains default institution settings when no specific institution set ## 9. Puma Configuration (`puma.rb`) ### Purpose + This file contains configuration settings for the Puma web server. ### Key Configurations - **Thread Configuration**: + ```ruby max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } @@ -1242,16 +1397,19 @@ This file contains configuration settings for the Puma web server. ``` - **Port Configuration**: + ```ruby port ENV.fetch("PORT") { 3000 } ``` - **Environment Configuration**: + ```ruby environment ENV.fetch("RAILS_ENV") { "development" } ``` - **PID File**: + ```ruby pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } ``` @@ -1264,16 +1422,19 @@ This file contains configuration settings for the Puma web server. ## 10. Routes Configuration (`routes.rb`) ### Purpose + This file defines the routes for the Rails application. ### Key Configurations - **Devise Routes**: + ```ruby devise_for :users ``` - **API Routes**: + ```ruby get 'api/submission/unit/:id/portfolio', to: 'portfolio_downloads#index' get 'api/submission/unit/:id/task_definitions/:task_def_id/download_submissions', to: 'task_downloads#index' @@ -1291,6 +1452,7 @@ This file defines the routes for the Rails application. ## 11. Schedule Configuration (`schedule.rb`) ### Purpose + This file contains cron job schedules for the application. ### Key Configurations @@ -1306,11 +1468,13 @@ This file contains cron job schedules for the application. ## 12. Schedule Configuration (`schedule.yml`) ### Purpose + This file contains job schedules for background tasks. ### Key Configurations - **Register Webhooks**: + ```yaml register_webhooks: cron: "every day at 5" @@ -1327,11 +1491,14 @@ This file contains job schedules for background tasks. ## 13. Storage Configuration (`storage.yml`) ### Purpose -This file contains configurations for storage services. Note that ActiveStorage is not used in this application. + +This file contains configurations for storage services. Note that ActiveStorage is not used in this +application. ### Key Configurations - **Sample Storage Configuration**: + ```yaml # local: # service: Disk @@ -1339,6 +1506,7 @@ This file contains configurations for storage services. Note that ActiveStorage ``` - **Amazon S3 Configuration**: + ```yaml # amazon: # service: S3 @@ -1359,31 +1527,37 @@ This file contains configurations for storage services. Note that ActiveStorage # Architecture Diagram of the Doubtfire API -The architecture diagram of the Doubtfire API illustrates the following components and their interactions: +The architecture diagram of the Doubtfire API illustrates the following components and their +interactions: ## Components 1. **Client:** + - Represents the user interface, typically a web browser or a mobile app. - Sends HTTP requests to the Web Server. 2. **Web Server:** + - Handles incoming HTTP requests from the client. - Serves static content (HTML, CSS, JavaScript). - Forwards API requests to the Application Server via REST API. 3. **Application Server:** + - Processes all business logic and data operations. - Contains all API endpoints such as user authentication, file downloads, and email handling. - Interacts with the Database using SQL. - Communicates with Third-party Services via API. 4. **Database:** + - Stores application data, including user information, files, and emails. - The Application Server interacts with the Database through SQL queries. 5. **Third-party Services:** - - Includes external services used by the application, such as payment gateways and email services (Postmark, SendGrid, Mandrill, Mailgun). + - Includes external services used by the application, such as payment gateways and email services + (Postmark, SendGrid, Mandrill, Mailgun). - The Application Server communicates with these services through APIs. ![figure_1](/diagram.png)