diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..31eeee0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..560212b --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,30 @@ +name: Linters + +on: pull_request + +env: + FORCE_COLOR: 1 + +jobs: + rubocop: + name: Rubocop + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-ruby@v1 + with: + ruby-version: 3.1.x + - name: Setup Rubocop + run: | + gem install --no-document rubocop -v '>= 1.0, < 2.0' # https://docs.rubocop.org/en/stable/installation/ + [ -f .rubocop.yml ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/ror/.rubocop.yml + - name: Rubocop Report + run: rubocop --color + nodechecker: + name: node_modules checker + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Check node_modules existence + run: | + if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8838121 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..07baeb4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,60 @@ +AllCops: + NewCops: enable + Exclude: + - "db/**/*" + - "bin/*" + - "config/**/*" + - "Guardfile" + - "Rakefile" + - "node_modules/**/*" + + DisplayCopNames: true + +Layout/LineLength: + Max: 120 +Metrics/MethodLength: + Include: + - "app/controllers/*" + - "app/models/*" + Max: 20 +Metrics/AbcSize: + Include: + - "app/controllers/*" + - "app/models/*" + Max: 50 +Metrics/ClassLength: + Max: 150 +Metrics/BlockLength: + AllowedMethods: ['describe'] + Max: 30 + +Style/Documentation: + Enabled: false +Style/ClassAndModuleChildren: + Enabled: false +Style/EachForSimpleLoop: + Enabled: false +Style/AndOr: + Enabled: false +Style/DefWithParentheses: + Enabled: false +Style/FrozenStringLiteralComment: + EnforcedStyle: never + +Layout/HashAlignment: + EnforcedColonStyle: key +Layout/ExtraSpacing: + AllowForAlignment: false +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented +Lint/RaiseException: + Enabled: false +Lint/StructNewOverride: + Enabled: false +Style/HashEachMethods: + Enabled: false +Style/HashTransformKeys: + Enabled: false +Style/HashTransformValues: + Enabled: false \ No newline at end of file diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..be94e6f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.2 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..426f149 --- /dev/null +++ b/Gemfile @@ -0,0 +1,60 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '3.2.2' + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem 'rails', '~> 7.0.7' + +# Use postgresql as the database for Active Record +gem 'pg', '~> 1.1' + +# Use the Puma web server [https://github.com/puma/puma] +gem 'puma', '~> 5.0' + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +# gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +# gem "redis", "~> 4.0" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] + +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible +gem 'rack-cors' + +group :development, :test do + gem 'rspec-rails' + + gem 'factory_bot_rails' + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem 'debug', platforms: %i[mri mingw x64_mingw] +end + +group :development do + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +gem 'rubocop', '~> 1.56' + +gem 'devise' + +gem 'devise-jwt' + +gem 'jsonapi-serializer' + +gem 'rswag' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..0a7a78e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,291 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.7) + actionpack (= 7.0.7) + activesupport (= 7.0.7) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.7) + actionpack (= 7.0.7) + activejob (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.7) + actionpack (= 7.0.7) + actionview (= 7.0.7) + activejob (= 7.0.7) + activesupport (= 7.0.7) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.7) + actionview (= 7.0.7) + activesupport (= 7.0.7) + rack (~> 2.0, >= 2.2.4) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.7) + actionpack (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.7) + activesupport (= 7.0.7) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.7) + activesupport (= 7.0.7) + globalid (>= 0.3.6) + activemodel (7.0.7) + activesupport (= 7.0.7) + activerecord (7.0.7) + activemodel (= 7.0.7) + activesupport (= 7.0.7) + activestorage (7.0.7) + actionpack (= 7.0.7) + activejob (= 7.0.7) + activerecord (= 7.0.7) + activesupport (= 7.0.7) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.7) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + base64 (0.1.1) + bcrypt (3.1.19) + bootsnap (1.16.0) + msgpack (~> 1.2) + builder (3.2.4) + concurrent-ruby (1.2.2) + crass (1.0.6) + date (3.3.3) + debug (1.8.0) + irb (>= 1.5.0) + reline (>= 0.3.1) + devise (4.9.2) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + devise-jwt (0.11.0) + devise (~> 4.0) + warden-jwt_auth (~> 0.8) + diff-lcs (1.5.0) + dry-auto_inject (1.0.1) + dry-core (~> 1.0) + zeitwerk (~> 2.6) + dry-configurable (1.1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + erubi (1.12.0) + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) + globalid (1.1.0) + activesupport (>= 5.0) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + io-console (0.6.0) + irb (1.7.4) + reline (>= 0.3.6) + json (2.6.3) + json-schema (3.0.0) + addressable (>= 2.8) + jsonapi-serializer (2.2.0) + activesupport (>= 4.2) + jwt (2.7.1) + language_server-protocol (3.17.0.3) + loofah (2.21.3) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + method_source (1.0.0) + mini_mime (1.1.5) + minitest (5.19.0) + msgpack (1.7.2) + net-imap (0.3.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol + nio4r (2.5.9) + nokogiri (1.15.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.15.4-x64-mingw-ucrt) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (1.23.0) + parser (3.2.2.3) + ast (~> 2.4.1) + racc + pg (1.5.3) + pg (1.5.3-x64-mingw-ucrt) + public_suffix (5.0.3) + puma (5.6.6) + nio4r (~> 2.0) + racc (1.7.1) + rack (2.2.8) + rack-cors (2.0.1) + rack (>= 2.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rails (7.0.7) + actioncable (= 7.0.7) + actionmailbox (= 7.0.7) + actionmailer (= 7.0.7) + actionpack (= 7.0.7) + actiontext (= 7.0.7) + actionview (= 7.0.7) + activejob (= 7.0.7) + activemodel (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) + bundler (>= 1.15.0) + railties (= 7.0.7) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.0.7) + actionpack (= 7.0.7) + activesupport (= 7.0.7) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rainbow (3.1.1) + rake (13.0.6) + regexp_parser (2.8.1) + reline (0.3.7) + io-console (~> 0.5) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.6) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (6.0.3) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) + rswag (2.10.1) + rswag-api (= 2.10.1) + rswag-specs (= 2.10.1) + rswag-ui (= 2.10.1) + rswag-api (2.10.1) + railties (>= 3.1, < 7.1) + rswag-specs (2.10.1) + activesupport (>= 3.1, < 7.1) + json-schema (>= 2.2, < 4.0) + railties (>= 3.1, < 7.1) + rspec-core (>= 2.14) + rswag-ui (2.10.1) + actionpack (>= 3.1, < 7.1) + railties (>= 3.1, < 7.1) + rubocop (1.56.0) + base64 (~> 0.1.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.3) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + thor (1.2.2) + timeout (0.4.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2023.3) + tzinfo (>= 1.0.0) + unicode-display_width (2.4.2) + warden (1.2.9) + rack (>= 2.0.9) + warden-jwt_auth (0.8.0) + dry-auto_inject (>= 0.8, < 2) + dry-configurable (>= 0.13, < 2) + jwt (~> 2.1) + warden (~> 1.2) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.6.11) + +PLATFORMS + arm64-darwin-22 + x64-mingw-ucrt + +DEPENDENCIES + bootsnap + debug + devise + devise-jwt + factory_bot_rails + jsonapi-serializer + pg (~> 1.1) + puma (~> 5.0) + rack-cors + rails (~> 7.0.7) + rspec-rails + rswag + rubocop (~> 1.56) + tzinfo-data + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.4.16 diff --git a/README.md b/README.md new file mode 100644 index 0000000..be0a1c7 --- /dev/null +++ b/README.md @@ -0,0 +1,228 @@ + + +
+ logo +
+ + + +
+ + +
+ 📗 Table of Contents + +- [📖 About the Project](#about-project) + - [🛠 Built With](#built-with) + - [Tech Stack](#tech-stack) + - [Key Features](#key-features) + - [🚀 Link to frontend app](#live-demo) +- [💻 Getting Started](#getting-started) + - [Setup](#setup) + - [Prerequisites](#prerequisites) + - [Install](#install) + - [Usage](#usage) + - [Run tests](#run-tests) +- [👥 Authors](#authors) +- [🔭 Future Features](#future-features) +- [🤝 Contributing](#contributing) +- [⭐️ Show your support](#support) +- [🙏 Acknowledgements](#acknowledgements) + +- [📝 License](#license) +
+ + +# 📚💻🌐 VECS SCHOOL BACKEND + +Welcome to the backend repository of VECS School's course booking system. This API-only project, built with Ruby on Rails, powers the seamless booking of courses by enabling users to specify their city and preferred dates. It offers a comprehensive set of endpoints, ensuring efficient communication between frontend applications and the backend server. +## 🛠 Built With + +### Tech Stack + +
+ Backend + +
+ + +
+Database + +
+ +
+ Authentication + +
+ +
+ Documentation + +
+ + +### Key Features + +- **Course Booking:** Users can browse available courses, specify their city, and choose preferred dates for course attendance.. + +- **Endpoint Documentation:** Comprehensive API documentation is provided to guide frontend developers on how to interact with the backend. + +- **User Authentication:** Secure user authentication and authorization mechanisms are implemented to protect sensitive user data and interactions. + +- **Scalability:** Built with scalability in mind, the backend can handle a growing number of users and course offerings. + +

(back to top)

+ + + +## 🚀 Link to Frontend + +- Check out VECS SCHOOL frontend app [here.](https://github.com/smunoz1988/vecs-school-frontend) + +

(back to top)

+ + + +## 💻 Getting Started + +To get a local copy up and running, follow these steps. + +### Prerequisites + +Ensure you have Ruby installed on your system. You can check by running the code below in your terminal. +```sh + ruby --version +``` +### Setup + +Clone or download the VECS School repository to your local machine. +```sh + git clone https://github.com/christianonoh/vecs-school-backend.git +``` +Open your terminal and navigate to the cloned project directory. + +### Install + +This project does not require additional dependencies, just Ruby interpreter. However, it uses Rubocop as a linter and you should set it up as well. + +```sh +bundle install +``` +**Credentials Setup:** Generate the master key. + +- Delete the `config/credentials.yml.enc` file and run: + +```sh +EDITOR="code --wait" rails credentials:edit +``` +- This will generate new `config/master.key` and `config/credentials.yml.enc` files. + +**Database Setup:** Navigate to the project directory and set up the database. +- In case it's necessary, add your database credentials to the `config/database.yml`. +- Run the following commands to create a database and run database migrations +```sh +rails db:create +rails db:migrate +rails db:seed +``` +### Usage +Start the Rails server to run the application locally. + +```sh + rails server +``` +Open your web browser and go to `http://localhost:3000/api/v1/` and add an endpoint to access your desired data. + +## Running Tests + +VECS School uses RSpec for testing. To run the tests, follow these steps: + +- **Navigate to Project Directory:** Make sure you are in the project directory (`vecs-school-backend`). + +- **Run Tests:** Use the following command to run the tests. +```sh + bundle exec rspec spec/models + bundle exec rspec spec/controllers +``` +- This will execute all the tests for the models and controllers directory and provide you with the results. + +

(back to top)

+ + + +## 👥 Authors + +👤 **Christian Onoh** + +- GitHub: [@christianonoh](https://github.com/christianonoh) +- Twitter: [@onohchristian](https://twitter.com/onohchristian) +- LinkedIn: [Christian Onoh](https://www.linkedin.com/in/christianonoh) + +👤 **Vitoesi Elijah** + +- GitHub: [@Ellyboi](https://github.com/Ellyboi) +- Twitter: [@elijahvitoesi](https://twitter.com/elijahvitoesi) +- LinkedIn: [@Elijah](https://www.linkedin.com/in/vitoesi-elijah-61961611a/) + +👤 **Vanessa Oliveros Padron** + +- GitHub: [@vvoo21](https://github.com/vvoo21) +- Twitter: [@vaneoliverosp](https://twitter.com/vaneoliverosp) +- LinkedIn: [vaneoliverosp](https://www.linkedin.com/in/vaneoliverosp/) + +👤 **Santiago Munoz** + +- GitHub: [@smunoz](https://github.com/smunoz1988) +- Twitter: [@twitterhandle](https://twitter.com/Santiag24209785) +- LinkedIn: [LinkedIn](https://www.linkedin.com/in/santiago-munoz-0b2b1a260) + +

(back to top)

+ + + +## 🔭 Future Features + +- [ ] **City and Date Filtering:** Courses can be filtered based on city and available dates, ensuring users find courses that match their preferences. +- [ ] **Feedback Mechanism:** Users can provide feedback on courses they've attended, helping improve the overall course quality. +

(back to top)

+ + + +## 🤝 Contributing + +We're committed to continuously improving VECS School. If you encounter any issues or have suggestions for improvement, please [submit an issue](https://github.com/christianonoh/vecs-school-backend/issues). + +

(back to top)

+ + + +## ⭐️ Show your support + +If you like this project please give it a star ⭐️. Thanks for your support! + +

(back to top)

+ + + +## 🙏 Acknowledgments +Thanks to all the VECS School contributors/team. + +

(back to top)

+ + + + +## 📝 License + +This project is [MIT](./LICENSE) licensed. + +

(back to top)

diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/api/v1/courses_controller.rb b/app/controllers/api/v1/courses_controller.rb new file mode 100644 index 0000000..d64dfe7 --- /dev/null +++ b/app/controllers/api/v1/courses_controller.rb @@ -0,0 +1,56 @@ +class Api::V1::CoursesController < ApplicationController + before_action :set_course, only: %i[show update destroy] + before_action :authenticate_user! + + def index + courses = Course.all.order(name: :asc) + render json: courses, each_serializer: CourseSerializer, status: :ok + end + + # GET /api/v1/courses/1 + def show + render json: @course, serializer: CourseSerializer, status: :ok + end + + # POST /api/v1/courses + def create + @course = Course.new(course_params) + + if @course.save + render json: @course, serializer: CourseSerializer, status: :created + else + render json: { errors: @course.errors.full_messages }, status: :unprocessable_entity + end + end + + # PATCH/PUT /api/v1/courses/1 + def update + if @course.update(course_params) + render json: @course, serializer: CourseSerializer, status: :ok + else + render json: { errors: @course.errors.full_messages }, status: :unprocessable_entity + end + end + + # DELETE /api/v1/courses/1 + def destroy + if @course.destroy + render json: { message: 'Course deleted' }, status: :ok + else + render json: { errors: @course.errors.full_messages }, status: :unprocessable_entity + end + end + + private + + def set_course + @course = Course.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render json: { error: 'Course not found' }, status: :not_found + end + + # Only allow a list of trusted parameters through. + def course_params + params.require(:course).permit(%i[name description photo price teacher]) + end +end diff --git a/app/controllers/api/v1/current_user_controller.rb b/app/controllers/api/v1/current_user_controller.rb new file mode 100644 index 0000000..27e89d4 --- /dev/null +++ b/app/controllers/api/v1/current_user_controller.rb @@ -0,0 +1,7 @@ +class Api::V1::CurrentUserController < ApplicationController + before_action :authenticate_user! + + def index + render json: UserSerializer.new(current_user).serializable_hash[:data][:attributes], status: :ok + end +end diff --git a/app/controllers/api/v1/reservations_controller.rb b/app/controllers/api/v1/reservations_controller.rb new file mode 100644 index 0000000..fc3dfea --- /dev/null +++ b/app/controllers/api/v1/reservations_controller.rb @@ -0,0 +1,57 @@ +class Api::V1::ReservationsController < ApplicationController + before_action :set_reservation, only: %i[show update destroy] + before_action :authenticate_user! + + # GET /api/v1/reservations + def index + reservations = Reservation.where(user_id: current_user.id) + render json: reservations, status: 200 + end + + # GET /api/v1/reservations/1 + def show + render json: @reservation + end + + # POST /api/v1/reservations + def create + @reservation = Reservation.new(reservation_params) + + if @reservation.save + render json: @reservation, status: :created + else + render json: { errors: @reservation.errors.full_messages }, status: :unprocessable_entity + end + end + + # PATCH/PUT /api/v1/reservations/1 + def update + if @reservation.update(reservation_params) + render json: @reservation + else + render json: { errors: @reservation.errors.full_messages }, status: :unprocessable_entity + end + end + + # DELETE /api/v1/reservations/1 + def destroy + if @reservation.destroy + render json: { message: 'Reservation deleted' }, status: :ok + else + render json: { errors: @reservation.errors.full_messages }, status: :unprocessable_entity + end + end + + private + + def set_reservation + @reservation = Reservation.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render json: { error: 'Reservation not found' }, status: :not_found + end + + # Only allow a list of trusted parameters through. + def reservation_params + params.require(:reservation).permit(%i[user_id course_id date city]) + end +end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb new file mode 100644 index 0000000..a27eceb --- /dev/null +++ b/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,56 @@ +class Api::V1::UsersController < ApplicationController + before_action :set_user, only: %i[show update destroy] + + # GET /api/v1/users + def index + @users = User.all + render json: UserSerializer.new(@users).serializable_hash[:data].map { |entry| entry[:attributes] }, status: :ok + end + + # GET /api/v1/users/1 + def show + render json: @user, serializer: UserSerializer, status: :ok + end + + # POST /api/v1/users + def create + @user = User.new(user_params) + + if @user.save + render json: @user, serializer: UserSerializer, status: :created + else + render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity + end + end + + # PATCH/PUT /api/v1/users/1 + def update + if @user.update(user_params) + render json: @user, serializer: UserSerializer, status: :ok + else + render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity + end + end + + # DELETE /api/v1/users/1 + def destroy + if @user.destroy + render json: { message: 'User deleted' }, status: :ok + else + render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity + end + end + + private + + def set_user + @user = User.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render json: { error: 'User not found' }, status: :not_found + end + + # Only allow a list of trusted parameters through. + def user_params + params.require(:user).permit(%i[name email password password_confirmation]) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..9845b10 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,10 @@ +class ApplicationController < ActionController::API + before_action :configure_permitted_parameters, if: :devise_controller? + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: %i[name]) + devise_parameter_sanitizer.permit(:account_update, keys: %i[name]) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/concerns/rack_sessions_fix.rb b/app/controllers/concerns/rack_sessions_fix.rb new file mode 100644 index 0000000..985359b --- /dev/null +++ b/app/controllers/concerns/rack_sessions_fix.rb @@ -0,0 +1,19 @@ +module RackSessionsFix + extend ActiveSupport::Concern + class FakeRackSession < Hash + def enabled? + false + end + + def destroy; end + end + included do + before_action :set_fake_session + + private + + def set_fake_session + request.env['rack.session'] ||= FakeRackSession.new + end + end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 0000000..49b238c --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,19 @@ +class Users::RegistrationsController < Devise::RegistrationsController + include RackSessionsFix + respond_to :json + + private + + def respond_with(current_user, _opts = {}) + if resource.persisted? + render json: { + status: { code: 200, message: 'Signed up successfully.' }, + data: UserSerializer.new(current_user).serializable_hash[:data][:attributes] + } + else + render json: { + status: { message: "User couldn't be created successfully. #{current_user.errors.full_messages.to_sentence}" } + }, status: :unprocessable_entity + end + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 0000000..94c328b --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,27 @@ +class Users::SessionsController < Devise::SessionsController + include RackSessionsFix + respond_to :json + + private + + def respond_with(resource, _opts = {}) + render json: { + status: { code: 200, message: 'Logged in sucessfully.' }, + data: UserSerializer.new(resource).serializable_hash[:data][:attributes] + }, status: :ok + end + + def respond_to_on_destroy + if current_user + render json: { + status: 200, + message: 'Logged out successfully' + }, status: :ok + else + render json: { + status: 401, + message: "Couldn't find an active session." + }, status: :unauthorized + end + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..286b223 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/course.rb b/app/models/course.rb new file mode 100644 index 0000000..932d004 --- /dev/null +++ b/app/models/course.rb @@ -0,0 +1,8 @@ +class Course < ApplicationRecord + has_many :reservations, dependent: :destroy + + validates :name, :description, :photo, :price, :teacher, presence: true + validates :name, :teacher, length: { minimum: 2, maximum: 25 } + validates :description, length: { minimum: 10, maximum: 500 } + validates :price, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100_000 } +end diff --git a/app/models/reservation.rb b/app/models/reservation.rb new file mode 100644 index 0000000..08becb1 --- /dev/null +++ b/app/models/reservation.rb @@ -0,0 +1,10 @@ +class Reservation < ApplicationRecord + belongs_to :user + belongs_to :course + + validates :date, :city, :user_id, :course_id, presence: true + + def as_json(options = {}) + super(options.merge(include: :course)) + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..8906404 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,11 @@ +class User < ApplicationRecord + include Devise::JWT::RevocationStrategies::JTIMatcher + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable, + :jwt_authenticatable, jwt_revocation_strategy: self + + has_many :reservations, dependent: :destroy + has_many :courses, through: :reservations + + validates :name, :email, presence: true +end diff --git a/app/serializers/course_serializer.rb b/app/serializers/course_serializer.rb new file mode 100644 index 0000000..7966fa1 --- /dev/null +++ b/app/serializers/course_serializer.rb @@ -0,0 +1,5 @@ +class CourseSerializer + include JSONAPI::Serializer + + attributes :name, :description, :photo, :price, :teacher +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 0000000..54ba592 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,4 @@ +class UserSerializer + include JSONAPI::Serializer + attributes :id, :name, :email +end diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..42c7fd7 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..ec47b79 --- /dev/null +++ b/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..ad1fbf2 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..07cd3aa --- /dev/null +++ b/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module VecsSchoolBackend + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.0 + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Only loads a smaller set of middleware suitable for API only apps. + # Middleware like session, flash, cookies can be added back manually. + # Skip views, helpers and assets when generating a new resource. + config.api_only = true + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..5d041d0 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: vecs_school_backend_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..2d9f800 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +f1h69qUE/zsA8ztmn6yZvKTJauk3YqP+QIn27fO1PvfVv7T4Chbg0K+BL75dHkO9e7/qaL4yaNKurGGZ5hqa+weYQqcS7a6LmkuXLBiZcrnOgEjygGfUQh7Y2whCRe29QP7GSFS5zj3mrPmb/aE7JbB91bBGg2dW4oOjEqfs5C2KuBd4atblAMQVXvmXZxeG3MfDC0iJGxSVsqYUCFjrWrUsKkbzUxKowfjbWIKBWG8OWAX4olQaDKmS/pfoqzIls+DX5E87NRTAIdKrCF084GBsJrbL6N5jV37mlye2r0+0fWiR3HFh6HyLDhFJDTSEE46QNV6ZoO7NtZyhK2Kp6EsYwJdKE27yGRRsInye810r/ErRUgOcodfOuGzAjQQqvoDDWigDpbG5fGMOEnn+3Y9AdJVoV11dGusm--NCldQ4HhNFt5MR+o--qkVRh0zR3hkYTfUAIPfLnA== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..7eb6ece --- /dev/null +++ b/config/database.yml @@ -0,0 +1,11 @@ +default: &default + adapter: postgresql + encoding: unicode + +development: + <<: *default + database: vecs_school_backend_development + +test: + <<: *default + database: vecs_school_backend_test diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..b0e19e3 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,67 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..819ecff --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,86 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "vecs_school_backend_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..6ea4d1e --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,60 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Turn false under Spring and add config.action_view.cache_template_loading = true. + config.cache_classes = true + + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..65ee246 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,18 @@ +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. + +# Read more: https://github.com/cyu/rack-cors + +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins 'http://localhost:5173' # or 'http://localhost:3000' for consistency + resource '*', + headers: :any, + methods: [:get, :post, :put, :patch, :delete, :options, :head], + credentials: true, + expose: [:Authorization] + end +end + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..d636d1c --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,324 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '273154f37797e963464e7e1a2284fe8639dcf10ae09dad672a7e0940a8a9955f5d5c277e984e8a4955e00cd64e40bfeb3b0fc24630e22abb8c2cc637d9a8ad3a' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '7448f493da824ef6c396a0b7bcef4fe6a347fedee0faf3417927386b6436c41c72afce0e6cd2c76d04c4014f30dbe1a0021d511f3d5e2afea1503bae17ce867b' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + config.navigational_formats = [] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found respectively`, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true + + config.jwt do |jwt| + jwt.secret = Rails.application.credentials.fetch(:secret_key_base) + jwt.dispatch_requests = [ + ['POST', %r{^/login$}] + ] + jwt.revocation_requests = [ + ['DELETE', %r{^/logout$}] + ] + jwt.expiration_time = 720.minutes.to_i + end +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..adc6568 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 0000000..4d72f68 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,14 @@ +Rswag::Api.configure do |c| + + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.swagger_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + #c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb new file mode 100644 index 0000000..0a768c1 --- /dev/null +++ b/config/initializers/rswag_ui.rb @@ -0,0 +1,16 @@ +Rswag::Ui.configure do |c| + + # List the Swagger endpoints that you want to be documented through the + # swagger-ui. The first parameter is the path (absolute or relative to the UI + # host) to the corresponding endpoint and the second is a title that will be + # displayed in the document selector. + # NOTE: If you're using rspec-api to expose Swagger files + # (under swagger_root) as JSON or YAML endpoints, then the list below should + # correspond to the relative paths for those endpoints. + + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 0000000..260e1c4 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + 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:" diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..8ca56fc --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..daaf036 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +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 + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..74cac5b --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,23 @@ +Rails.application.routes.draw do + root to: redirect('/api-docs') + mount Rswag::Ui::Engine => '/api-docs' + mount Rswag::Api::Engine => '/api-docs' + namespace :api do + namespace :v1 do + get 'current_user/', to: 'current_user#index' + resources :users, only: [:index] + resources :courses + resources :reservations + end + end + + devise_for :users, path: '', path_names: { + sign_in: 'login', + sign_out: 'logout', + registration: 'signup' + }, + controllers: { + sessions: 'users/sessions', + registrations: 'users/registrations' + } +end \ No newline at end of file diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# 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-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20230817100832_create_users.rb b/db/migrate/20230817100832_create_users.rb new file mode 100644 index 0000000..d5710a6 --- /dev/null +++ b/db/migrate/20230817100832_create_users.rb @@ -0,0 +1,9 @@ +class CreateUsers < ActiveRecord::Migration[7.0] + def change + create_table :users do |t| + t.string :name, :null => false + + t.timestamps + end + end +end diff --git a/db/migrate/20230817121922_create_courses.rb b/db/migrate/20230817121922_create_courses.rb new file mode 100644 index 0000000..14cdd9f --- /dev/null +++ b/db/migrate/20230817121922_create_courses.rb @@ -0,0 +1,14 @@ +class CreateCourses < ActiveRecord::Migration[7.0] + def change + create_table :courses do |t| + t.string :name + t.string :description + t.string :photo + t.float :price + t.string :teacher + t.references :user, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20230817181333_create_reservations.rb b/db/migrate/20230817181333_create_reservations.rb new file mode 100644 index 0000000..ab53e1d --- /dev/null +++ b/db/migrate/20230817181333_create_reservations.rb @@ -0,0 +1,12 @@ +class CreateReservations < ActiveRecord::Migration[7.0] + def change + create_table :reservations do |t| + t.references :user, foreign_key: true + t.references :course, foreign_key: true + t.string :city + t.datetime :date + + t.timestamps + end + end +end diff --git a/db/migrate/20230821222957_add_devise_to_users.rb b/db/migrate/20230821222957_add_devise_to_users.rb new file mode 100644 index 0000000..98ed9dc --- /dev/null +++ b/db/migrate/20230821222957_add_devise_to_users.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class AddDeviseToUsers < ActiveRecord::Migration[7.0] + def self.up + change_table :users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + # Uncomment below if timestamps were not included in your original model. + # t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end + + def self.down + # By default, we don't want to make any assumption about how to roll back a migration when your + # model already existed. Please edit below which fields you would like to remove in this migration. + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20230821233617_add_jti_to_users.rb b/db/migrate/20230821233617_add_jti_to_users.rb new file mode 100644 index 0000000..cf1f29f --- /dev/null +++ b/db/migrate/20230821233617_add_jti_to_users.rb @@ -0,0 +1,6 @@ +class AddJtiToUsers < ActiveRecord::Migration[7.0] + def change + add_column :users, :jti, :string, null: false + add_index :users, :jti, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..ade0bbc --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,58 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.0].define(version: 2023_08_21_233617) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "courses", force: :cascade do |t| + t.string "name" + t.string "description" + t.string "photo" + t.float "price" + t.string "teacher" + t.bigint "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_courses_on_user_id" + end + + create_table "reservations", force: :cascade do |t| + t.bigint "user_id" + t.bigint "course_id" + t.string "city" + t.datetime "date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["course_id"], name: "index_reservations_on_course_id" + t.index ["user_id"], name: "index_reservations_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.string "jti", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["jti"], name: "index_users_on_jti", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + end + + add_foreign_key "courses", "users" + add_foreign_key "reservations", "courses" + add_foreign_key "reservations", "users" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..09d439a --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,30 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) +# Character.create(name: "Luke", movie: movies.first) +User.create(name: "Usom", email: "usom@gmail.com", password: "123456") + +ActiveRecord::Base.transaction do + Course.create(name: "Ruby on Rails", description: "Ruby on Rails, often referred to simply as Rails, is an open-source web application framework written in the Ruby programming language. + Rails is designed to make it easier to build dynamic and robust web applications by providing a structured and organized development environment. + It follows the Model-View-Controller (MVC) architectural pattern, which separates the application's concerns into three distinct components", + price: 20.00, teacher: "Burger", photo:"https://tinyurl.com/5e4tdnyv") + + Course.create(name: "Python for Data Science", description: "Learn how to leverage the power of Python for data analysis and visualization. Explore libraries like NumPy, Pandas, and Matplotlib to process and visualize data effectively.", + price: 25.00, teacher: "Smith", photo: "https://tinyurl.com/3u9cw3da") + + Course.create(name: "Full Stack Web Dev", description: "Become a full stack web developer by mastering both front-end and back-end technologies. Learn HTML, CSS, JavaScript, Node.js, and databases to build complete web applications.", + price: 30.00, teacher: "Johnson", photo: "https://tinyurl.com/wvxcstn6") + + Course.create(name: "Machine Learning Fundamentals", description: "Dive into the world of machine learning. Understand the basics of supervised and unsupervised learning, and get hands-on experience with popular machine learning frameworks.", + price: 40.00, teacher: "Garcia", photo: "https://tinyurl.com/y4brxtc8") + + Course.create(name: "Mobile App Dev(Flutter)", description: "Create cross-platform mobile apps using Flutter framework. Build native-like apps for iOS and Android with a single codebase, and explore widgets, animations, and APIs.", + price: 28.00, teacher: "Lee", photo: "https://tinyurl.com/399e9w5e") + + Course.create(name: "Intro to Blockchain Tech", description: "Discover the fundamentals of blockchain technology and cryptocurrencies. Learn about decentralized ledgers, smart contracts, and how blockchain is transforming industries.", + price: 22.00, teacher: "Brown", photo: "https://tinyurl.com/4t9vf7v3") +end diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..7012ed6 Binary files /dev/null and b/logo.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/spec/controllers/course_spec.rb b/spec/controllers/course_spec.rb new file mode 100644 index 0000000..ccc0a8a --- /dev/null +++ b/spec/controllers/course_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +RSpec.describe Api::V1::CoursesController, type: :request do + include Devise::Test::IntegrationHelpers + before(:each) do + @user = create(:user) + login_as(@user, scope: :user) + end + + describe 'GET /api/v1/courses' do + it 'returns a list of courses' do + courses = create_list(:course, 3) + + get '/api/v1/courses' + + expect(courses.count).to eq(3) + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response.size).to eq(3) + end + end + + describe 'GET /api/v1/courses/:id' do + it 'returns a single course' do + course = create(:course) + + get "/api/v1/courses/#{course.id}" + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response['name']).to eq(course.name) + end + + it 'returns a not found response for non-existent course' do + get '/api/v1/courses/999' + + expect(response).to have_http_status(:not_found) + end + end + + describe 'POST /api/v1/courses' do + it 'creates a new course' do + course_params = { course: { name: 'New Course', description: 'Course description', photo: 'photo.jpg', + price: 100, teacher: 'John Doe' } } + + post '/api/v1/courses', params: course_params + + expect(response).to have_http_status(:created) + json_response = JSON.parse(response.body) + expect(json_response['name']).to eq(course_params[:course][:name]) + end + end + + describe 'PATCH /api/v1/courses/:id' do + it 'updates a course' do + course = create(:course) + updated_name = 'Updated Course Name' + updated_params = { course: { name: updated_name } } + + patch "/api/v1/courses/#{course.id}", params: updated_params + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response['name']).to eq(updated_name) + end + end + + describe 'DELETE /api/v1/courses/:id' do + it 'deletes a course' do + course = create(:course) + + delete "/api/v1/courses/#{course.id}" + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response['message']).to eq('Course deleted') + end + end +end diff --git a/spec/controllers/reservation_spec.rb b/spec/controllers/reservation_spec.rb new file mode 100644 index 0000000..12a6326 --- /dev/null +++ b/spec/controllers/reservation_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' + +RSpec.describe Api::V1::ReservationsController, type: :request do + include Devise::Test::IntegrationHelpers + before(:each) do + @user = create(:user) + @course = create(:course) + login_as(@user, scope: :user) + end + + describe 'GET /api/v1/reservations' do + it 'returns a list of reservations' do + reservations = [] + 3.times do + reservations << Reservation.create!( + city: 'Lagos', + date: Date.today, + user_id: @user.id, + course_id: @course.id + ) + end + + get '/api/v1/reservations' + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response.size).to eq(3) + end + end + + describe 'GET /api/v1/reservations/:id' do + it 'returns a single reservation' do + reservation = Reservation.create!( + city: 'Lagos', + date: Date.today, + user_id: @user.id, + course_id: @course.id + ) + + get "/api/v1/reservations/#{reservation.id}" + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response['city']).to eq(reservation.city) + expect(json_response['user_id']).to eq(reservation.user_id) + expect(json_response['course_id']).to eq(reservation.course_id) + end + + it 'returns a not found response for non-existent reservation' do + get '/api/v1/reservations/999' + + expect(response).to have_http_status(:not_found) + end + end + + describe 'POST /api/v1/reservations' do + it 'creates a new reservation' do + reservation_params = { reservation: { + city: 'Lagos', + date: Date.today, + user_id: @user.id, + course_id: @course.id + } } + + post '/api/v1/reservations', params: reservation_params + + expect(response).to have_http_status(:created) + json_response = JSON.parse(response.body) + expect(json_response['city']).to eq(reservation_params[:reservation][:city]) + end + end + + describe 'PATCH /api/v1/reservations/:id' do + it 'updates a reservation' do + reservation = Reservation.create!( + city: 'Lagos', + date: Date.today, + user_id: @user.id, + course_id: @course.id + ) + updated_city = 'Abuja' + updated_params = { reservation: { city: updated_city } } + + patch "/api/v1/reservations/#{reservation.id}", params: updated_params + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response['city']).to eq(updated_city) + end + end + + describe 'DELETE /api/v1/reservations/:id' do + it 'deletes a reservation' do + reservation = Reservation.create!( + city: 'Lagos', + date: Date.today, + user_id: @user.id, + course_id: @course.id + ) + + delete "/api/v1/reservations/#{reservation.id}" + + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response['message']).to eq('Reservation deleted') + end + end +end diff --git a/spec/factories/courses.rb b/spec/factories/courses.rb new file mode 100644 index 0000000..2d9fc5d --- /dev/null +++ b/spec/factories/courses.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :course do + name { 'Redux' } + description { 'description for redux' } + photo { 'photo.png' } + price { 100.0 } + teacher { 'Elijah' } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..dd9615f --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :user do + name { 'Burger' } + email { 'burger@example.com' } + password { '123456' } + end +end diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb new file mode 100644 index 0000000..6bde0bc --- /dev/null +++ b/spec/models/course_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +RSpec.describe Course, type: :model do + before(:each) do + @course = create(:course) + end + + describe 'initialization' do + it 'should be a Course object' do + expect(@course).to be_a Course + end + + it 'should have attributes' do + expect(@course).to have_attributes(name: 'Redux', description: 'description for redux', photo: 'photo.png', + price: 100.0, teacher: 'Elijah') + end + end + + describe 'validations' do + before(:each) do + expect(@course).to be_valid + end + + it 'should validate presence of name' do + @course.name = nil + expect(@course).not_to be_valid + end + + it 'should validate length of name' do + @course.name = 'a' + expect(@course).not_to be_valid + @course.name = 'a' * 26 + expect(@course).not_to be_valid + end + + it 'should validate presence of description' do + @course.description = nil + expect(@course).not_to be_valid + @course.description = 'a' * 9 + expect(@course).not_to be_valid + @course.description = 'a' * 501 + expect(@course).not_to be_valid + end + + it 'should validate length of teacher' do + @course.teacher = 'a' + expect(@course).not_to be_valid + @course.teacher = 'a' * 26 + expect(@course).not_to be_valid + end + end +end diff --git a/spec/models/reservations_spec.rb b/spec/models/reservations_spec.rb new file mode 100644 index 0000000..3937d77 --- /dev/null +++ b/spec/models/reservations_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe Reservation, type: :model do + before(:each) do + @user = create(:user) + @course = create(:course) + @reservation = Reservation.new( + user_id: @user.id, + course_id: @course.id, + city: 'Enugu', + date: '1998-05-23' + ) + end + + describe 'initialization' do + it 'should be a Reservation object' do + expect(@reservation).to be_a Reservation + end + + it 'should have attributes' do + expect(@reservation).to have_attributes( + user_id: @user.id, + course_id: @course.id, + city: 'Enugu', + date: Time.zone.parse('1998-05-23') # Convert expected date string to timestamp + ) + end + end + + describe 'validations' do + before(:each) do + expect(@reservation).to be_valid + end + + it 'should validate presence of city' do + @reservation.city = nil + expect(@reservation).not_to be_valid + end + + it 'should validate presence of date' do + @reservation.date = nil + expect(@reservation).not_to be_valid + end + + it 'should validate presence of user_id' do + @reservation.user_id = nil + expect(@reservation).not_to be_valid + end + + it 'should validate presence of course_id' do + @reservation.course_id = nil + expect(@reservation).not_to be_valid + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..cffb4ac --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,29 @@ +# spec/models/user_spec.rb +require 'rails_helper' + +RSpec.describe User, type: :model do + describe 'validations' do + it 'is valid with a name' do + user = create(:user) + expect(user).to be_valid + end + + it 'is invalid without a name' do + user = User.new(name: nil, email: 'burger@example.com', password: '123456') + expect(user).not_to be_valid + expect(user.errors[:name]).to include("can't be blank") + end + + it 'is invalid without an email' do + user = User.new(name: 'Burger', email: nil, password: '123456') + expect(user).not_to be_valid + expect(user.errors[:email]).to include("can't be blank") + end + + it 'is invalid without a password' do + user = User.new(name: 'Burger', email: 'burger@example.com', password: nil) + expect(user).not_to be_valid + expect(user.errors[:password]).to include("can't be blank") + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..5f319e2 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,66 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +require 'factory_bot_rails' + +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/requests/api/1registrations_spec.rb b/spec/requests/api/1registrations_spec.rb new file mode 100644 index 0000000..c7ffa05 --- /dev/null +++ b/spec/requests/api/1registrations_spec.rb @@ -0,0 +1,55 @@ +# rubocop:disable Metrics/BlockLength +require 'swagger_helper' + +RSpec.describe 'api/registrations', type: :request do + path '/signup' do + post 'Sign up' do + tags 'Sign up' + consumes 'application/json' + produces 'application/json' + + parameter name: :user, in: :body, schema: { + type: :object, + properties: { + user: { + type: :object, + properties: { + name: { type: :string }, + email: { type: :string }, + password: { type: :string } + } + } + } + } + + response '200', 'Signed up successfully.' do + schema type: :object, + properties: { + id: { type: :integer }, + email: { type: :string }, + name: { type: :string } + }, + required: %i[id email name] + + let(:user) do + { + user: { + name: 'Elijah', + email: 'vitoesi@gmail.com', + password: '123456' + } + } + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:ok) + end + end + + response '422', 'Something went wrong' do + run_test! + end + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/requests/api/2sessions_spec.rb b/spec/requests/api/2sessions_spec.rb new file mode 100644 index 0000000..174effa --- /dev/null +++ b/spec/requests/api/2sessions_spec.rb @@ -0,0 +1,58 @@ +require 'swagger_helper' + +RSpec.describe 'api/sessions', type: :request do + path '/login' do + post 'Login' do + tags 'Login' + consumes 'application/json' + produces 'application/json' + + parameter name: :user, in: :body, schema: { + type: :object, + properties: { + user: { + type: :object, + properties: { + email: { type: :string }, + password: { type: :string } + } + } + } + } + + response '200', 'You are logged in.' do + let(:user) { { user: { email: 'vitoesi@gmail.com', password: 'password' } } } + run_test! + end + + response '401', 'Invalid login credentials.' do + let(:user) { { user: { email: 'user.vitoesi.com', password: 'password' } } } + run_test! + end + + response '404', 'User not found.' do + let(:user) { { user: { email: 'user.vitoesi.com', password: 'password' } } } + run_test! + end + end + end + + path '/logout' do + delete 'Logout' do + tags 'Logout' + security [Bearer: []] + consumes 'application/json' + produces 'application/json' + parameter name: :Authorization, in: :header, type: :string + + response '200', 'You are logged out.' do + let(:user) { { user: { email: 'vitoesi@gmail.com', password: '123456' } } } + run_test! + end + + response '401', 'Invalid token.' do + run_test! + end + end + end +end diff --git a/spec/requests/api/3courses_spec.rb b/spec/requests/api/3courses_spec.rb new file mode 100644 index 0000000..2ba51a3 --- /dev/null +++ b/spec/requests/api/3courses_spec.rb @@ -0,0 +1,188 @@ +# rubocop:disable Metrics/BlockLength +require 'swagger_helper' + +RSpec.describe '/api/v1/courses', type: :request do + path '/api/v1/courses' do + get 'Retrieve all courses' do + tags 'Courses' + security [Bearer: []] + produces 'application/json' + parameter name: :Authorization, in: :header, type: :string + + response '200', 'Get courses' do + schema type: :array, + items: { + type: :object, + properties: { + id: { type: :integer }, + name: { type: :string }, + photo: { type: :string }, + teacher: { type: :string }, + description: { type: :string }, + price: { type: :number, format: :float } + }, + required: %i[id name description photo teacher price] + } + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:ok) + end + end + end + + post 'Create a course' do + tags 'Courses' + security [Bearer: []] + consumes 'application/json' + produces 'application/json' + parameter name: :Authorization, in: :header, type: :string + parameter name: :course, in: :body, schema: { + type: :object, + properties: { + course: { + type: :object, + properties: { + name: { type: :string }, + description: { type: :string }, + price: { type: :number, format: :float }, + photo: { type: :string }, + teacher: { type: :string } + }, + required: %i[name description price photo teacher] + } + }, + required: [:course] + } + + response '200', 'Course created' do + schema type: :object, + properties: { + id: { type: :integer }, + name: { type: :string }, + description: { type: :string }, + price: { type: :number, format: :float }, + photo: { type: :string }, + teacher: { type: :string } + }, + required: %i[id name description price photo teacher] + + let(:course) do + { course: { name: 'Example Course', description: 'Example description', price: 10.0, + photo: 'example.jpg', teacher: 'Example Teacher' } } + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:ok) + end + end + + response '401', 'You are not authorized to create a course.' do + let(:course) do + { course: { name: 'Example Course', description: 'Example description', price: 10.0, + photo: 'example.jpg', teacher: 'Example Teacher' } } + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:unauthorized) + end + end + + response '422', 'Unprocessable Entity' do + let(:course) do + { course: { name: nil, description: 'Example description', price: 10.0, + photo: 'example.jpg', teacher: 'Example Teacher' } } + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + end + + path '/api/v1/courses/{id}' do + get 'Retrieve a specific course' do + tags 'Courses' + security [Bearer: []] + produces 'application/json' + parameter name: :id, in: :path, type: :integer + parameter name: :Authorization, in: :header, type: :string + + response '200', 'Course details' do + schema type: :object, + properties: { + id: { type: :integer }, + name: { type: :string }, + description: { type: :string }, + price: { type: :number, format: :float }, + photo: { type: :string }, + teacher: { type: :string } + }, + required: %i[id name description price photo teacher] + + let(:id) do + Course.create(name: 'Example Course', description: 'Example description', price: 10.0, + photo: 'example.jpg', teacher: 'Example Teacher').id + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:ok) + end + end + + response '404', 'Not Found' do + let(:id) { 9999 } + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:not_found) + end + end + end + + path '/api/v1/courses/{id}' + delete 'Delete a course' do + tags 'Courses' + security [Bearer: []] + produces 'application/json' + parameter name: :id, in: :path, type: :integer + parameter name: :Authorization, in: :header, type: :string + + response '200', 'Course deleted' do + schema type: :object, + properties: { + message: { type: :string } + }, + required: [:message] + + let(:id) do + Course.create(name: 'Example Course', description: 'Example description', price: 10.0, + photo: 'example.jpg', teacher: 'Example Teacher').id + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:ok) + end + end + + response '401', 'You are not authorized to delete a course.' do + let(:id) do + Course.create(name: 'Example Course', description: 'Example description', price: 10.0, + photo: 'example.jpg', teacher: 'Example Teacher').id + end + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:unauthorized) + end + end + + response '404', 'Not Found' do + let(:id) { 9999 } + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:not_found) + end + end + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/requests/api/4reservations_spec.rb b/spec/requests/api/4reservations_spec.rb new file mode 100644 index 0000000..3ace63a --- /dev/null +++ b/spec/requests/api/4reservations_spec.rb @@ -0,0 +1,112 @@ +require 'swagger_helper' + +RSpec.describe 'api/v1/reservations', type: :request do + # rubocop:disable Metrics/BlockLength + path '/api/v1/reservations' do + post 'Create a reservation' do + tags 'Reservations' + security [Bearer: []] + consumes 'application/json' + produces 'application/json' + parameter name: :Authorization, in: :header, type: :string + parameter name: :reservation, in: :body, schema: { + type: :object, + properties: { + reservation: { + type: :object, + properties: { + course_id: { type: :integer }, + date: { type: :string }, + city: { type: :string }, + user_id: { type: :integer } + }, + required: %i[course_id date city user_id] + } + }, + required: [:reservation] + } + + response '201', 'Reservation created' do + schema type: :object, + properties: { + id: { type: :integer }, + user_id: { type: :integer }, + course_id: { type: :integer }, + city: { type: :string }, + date: { type: :integer }, + course: { + type: :object, + properties: { + id: { type: :integer }, + name: { type: :string }, + photo: { type: :string }, + teacher: { type: :string }, + user_id: { type: :integer }, + description: { type: :string }, + price: { type: :number, format: :float } + }, + required: %i[id name] + } + }, + required: %i[id user_id course_id city date] + + let(:reservation) { { reservation: { course_id: 1, date: '1998-05-23', city: 'Enugu', user_id: 123 } } } + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:created) + end + end + + response '422', 'Unprocessable Entity' do + let(:reservation) { { reservation: { course_id: 1, date: nil, city: nil, user_id: nil } } } + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + end + + path '/api/v1/reservations' do + get 'List reservations' do + tags 'Reservations' + security [Bearer: []] + consumes 'application/json' + produces 'application/json' + parameter name: :Authorization, in: :header, type: :string + + response '200', 'List of reservations' do + schema type: :array, + items: { + type: :object, + properties: { + id: { type: :integer }, + user_id: { type: :integer }, + course_id: { type: :integer }, + city: { type: :string }, + date: { type: :integer }, + course: { + type: :object, + properties: { + id: { type: :integer }, + name: { type: :string }, + photo: { type: :string }, + teacher: { type: :string }, + user_id: { type: :integer }, + description: { type: :string }, + price: { type: :number, format: :float } + }, + required: %i[id name] + } + }, + required: %i[id user_id course_id city date] + } + + run_test! do |_params, _body, _headers| + expect(response).to have_http_status(:ok) + end + end + end + end + # rubocop:enable Metrics/BlockLength +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..9c96a9b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,92 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 0000000..feaae2b --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.swagger_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under swagger_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a swagger_doc tag to the + # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' + config.swagger_docs = { + 'v1/swagger.yaml' => { + openapi: '3.0.1', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ] + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml +end diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml new file mode 100644 index 0000000..02c653d --- /dev/null +++ b/swagger/v1/swagger.yaml @@ -0,0 +1,428 @@ +--- +openapi: 3.0.1 +info: + title: API V1 + version: v1 +paths: + "/signup": + post: + summary: Sign up + tags: + - Sign up + parameters: [] + responses: + '200': + description: Signed up successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: integer + email: + type: string + name: + type: string + required: + - id + - email + - name + '422': + description: Something went wrong + requestBody: + content: + application/json: + schema: + type: object + properties: + user: + type: object + properties: + name: + type: string + email: + type: string + password: + type: string + "/login": + post: + summary: Login + tags: + - Login + parameters: [] + responses: + '200': + description: You are logged in. + '401': + description: Invalid login credentials. + '404': + description: User not found. + requestBody: + content: + application/json: + schema: + type: object + properties: + user: + type: object + properties: + email: + type: string + password: + type: string + "/logout": + delete: + summary: Logout + tags: + - Logout + security: + - Bearer: [] + parameters: + - name: Authorization + in: header + schema: + type: string + responses: + '200': + description: You are logged out. + '401': + description: Invalid token. + "/api/v1/courses": + get: + summary: Retrieve all courses + tags: + - Courses + security: + - Bearer: [] + parameters: + - name: Authorization + in: header + schema: + type: string + responses: + '200': + description: Get courses + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + photo: + type: string + teacher: + type: string + description: + type: string + price: + type: number + format: float + required: + - id + - name + - description + - photo + - teacher + - price + post: + summary: Create a course + tags: + - Courses + security: + - Bearer: [] + parameters: + - name: Authorization + in: header + schema: + type: string + responses: + '200': + description: Course created + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + price: + type: number + format: float + photo: + type: string + teacher: + type: string + required: + - id + - name + - description + - price + - photo + - teacher + '401': + description: You are not authorized to create a course. + '422': + description: Unprocessable Entity + requestBody: + content: + application/json: + schema: + type: object + properties: + course: + type: object + properties: + name: + type: string + description: + type: string + price: + type: number + format: float + photo: + type: string + teacher: + type: string + required: + - name + - description + - price + - photo + - teacher + required: + - course + "/api/v1/courses/{id}": + get: + summary: Retrieve a specific course + tags: + - Courses + security: + - Bearer: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + - name: Authorization + in: header + schema: + type: string + responses: + '200': + description: Course details + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + price: + type: number + format: float + photo: + type: string + teacher: + type: string + required: + - id + - name + - description + - price + - photo + - teacher + '404': + description: Not Found + delete: + summary: Delete a course + tags: + - Courses + security: + - Bearer: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + - name: Authorization + in: header + schema: + type: string + responses: + '200': + description: Course deleted + content: + application/json: + schema: + type: object + properties: + message: + type: string + required: + - message + '401': + description: You are not authorized to delete a course. + '404': + description: Not Found + "/api/v1/reservations": + post: + summary: Create a reservation + tags: + - Reservations + security: + - Bearer: [] + parameters: + - name: Authorization + in: header + schema: + type: string + responses: + '201': + description: Reservation created + content: + application/json: + schema: + type: object + properties: + id: + type: integer + user_id: + type: integer + course_id: + type: integer + city: + type: string + date: + type: integer + course: + type: object + properties: + id: + type: integer + name: + type: string + photo: + type: string + teacher: + type: string + user_id: + type: integer + description: + type: string + price: + type: number + format: float + required: + - id + - name + required: + - id + - user_id + - course_id + - city + - date + '422': + description: Unprocessable Entity + requestBody: + content: + application/json: + schema: + type: object + properties: + reservation: + type: object + properties: + course_id: + type: integer + date: + type: string + city: + type: string + user_id: + type: integer + required: + - course_id + - date + - city + - user_id + required: + - reservation + get: + summary: List reservations + tags: + - Reservations + security: + - Bearer: [] + parameters: + - name: Authorization + in: header + schema: + type: string + responses: + '200': + description: List of reservations + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + user_id: + type: integer + course_id: + type: integer + city: + type: string + date: + type: integer + course: + type: object + properties: + id: + type: integer + name: + type: string + photo: + type: string + teacher: + type: string + user_id: + type: integer + description: + type: string + price: + type: number + format: float + required: + - id + - name + required: + - id + - user_id + - course_id + - city + - date +servers: +- url: https://{defaultHost} + variables: + defaultHost: + default: www.example.com diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29