├── .browserslistrc
├── .circleci
└── config.yml
├── .deploy
├── bin
│ ├── build
│ │ └── nginx_image.sh
│ ├── deploy
│ │ ├── base.sh
│ │ ├── production.sh
│ │ └── staging.sh
│ └── variables.sh
└── nginx
│ ├── Dockerfile
│ ├── configs
│ ├── nginx.conf
│ └── templates
│ │ └── default.conf
│ └── docker-entrypoint.sh
├── .docker
├── development
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── entrypoint.sh
├── production
│ ├── Dockerfile
│ └── entrypoint.sh
└── staging
│ ├── Dockerfile
│ └── entrypoint.sh
├── .dockerignore
├── .fasterer.yml
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── dependabot.yml
├── .gitignore
├── .reek.yml
├── .rspec
├── .rubocop.yml
├── .ruby-gemset
├── .ruby-version
├── .simplecov
├── .terraform
├── .gitignore
├── modules
│ ├── ecs_cluster
│ │ ├── autoscaling_group.tf
│ │ ├── autoscaling_policy.tf
│ │ ├── launch_template.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── templates
│ │ │ └── user_data.sh
│ │ └── variables.tf
│ ├── global
│ │ ├── iam.tf
│ │ ├── outputs.tf
│ │ ├── variables.tf
│ │ └── vpc.tf
│ ├── main
│ │ ├── cloudwatch.tf
│ │ ├── ecr.tf
│ │ ├── keypair.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ ├── s3.tf
│ │ ├── variables.tf
│ │ └── versions.tf
│ └── variables
│ │ ├── outputs.tf
│ │ └── variables.tf
├── production
│ ├── acm.tf
│ ├── alb.tf
│ ├── backend.tf
│ ├── ecs_application.tf
│ ├── ecs_worker.tf
│ ├── elasticache.tf
│ ├── main.tf
│ ├── rds.tf
│ ├── sg.tf
│ ├── ssh_keys
│ │ └── boilerplate-api-production.pub
│ ├── templates
│ │ ├── container_definitions.json
│ │ └── worker_definitions.json
│ └── variables.tf
└── staging
│ ├── alb.tf
│ ├── backend.tf
│ ├── ecs.tf
│ ├── main.tf
│ ├── sg.tf
│ ├── ssh_keys
│ └── boilerplate-api-staging.pub
│ ├── ssl_certificates
│ └── .keep
│ ├── templates
│ └── container_definitions.json
│ └── variables.tf
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── admin
│ ├── admin_users.rb
│ └── dashboard.rb
├── assets
│ └── config
│ │ └── manifest.js
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── concepts
│ ├── api
│ │ └── v1
│ │ │ ├── lib
│ │ │ ├── contract
│ │ │ │ ├── filtering_pre_validation.rb
│ │ │ │ ├── filtering_validation.rb
│ │ │ │ ├── inclusion_validation.rb
│ │ │ │ ├── pagination_validation.rb
│ │ │ │ ├── sorting_pre_validation.rb
│ │ │ │ └── sorting_validation.rb
│ │ │ ├── operation
│ │ │ │ ├── filtering.rb
│ │ │ │ ├── inclusion.rb
│ │ │ │ ├── pagination.rb
│ │ │ │ ├── perform_filtering.rb
│ │ │ │ ├── perform_ordering.rb
│ │ │ │ └── sorting.rb
│ │ │ ├── serializer
│ │ │ │ └── account.rb
│ │ │ └── service
│ │ │ │ └── json_api.rb
│ │ │ └── users
│ │ │ ├── account
│ │ │ └── profiles
│ │ │ │ ├── operation
│ │ │ │ └── show.rb
│ │ │ │ └── serializer
│ │ │ │ └── show.rb
│ │ │ ├── lib
│ │ │ ├── contract
│ │ │ │ └── decrypt_email_token_validation.rb
│ │ │ ├── operation
│ │ │ │ ├── check_email_token_redis_equality.rb
│ │ │ │ └── decrypt_email_token.rb
│ │ │ └── service
│ │ │ │ ├── email_token.rb
│ │ │ │ ├── redis_adapter.rb
│ │ │ │ ├── session_token.rb
│ │ │ │ └── token_namespace.rb
│ │ │ ├── registrations
│ │ │ ├── contract
│ │ │ │ └── create.rb
│ │ │ ├── operation
│ │ │ │ └── create.rb
│ │ │ └── worker
│ │ │ │ └── email_confirmation.rb
│ │ │ ├── reset_passwords
│ │ │ ├── contract
│ │ │ │ ├── create.rb
│ │ │ │ └── update.rb
│ │ │ ├── operation
│ │ │ │ ├── create.rb
│ │ │ │ ├── show.rb
│ │ │ │ └── update.rb
│ │ │ └── worker
│ │ │ │ └── email_reset_password_url.rb
│ │ │ ├── sessions
│ │ │ ├── contract
│ │ │ │ └── create.rb
│ │ │ ├── operation
│ │ │ │ ├── create.rb
│ │ │ │ └── destroy.rb
│ │ │ └── refreshes
│ │ │ │ └── operation
│ │ │ │ └── create.rb
│ │ │ └── verifications
│ │ │ └── operation
│ │ │ └── show.rb
│ ├── application_contract.rb
│ ├── application_decorator.rb
│ ├── application_operation.rb
│ ├── application_serializer.rb
│ └── application_worker.rb
├── controllers
│ ├── api
│ │ └── v1
│ │ │ └── users
│ │ │ ├── account
│ │ │ └── profiles_controller.rb
│ │ │ ├── registrations_controller.rb
│ │ │ ├── reset_passwords_controller.rb
│ │ │ ├── session
│ │ │ └── refreshes_controller.rb
│ │ │ ├── sessions_controller.rb
│ │ │ └── verifications_controller.rb
│ ├── api_controller.rb
│ ├── application_controller.rb
│ ├── authorized_api_controller.rb
│ └── concerns
│ │ ├── authentication.rb
│ │ └── default_endpoint.rb
├── jobs
│ └── application_job.rb
├── mailers
│ ├── application_mailer.rb
│ └── user_mailer.rb
├── models
│ ├── account.rb
│ ├── admin_user.rb
│ ├── application_record.rb
│ ├── concerns
│ │ └── .keep
│ └── user.rb
├── uploaders
│ └── application_uploader.rb
├── views
│ ├── layouts
│ │ ├── mailer.html.erb
│ │ └── mailer.text.erb
│ └── user_mailer
│ │ ├── confirmation.html.erb
│ │ ├── confirmation.text.erb
│ │ ├── reset_password.html.erb
│ │ ├── reset_password.text.erb
│ │ ├── reset_password_successful.html.erb
│ │ ├── reset_password_successful.text.erb
│ │ ├── verification_successful.html.erb
│ │ └── verification_successful.text.erb
├── webpacker
│ ├── packs
│ │ ├── active_admin.js
│ │ └── active_admin
│ │ │ └── print.scss
│ └── stylesheets
│ │ └── active_admin.scss
└── workers
│ ├── application_worker.rb
│ └── sentry_worker.rb
├── babel.config.js
├── bin
├── bundle
├── docker
├── rails
├── rake
├── setup
├── spring
├── webpack
└── webpack-dev-server
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── credentials
│ ├── credentials.yml
│ ├── development.yml.enc
│ ├── production.yml.enc
│ ├── staging.yml.enc
│ └── test.yml.enc
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ ├── staging.rb
│ └── test.rb
├── i18n-tasks.yml
├── initializers
│ ├── active_admin.rb
│ ├── application_controller_renderer.rb
│ ├── backtrace_silencers.rb
│ ├── constants
│ │ ├── shared.rb
│ │ └── tokens_namespace.rb
│ ├── cors.rb
│ ├── devise.rb
│ ├── dry_validation.rb
│ ├── filter_parameter_logging.rb
│ ├── generators.rb
│ ├── inflections.rb
│ ├── json_api
│ │ ├── filtering.rb
│ │ ├── pagination.rb
│ │ └── sorting.rb
│ ├── jwt_sessions.rb
│ ├── mime_types.rb
│ ├── oj.rb
│ ├── pagy.rb
│ ├── ransack.rb
│ ├── redis.rb
│ ├── reform.rb
│ ├── rswag_api.rb
│ ├── rswag_ui.rb
│ ├── sentry.rb
│ ├── shrine.rb
│ ├── sidekiq.rb
│ ├── trailblazer_macro.rb
│ ├── wrap_parameters.rb
│ └── zeitwerk.rb
├── locales
│ ├── devise.en.yml
│ ├── en.errors.yml
│ └── en.user_mailer.yml
├── puma.rb
├── rails_best_practices.yml
├── routes.rb
├── sidekiq.yml
├── spring.rb
├── storage.yml
├── webpack
│ ├── development.js
│ ├── environment.js
│ ├── plugins
│ │ └── jquery.js
│ ├── production.js
│ └── test.js
└── webpacker.yml
├── db
├── migrate
│ ├── 20190813110526_enable_uuid.rb
│ ├── 20190813110527_create_users.rb
│ ├── 20190813124813_create_accounts.rb
│ └── 20200831072010_devise_create_admin_users.rb
├── schema.rb
└── seeds.rb
├── dip.yml
├── lefthook.yml
├── lib
├── container.rb
├── macro
│ ├── add_contract_error.rb
│ ├── assign.rb
│ ├── decorate.rb
│ ├── inject.rb
│ ├── links_builder.rb
│ ├── model.rb
│ ├── model_remove.rb
│ ├── policy.rb
│ ├── renderer.rb
│ ├── schema.rb
│ └── semantic.rb
├── service
│ ├── json_api
│ │ ├── base_error_serializer.rb
│ │ ├── error_data_structure_parser.rb
│ │ ├── hash_error_serializer.rb
│ │ ├── paginator.rb
│ │ ├── resource_error_serializer.rb
│ │ ├── resource_serializer.rb
│ │ └── uri_query_error_serializer.rb
│ └── pagy.rb
├── tasks
│ ├── factory_bot_linter.rake
│ └── generate_api_documentation.rake
└── types
│ └── json_api.rb
├── log
└── .keep
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── robots.txt
├── spec
├── concepts
│ ├── api
│ │ └── v1
│ │ │ ├── lib
│ │ │ ├── operation
│ │ │ │ ├── filtering_spec.rb
│ │ │ │ ├── inclusion_spec.rb
│ │ │ │ ├── pagination_spec.rb
│ │ │ │ ├── perform_filtering_spec.rb
│ │ │ │ ├── perform_ordering_spec.rb
│ │ │ │ └── sorting_spec.rb
│ │ │ └── service
│ │ │ │ └── json_api_spec.rb
│ │ │ └── users
│ │ │ ├── account
│ │ │ └── profiles
│ │ │ │ └── show_spec.rb
│ │ │ ├── lib
│ │ │ ├── operation
│ │ │ │ ├── check_email_token_redis_equality_spec.rb
│ │ │ │ └── decrypt_email_token_spec.rb
│ │ │ └── service
│ │ │ │ ├── email_token_spec.rb
│ │ │ │ ├── redis_adapter_spec.rb
│ │ │ │ ├── session_token_spec.rb
│ │ │ │ └── token_namespace_spec.rb
│ │ │ ├── registrations
│ │ │ ├── create_spec.rb
│ │ │ └── worker
│ │ │ │ └── email_confirmation_spec.rb
│ │ │ ├── reset_passwords
│ │ │ ├── create_spec.rb
│ │ │ ├── show_spec.rb
│ │ │ ├── update_spec.rb
│ │ │ └── worker
│ │ │ │ └── email_reset_password_url_spec.rb
│ │ │ ├── sessions
│ │ │ ├── create_spec.rb
│ │ │ ├── destroy_spec.rb
│ │ │ └── refreshes
│ │ │ │ └── create_spec.rb
│ │ │ └── verifications
│ │ │ └── show_spec.rb
│ ├── application_contract_spec.rb
│ ├── application_decorator_spec.rb
│ ├── application_operation_spec.rb
│ ├── application_serializer_spec.rb
│ └── application_worker_spec.rb
├── constants
│ ├── shared_spec.rb
│ └── token_namespace_spec.rb
├── controllers
│ ├── api
│ │ └── v1
│ │ │ └── users
│ │ │ └── sessions_controller_spec.rb
│ ├── api_controller_spec.rb
│ └── application_controller_spec.rb
├── factories
│ ├── accounts.rb
│ ├── admin_users.rb
│ └── users.rb
├── features
│ └── admin
│ │ ├── admin_users
│ │ ├── edit_spec.rb
│ │ ├── index_spec.rb
│ │ └── show_spec.rb
│ │ ├── check_root_path_spec.rb
│ │ ├── sign_in_spec.rb
│ │ └── sign_out_spec.rb
├── jobs
│ └── application_job_spec.rb
├── lib
│ ├── macro
│ │ ├── add_contract_error_spec.rb
│ │ ├── assign_spec.rb
│ │ ├── decorate_spec.rb
│ │ ├── inject_spec.rb
│ │ ├── links_builder_spec.rb
│ │ ├── model_remove_spec.rb
│ │ ├── model_spec.rb
│ │ ├── policy_spec.rb
│ │ ├── renderer_spec.rb
│ │ ├── schema_spec.rb
│ │ └── semantic_spec.rb
│ ├── service
│ │ ├── json_api
│ │ │ ├── hash_error_serializer_spec.rb
│ │ │ ├── paginator_spec.rb
│ │ │ ├── resource_error_serializer_spec.rb
│ │ │ ├── resource_serializer_spec.rb
│ │ │ └── uri_query_error_serializer_spec.rb
│ │ └── pagy_spec.rb
│ └── types
│ │ └── json_api_spec.rb
├── mailers
│ ├── application_mailer_spec.rb
│ └── user_mailer_spec.rb
├── models
│ ├── account_spec.rb
│ ├── application_record_spec.rb
│ └── user_spec.rb
├── rails_helper.rb
├── requests
│ └── api
│ │ └── v1
│ │ └── users
│ │ ├── account
│ │ └── profiles_spec.rb
│ │ ├── registrations_spec.rb
│ │ ├── reset_passwords_spec.rb
│ │ ├── session
│ │ └── refreshes_spec.rb
│ │ ├── sessions_spec.rb
│ │ └── verifications_spec.rb
├── spec_helper.rb
├── support
│ ├── config
│ │ ├── capybara.rb
│ │ ├── ffaker.rb
│ │ ├── json_matchers.rb
│ │ ├── n_plus_one_control.rb
│ │ ├── shoulda_matchers.rb
│ │ └── sidekiq.rb
│ ├── helpers
│ │ ├── operation_helpers.rb
│ │ ├── request_helpers.rb
│ │ ├── root_helpers.rb
│ │ └── root_helpers_spec.rb
│ ├── matchers
│ │ ├── macro_id_with.rb
│ │ └── not_change.rb
│ ├── schemas
│ │ ├── errors.json
│ │ └── v1
│ │ │ └── users
│ │ │ ├── account
│ │ │ └── profile
│ │ │ │ └── show.json
│ │ │ ├── registration
│ │ │ └── create.json
│ │ │ └── session
│ │ │ ├── create.json
│ │ │ └── refresh
│ │ │ └── create.json
│ └── shared_examples
│ │ ├── operation
│ │ ├── has_email_token_decryption_errors.rb
│ │ ├── has_email_token_equality_errors.rb
│ │ ├── has_validation_errors.rb
│ │ └── nested_inclusion_errors.rb
│ │ ├── request
│ │ ├── renders_bad_request_errors.rb
│ │ ├── renders_unauthenticated_errors.rb
│ │ ├── renders_unprocessable_entity_errors.rb
│ │ ├── renders_uri_query_errors.rb
│ │ └── returns_not_found_status.rb
│ │ └── service
│ │ └── json_api
│ │ └── error_data_structure.rb
├── swagger_helper.rb
├── uploaders
│ └── application_uploader_spec.rb
└── workers
│ ├── application_worker_spec.rb
│ └── sentry_worker_spec.rb
├── storage
└── .keep
├── swagger
└── v1
│ └── swagger.yaml
├── tmp
└── .keep
├── vendor
└── .keep
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 |
--------------------------------------------------------------------------------
/.deploy/bin/build/nginx_image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | source ".deploy/bin/variables.sh"
6 |
7 | while [ $# -gt 0 ]
8 | do
9 | key="$1"
10 |
11 | case $key in
12 | --running-tag)
13 | RUNNING_TAG="$2"
14 | shift
15 | shift
16 | ;;
17 | *)
18 | echo "Unknown option $1\n"
19 | shift
20 | shift
21 | esac
22 | done
23 |
24 | IMAGE_NAME="boilerplate-api/$RUNNING_TAG/web-server"
25 | REPO="$ECR_ID.dkr.ecr.$REGION.amazonaws.com/$IMAGE_NAME"
26 |
27 | HASH_TAG="$(git rev-parse --short HEAD)"
28 |
29 | RUNNING_IMAGE=$REPO:$RUNNING_TAG
30 | CURRENT_IMAGE=$IMAGE_NAME:$HASH_TAG
31 |
32 | $(aws ecr get-login --region $REGION --no-include-email)
33 |
34 | docker build --cache-from=$RUNNING_IMAGE -t $CURRENT_IMAGE ./.deploy/nginx
35 |
36 | docker tag $CURRENT_IMAGE $REPO:$HASH_TAG
37 | docker tag $CURRENT_IMAGE $RUNNING_IMAGE
38 |
39 | docker push $REPO:$HASH_TAG
40 | docker push $RUNNING_IMAGE
41 |
--------------------------------------------------------------------------------
/.deploy/bin/deploy/production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source ".deploy/bin/variables.sh"
4 | source ".deploy/bin/deploy/base.sh"
5 |
6 | IMAGE_NAME="boilerplate-api/production/server-app"
7 |
8 | deploy \
9 | --region "$REGION" \
10 | --aws-access-key "$AWS_ACCESS_KEY_ID" \
11 | --aws-secret-key "$AWS_SECRET_ACCESS_KEY" \
12 | --image-name "$IMAGE_NAME" \
13 | --repo "$ECR_ID.dkr.ecr.$REGION.amazonaws.com/$IMAGE_NAME" \
14 | --cluster "boilerplate-api-production" \
15 | --service "boilerplate-api-production" \
16 | --running-tag "production" \
17 | --docker_file ".docker/production/Dockerfile"
18 |
19 | deploy \
20 | --region "$REGION" \
21 | --aws-access-key "$AWS_ACCESS_KEY_ID" \
22 | --aws-secret-key "$AWS_SECRET_ACCESS_KEY" \
23 | --image-name "$IMAGE_NAME" \
24 | --repo "$ECR_ID.dkr.ecr.$REGION.amazonaws.com/$IMAGE_NAME" \
25 | --cluster "boilerplate-api-production" \
26 | --service "boilerplate-api-production-worker" \
27 | --running-tag "production" \
28 | --docker_file ".docker/production/Dockerfile" \
29 | --skip-build true
30 |
--------------------------------------------------------------------------------
/.deploy/bin/deploy/staging.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source ".deploy/bin/variables.sh"
4 | source ".deploy/bin/deploy/base.sh"
5 |
6 | IMAGE_NAME="boilerplate-api/staging/server-app"
7 |
8 | deploy \
9 | --region "$REGION" \
10 | --aws-access-key "$AWS_ACCESS_KEY_ID" \
11 | --aws-secret-key "$AWS_SECRET_ACCESS_KEY" \
12 | --image-name "$IMAGE_NAME" \
13 | --repo "$ECR_ID.dkr.ecr.$REGION.amazonaws.com/$IMAGE_NAME" \
14 | --cluster "boilerplate-api-staging" \
15 | --service "boilerplate-api-staging" \
16 | --running-tag "staging" \
17 | --docker_file ".docker/staging/Dockerfile"
18 |
--------------------------------------------------------------------------------
/.deploy/bin/variables.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export REGION=""
4 | export ECR_ID=""
5 |
--------------------------------------------------------------------------------
/.deploy/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.17-alpine
2 |
3 | ENV USER=nginx
4 |
5 | RUN chown -R $USER:$USER /etc/nginx/conf.d
6 |
7 | USER $USER
8 |
9 | COPY --chown=$USER configs/nginx.conf /etc/nginx/nginx.conf
10 | COPY --chown=$USER configs/templates/default.conf /etc/nginx/conf.d/templates/default.conf
11 |
12 | COPY --chown=$USER docker-entrypoint.sh /usr/local/bin/
13 | ENTRYPOINT ["docker-entrypoint.sh"]
14 |
15 | CMD ["nginx", "-g", "daemon off;"]
16 |
--------------------------------------------------------------------------------
/.deploy/nginx/configs/nginx.conf:
--------------------------------------------------------------------------------
1 | pid /tmp/nginx.pid;
2 |
3 | worker_processes auto;
4 | worker_rlimit_nofile 4096;
5 |
6 | events {
7 | worker_connections 1024;
8 | }
9 |
10 | http {
11 | client_body_temp_path /tmp/client_temp;
12 |
13 | proxy_temp_path /tmp/proxy_temp_path;
14 | fastcgi_temp_path /tmp/fastcgi_temp;
15 | uwsgi_temp_path /tmp/uwsgi_temp;
16 | scgi_temp_path /tmp/scgi_temp;
17 |
18 | include /etc/nginx/mime.types;
19 | default_type application/octet-stream;
20 |
21 | server_tokens off;
22 |
23 | add_header X-Frame-Options SAMEORIGIN;
24 | add_header X-Content-Type-Options nosniff;
25 | add_header X-XSS-Protection "1; mode=block";
26 |
27 | server_names_hash_bucket_size 64;
28 | server_names_hash_max_size 512;
29 |
30 | sendfile on;
31 | tcp_nopush on;
32 |
33 | types_hash_max_size 2048;
34 |
35 | gzip on;
36 | gzip_disable "msie6";
37 |
38 | include /etc/nginx/conf.d/default.conf;
39 | }
40 |
--------------------------------------------------------------------------------
/.deploy/nginx/configs/templates/default.conf:
--------------------------------------------------------------------------------
1 | upstream app {
2 | server app:3000;
3 | }
4 |
5 | server {
6 | listen 8080 deferred;
7 |
8 | client_max_body_size 15000M;
9 |
10 | keepalive_timeout 10;
11 |
12 | root /home/www/boilerplate-api/public/;
13 |
14 | # Redirect from www to non-www ----------------------------------------------
15 | if ($host ~* "^www\.(.*)") {
16 | return 301 http://$1$request_uri;
17 | }
18 | # ---------------------------------------------------------------------------
19 |
20 | location ^~ /assets/ {
21 | gzip_static on;
22 | expires max;
23 | add_header Cache-Control public;
24 | }
25 |
26 | location ^~ \/admin\/settings(.*) {
27 | proxy_connect_timeout 10800;
28 | proxy_send_timeout 10800;
29 | proxy_read_timeout 10800;
30 | send_timeout 10800;
31 | keepalive_timeout 10800;
32 | }
33 |
34 | location / {
35 | try_files $uri @app_proxy;
36 | }
37 |
38 | location ^~ \/admin\/videos(.*) {
39 | proxy_connect_timeout 10800;
40 | proxy_send_timeout 10800;
41 | proxy_read_timeout 10800;
42 | send_timeout 10800;
43 | keepalive_timeout 10800;
44 | }
45 |
46 | location @app_proxy {
47 | proxy_redirect off;
48 |
49 | proxy_set_header Client-Ip $remote_addr;
50 | proxy_set_header Host $host;
51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
52 | proxy_set_header X-Forwarded-Proto $scheme;
53 | proxy_set_header X-Forwarded-Ssl off;
54 | proxy_set_header X-Forwarded-Port $server_port;
55 | proxy_set_header X-Forwarded-Host $host;
56 |
57 | proxy_pass http://app;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.deploy/nginx/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | envsubst '$STATIC_PATH' < /etc/nginx/conf.d/templates/default.conf > /etc/nginx/conf.d/default.conf
6 |
7 | exec "$@"
8 |
--------------------------------------------------------------------------------
/.docker/development/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:2.7.2-alpine3.12 as Builder
2 |
3 | RUN apk --no-cache add bash git openssh httpie libxml2-dev libxslt-dev postgresql-dev \
4 | tzdata npm nodejs imagemagick yarn make cmake g++ postgresql-client nano
5 |
6 | ENV APP_USER app
7 | ENV APP_USER_HOME /home/$APP_USER
8 | ENV APP_HOME /home/www/boilerplate-api
9 |
10 | RUN adduser -D -h $APP_USER_HOME $APP_USER
11 |
12 | WORKDIR $APP_HOME
13 |
14 | USER $APP_USER
15 |
16 | COPY Gemfile* package.json yarn.lock .ruby-version ./
17 |
18 | RUN gem i bundler -v $(tail -1 Gemfile.lock | tr -d ' ') && \
19 | bundle install || bundle check \
20 | && rm -rf /usr/local/bundle/cache/*.gem \
21 | && find /usr/local/bundle/gems/ -name "*.c" -delete \
22 | && find /usr/local/bundle/gems/ -name "*.o" -delete
23 |
24 | USER root
25 |
26 | RUN yarn install --check-files
27 |
28 | COPY . .
29 |
30 | RUN RAILS_ENV=development rake assets:precompile
31 |
32 | RUN rm -rf node_modules spec tmp/cache
33 |
34 | FROM ruby:2.7.2-alpine3.12
35 |
36 | RUN apk --no-cache add bash openssh httpie libxml2-dev libxslt-dev postgresql-dev \
37 | tzdata nodejs imagemagick postgresql-client nano
38 |
39 | ENV APP_USER app
40 | ENV APP_HOME /home/www/boilerplate-api
41 |
42 | RUN addgroup -g 1000 -S $APP_USER && adduser -u 1000 -S $APP_USER -G $APP_USER
43 |
44 | USER $APP_USER
45 |
46 | COPY --from=Builder /usr/local/bundle/ /usr/local/bundle/
47 | COPY --from=Builder --chown=1000:1000 $APP_HOME $APP_HOME
48 |
49 | WORKDIR $APP_HOME
50 |
51 | USER $APP_USER
52 |
--------------------------------------------------------------------------------
/.docker/development/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 |
3 | volumes:
4 | postgres:
5 | in_memory_store:
6 |
7 | services:
8 | db:
9 | image: postgres:11-alpine
10 | ports:
11 | - 5432:5432
12 | environment:
13 | POSTGRES_USER: postgres
14 | POSTGRES_PASSWORD: postgres
15 | volumes:
16 | - postgres:/var/lib/postgresql/data
17 | healthcheck:
18 | test: ["CMD", "pg_isready", "-U", "postgres"]
19 |
20 | in_memory_store:
21 | image: redis:5-alpine
22 | ports:
23 | - 6379:6379
24 | volumes:
25 | - in_memory_store:/var/lib/redis/data
26 | healthcheck:
27 | test: ["CMD", "redis-cli", "-h", "localhost", "ping"]
28 |
29 | server_app: &server_app
30 | build:
31 | context: ../../
32 | dockerfile: ".docker/development/Dockerfile"
33 | command: bundle exec rails server -b 0.0.0.0
34 | entrypoint: .docker/development/entrypoint.sh
35 | volumes:
36 | - ../..:/home/www/boilerplate-api
37 | tty: true
38 | stdin_open: true
39 | environment: &server_app_env
40 | DB_HOST: db
41 | DB_PORT: 5432
42 | DB_USERNAME: postgres
43 | DB_PASSWORD: postgres
44 | REDIS_DB: redis://in_memory_store:6379
45 | depends_on:
46 | - db
47 | ports:
48 | - 3000:3000
49 | healthcheck:
50 | test: ["CMD", "http", "GET", "localhost:3000"]
51 |
52 | server_worker_app:
53 | <<: *server_app
54 | command: bundle exec sidekiq -C config/sidekiq.yml
55 | entrypoint: ""
56 | ports: []
57 | depends_on:
58 | - db
59 | - in_memory_store
60 | healthcheck:
61 | test: ["CMD-SHELL", "ps ax | grep -v grep | grep sidekiq || exit 1"]
62 |
--------------------------------------------------------------------------------
/.docker/development/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Interpreter identifier
3 |
4 | # Exit on fail
5 | set -e
6 |
7 | rm -f $APP_HOME/tmp/pids/server.pid
8 | rm -f $APP_HOME/tmp/pids/sidekiq.pid
9 |
10 | bundle exec rails db:create
11 | bundle exec rails db:migrate
12 |
13 | exec "$@"
14 |
--------------------------------------------------------------------------------
/.docker/production/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:2.7.2-alpine3.12 as Builder
2 |
3 | RUN apk add --update --no-cache \
4 | bash \
5 | git \
6 | libxml2-dev \
7 | libxslt-dev \
8 | postgresql-dev \
9 | tzdata \
10 | nodejs \
11 | yarn \
12 | cmake \
13 | build-base \
14 | nano
15 |
16 | ENV APP_USER app
17 | ENV APP_USER_HOME /home/$APP_USER
18 | ENV APP_HOME /home/www/boilerplate-api
19 |
20 | RUN adduser -D -h $APP_USER_HOME $APP_USER
21 |
22 | WORKDIR $APP_HOME
23 |
24 | USER $APP_USER
25 |
26 | COPY Gemfile* package.json yarn.lock .ruby-version ./
27 |
28 | RUN gem i bundler -v $(tail -1 Gemfile.lock | tr -d ' ') && \
29 | bundle install --without development test -j $(nproc) --retry 5 || bundle check \
30 | && rm -rf /usr/local/bundle/cache/*.gem \
31 | && find /usr/local/bundle/gems/ -name "*.c" -delete \
32 | && find /usr/local/bundle/gems/ -name "*.o" -delete
33 |
34 | USER root
35 |
36 | RUN yarn install --check-files
37 |
38 | COPY . .
39 |
40 | RUN RAILS_ENV=development rails assets:precompile
41 |
42 | RUN rm -rf node_modules spec tmp/cache
43 |
44 | FROM ruby:2.7.2-alpine3.12
45 |
46 | RUN apk add --update --no-cache \
47 | bash \
48 | build-base \
49 | libxml2-dev \
50 | libxslt-dev \
51 | tzdata \
52 | curl \
53 | git \
54 | nodejs \
55 | imagemagick \
56 | postgresql-client \
57 | nano
58 |
59 | ENV APP_USER app
60 | ENV APP_HOME /home/www/boilerplate-api
61 |
62 | RUN addgroup -g 1000 -S $APP_USER && adduser -u 1000 -S $APP_USER -G $APP_USER
63 |
64 | USER $APP_USER
65 |
66 | COPY --from=Builder /usr/local/bundle/ /usr/local/bundle/
67 | COPY --from=Builder --chown=1000:1000 $APP_HOME $APP_HOME
68 |
69 | RUN chmod -R 777 $APP_HOME/.docker/production/entrypoint.sh
70 | RUN chmod +x $APP_HOME/.docker/production/entrypoint.sh
71 |
72 | WORKDIR $APP_HOME
73 |
74 | USER $APP_USER
75 |
--------------------------------------------------------------------------------
/.docker/production/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | rm -f $APP_HOME/tmp/pids/server.pid
6 | rm -f $APP_HOME/tmp/pids/sidekiq.pid
7 |
8 | bundle exec rails db:create
9 | bundle exec rails db:migrate
10 |
11 | exec "$@"
12 |
--------------------------------------------------------------------------------
/.docker/staging/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:2.7.2-alpine3.12 as Builder
2 |
3 | RUN apk add --update --no-cache \
4 | bash \
5 | git \
6 | libxml2-dev \
7 | libxslt-dev \
8 | postgresql-dev \
9 | tzdata \
10 | nodejs \
11 | yarn \
12 | cmake \
13 | build-base \
14 | nano
15 |
16 | ENV APP_USER app
17 | ENV APP_USER_HOME /home/$APP_USER
18 | ENV APP_HOME /home/www/boilerplate-api
19 |
20 | RUN adduser -D -h $APP_USER_HOME $APP_USER
21 |
22 | WORKDIR $APP_HOME
23 |
24 | USER $APP_USER
25 |
26 | COPY Gemfile* package.json yarn.lock .ruby-version ./
27 |
28 | RUN gem i bundler -v $(tail -1 Gemfile.lock | tr -d ' ') && \
29 | bundle install --without development test -j $(nproc) --retry 5 || bundle check \
30 | && rm -rf /usr/local/bundle/cache/*.gem \
31 | && find /usr/local/bundle/gems/ -name "*.c" -delete \
32 | && find /usr/local/bundle/gems/ -name "*.o" -delete
33 |
34 | USER root
35 |
36 | RUN yarn install --check-files
37 |
38 | COPY . .
39 |
40 | RUN RAILS_ENV=development rails assets:precompile
41 |
42 | RUN rm -rf node_modules spec tmp/cache
43 |
44 | FROM ruby:2.7.2-alpine3.12
45 |
46 | RUN apk add --update --no-cache \
47 | bash \
48 | build-base \
49 | libxml2-dev \
50 | libxslt-dev \
51 | tzdata \
52 | curl \
53 | git \
54 | nodejs \
55 | imagemagick \
56 | postgresql-client \
57 | nano
58 |
59 | ENV APP_USER app
60 | ENV APP_HOME /home/www/boilerplate-api
61 |
62 | RUN addgroup -g 1000 -S $APP_USER && adduser -u 1000 -S $APP_USER -G $APP_USER
63 |
64 | USER $APP_USER
65 |
66 | COPY --from=Builder /usr/local/bundle/ /usr/local/bundle/
67 | COPY --from=Builder --chown=1000:1000 $APP_HOME $APP_HOME
68 |
69 | RUN chmod -R 777 $APP_HOME/.docker/staging/entrypoint.sh
70 | RUN chmod +x $APP_HOME/.docker/staging/entrypoint.sh
71 |
72 | WORKDIR $APP_HOME
73 |
74 | USER $APP_USER
75 |
--------------------------------------------------------------------------------
/.docker/staging/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | rm -f $APP_HOME/tmp/pids/server.pid
6 | rm -f $APP_HOME/tmp/pids/sidekiq.pid
7 |
8 | bundle exec rails db:create
9 | bundle exec rails db:migrate
10 |
11 | exec "$@"
12 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | /log/\*
3 | /tmp/\*
4 | !/log/.keep
5 | !/tmp/.keep
6 | !/tmp/pids/.keep
7 | /vendor/bundle
8 | /public/assets
9 | /config/master.key
10 | /config/credentials.local.yml
11 | /deploy/configs
12 | /.bundle
13 | /venv
14 | /node_modules
15 | /.terraform
16 | /coverage
17 | /.circleci
18 | /.github
19 |
--------------------------------------------------------------------------------
/.fasterer.yml:
--------------------------------------------------------------------------------
1 | exclude_paths:
2 | - 'vendor/**/*.rb'
3 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | [Replace this text by the issue key](replace_this_text_by_the_issue_link)
2 |
3 | ### Description
4 |
5 | Replace this text with a summary of the changes in your merge request.
6 |
7 | ### Before submitting the merge request make sure the following are checked
8 |
9 | - [ ] Followed the style guidelines of this project
10 | - [ ] Performed a self-review of own code
11 | - [ ] Wrote the tests that prove that fix is effective/that feature works
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "bundler"
5 | directory: "/"
6 | schedule:
7 | interval: "monthly"
8 | labels:
9 | - "dependency"
10 | - "need review"
11 | rebase-strategy: "disabled"
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore secrets
11 | /config/application.yml
12 | /config/cable.yml
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/pids/.keep
19 | !/tmp/.keep
20 | .rvmrc
21 |
22 | .DS_Store
23 | .byebug_history
24 |
25 | # Ignore spec files
26 | /spec/examples.txt
27 |
28 | # Ignore test coverage info
29 | /coverage/*
30 |
31 | # Ignore RubyMine IDE configs
32 | /.idea
33 |
34 | /vendor/bundle
35 |
36 | /public/packs
37 | /public/packs-test
38 | /public/assets
39 | /node_modules
40 | /yarn-error.log
41 | yarn-debug.log*
42 | .yarn-integrity
43 |
44 | .history
45 |
46 | /config/credentials/development.key
47 | /config/credentials/test.key
48 | /config/credentials/staging.key
49 | /config/credentials/production.key
50 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require rails_helper
2 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | boilerplate-rails-api
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.7.2
2 |
--------------------------------------------------------------------------------
/.simplecov:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov-lcov'
4 |
5 | SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
6 | SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new(
7 | [
8 | SimpleCov::Formatter::HTMLFormatter,
9 | SimpleCov::Formatter::LcovFormatter
10 | ]
11 | )
12 |
13 | SimpleCov.minimum_coverage(100)
14 |
15 | if ARGV.grep(/spec.\w+/).empty?
16 | SimpleCov.start 'rails' do
17 | add_filter(%r{^/spec/})
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/.terraform/.gitignore:
--------------------------------------------------------------------------------
1 | *.terraform
2 | *.tfstate
3 | *.tfstate.backup
4 | *.tfvars
5 | */ssh_keys/*
6 | !*/ssh_keys/*.pub
7 | */ssl_certificates/*
8 | !*/ssl_certificates/.keep
9 |
--------------------------------------------------------------------------------
/.terraform/modules/ecs_cluster/autoscaling_group.tf:
--------------------------------------------------------------------------------
1 | resource "aws_autoscaling_group" "cluster-instance" {
2 | name = var.name
3 | vpc_zone_identifier = var.subnet_ids
4 | desired_capacity = var.number_of_instances
5 | min_size = var.min_number_of_instances
6 | max_size = var.max_number_of_instances
7 | health_check_grace_period = 30
8 |
9 | launch_template {
10 | id = aws_launch_template.this.id
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.terraform/modules/ecs_cluster/launch_template.tf:
--------------------------------------------------------------------------------
1 | data "aws_ami" "ecs_optimized" {
2 | owners = ["amazon"]
3 | most_recent = true
4 |
5 | filter {
6 | name = "name"
7 | values = ["amzn2-ami-ecs-hvm-2.0.20191114-x86_64-ebs"]
8 | }
9 | }
10 |
11 | data "template_file" "user_data" {
12 | template = file("${path.module}/templates/user_data.sh")
13 |
14 | vars = {
15 | cluster_name = aws_ecs_cluster.this.name
16 | swap_size = var.swap_size
17 | }
18 | }
19 |
20 | resource "aws_launch_template" "this" {
21 | name = var.name
22 | instance_type = var.instance_type
23 | key_name = var.key_pair.key_name
24 | vpc_security_group_ids = [var.cluster_instance_sg.id]
25 | image_id = data.aws_ami.ecs_optimized.id
26 | user_data = base64encode(data.template_file.user_data.rendered)
27 |
28 | iam_instance_profile {
29 | name = var.ecs_instance_iam_instance_profile.name
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.terraform/modules/ecs_cluster/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_ecs_cluster" "this" {
2 | name = var.name
3 | }
4 |
--------------------------------------------------------------------------------
/.terraform/modules/ecs_cluster/outputs.tf:
--------------------------------------------------------------------------------
1 | output "cluster" {
2 | value = aws_ecs_cluster.this
3 | }
4 |
5 | output "autoscaling_group_name" {
6 | value = aws_autoscaling_group.cluster-instance.name
7 | }
8 |
--------------------------------------------------------------------------------
/.terraform/modules/ecs_cluster/templates/user_data.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Register
4 | echo "ECS_CLUSTER=${cluster_name}" >> /etc/ecs/ecs.config
5 |
6 | # Swap tuning
7 | dd if=/dev/zero of=/swapfile bs=1M count=${swap_size}
8 | chmod 600 /swapfile
9 | mkswap /swapfile
10 | swapon /swapfile
11 | swapon -s
12 | echo "/swapfile swap swap defaults 0 0" >> /etc/fstab
13 |
--------------------------------------------------------------------------------
/.terraform/modules/ecs_cluster/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | description = "ECS cluster name"
3 | }
4 |
5 | variable "instance_type" {
6 | description = "EC2 instance type to run in ECS cluster"
7 | }
8 |
9 | variable "swap_size" {
10 | description = "Size of the swap file"
11 | }
12 |
13 | variable "number_of_instances" {
14 | description = "Number of instances in ECS cluster"
15 | }
16 |
17 | variable "min_number_of_instances" {
18 | description = "Minimum number of instances in ECS cluster"
19 | }
20 |
21 | variable "max_number_of_instances" {
22 | description = "Maximum number of instances in ECS cluster"
23 | }
24 |
25 | variable "key_pair" {
26 | description = "SSH key pair"
27 | }
28 |
29 | variable "cluster_instance_sg" {
30 | description = "Security group for ECS cluster instance"
31 | }
32 |
33 | variable "ecs_instance_iam_instance_profile" {
34 | description = "IAM instance profile with ECS instance role"
35 | }
36 |
37 | variable "subnet_ids" {
38 | description = "Subnet ids to launch ECS cluster instances in"
39 | }
40 |
--------------------------------------------------------------------------------
/.terraform/modules/global/iam.tf:
--------------------------------------------------------------------------------
1 | # ECS instance role
2 |
3 | data "aws_iam_policy_document" "ecs_instance" {
4 | statement {
5 | actions = ["sts:AssumeRole"]
6 |
7 | principals {
8 | type = "Service"
9 | identifiers = ["ec2.amazonaws.com"]
10 | }
11 | }
12 | }
13 |
14 | resource "aws_iam_role" "ecs_instance" {
15 | name = "${var.project_name_env}-ecs-cloud-instance"
16 | assume_role_policy = data.aws_iam_policy_document.ecs_instance.json
17 | }
18 |
19 | resource "aws_iam_role_policy_attachment" "ecs_instance" {
20 | role = aws_iam_role.ecs_instance.name
21 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
22 | }
23 |
24 | resource "aws_iam_instance_profile" "ecs_instance" {
25 | name = "${var.project_name_env}-ecs-cloud-instance"
26 | role = aws_iam_role.ecs_instance.name
27 | }
28 |
--------------------------------------------------------------------------------
/.terraform/modules/global/outputs.tf:
--------------------------------------------------------------------------------
1 | output "ecs_instance_iam_instance_profile" {
2 | value = aws_iam_instance_profile.ecs_instance
3 | }
4 |
5 | output "ecs_instance_iam_role" {
6 | value = aws_iam_role.ecs_instance
7 | }
8 |
9 | output "default_subnet_ids" {
10 | value = data.aws_subnet_ids.default.ids
11 | }
12 |
13 | output "default_vpc_id" {
14 | value = data.aws_vpc.default.id
15 | }
16 |
--------------------------------------------------------------------------------
/.terraform/modules/global/variables.tf:
--------------------------------------------------------------------------------
1 | variable "project_name_env" {
2 | description = "Project name with environment"
3 | }
4 |
--------------------------------------------------------------------------------
/.terraform/modules/global/vpc.tf:
--------------------------------------------------------------------------------
1 | data "aws_vpc" "default" {
2 | default = true
3 | }
4 |
5 | data "aws_subnet_ids" "default" {
6 | vpc_id = data.aws_vpc.default.id
7 | }
8 |
--------------------------------------------------------------------------------
/.terraform/modules/main/cloudwatch.tf:
--------------------------------------------------------------------------------
1 | resource "aws_cloudwatch_log_group" "this" {
2 | name = var.project_name_env
3 | retention_in_days = var.log_retention_in_days
4 | }
5 |
--------------------------------------------------------------------------------
/.terraform/modules/main/ecr.tf:
--------------------------------------------------------------------------------
1 | resource "aws_ecr_repository" "web_server" {
2 | name = "${var.project_name}/${var.environment}/web-server"
3 | }
4 |
5 | resource "aws_ecr_repository" "app" {
6 | name = "${var.project_name}/${var.environment}/server-app"
7 | }
8 |
--------------------------------------------------------------------------------
/.terraform/modules/main/keypair.tf:
--------------------------------------------------------------------------------
1 | resource "aws_key_pair" "this" {
2 | key_name = var.project_name_env
3 | public_key = file("${path.root}/ssh_keys/${var.project_name_env}.pub")
4 | }
5 |
--------------------------------------------------------------------------------
/.terraform/modules/main/outputs.tf:
--------------------------------------------------------------------------------
1 | output "ecr_repositories" {
2 | value = {
3 | web_server = aws_ecr_repository.web_server
4 | app = aws_ecr_repository.app
5 | }
6 | }
7 |
8 | output "aws_key_pair" {
9 | value = aws_key_pair.this
10 | }
11 |
12 | output "aws_cloudwatch_log_group" {
13 | value = aws_cloudwatch_log_group.this
14 | }
15 |
16 | output "bucket_name" {
17 | value = aws_s3_bucket.assets.bucket
18 | }
19 |
--------------------------------------------------------------------------------
/.terraform/modules/main/provider.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | version = "~> 2.34"
3 | region = var.region
4 | }
5 |
6 | provider "template" {
7 | version = "~> 2.1"
8 | }
9 |
--------------------------------------------------------------------------------
/.terraform/modules/main/s3.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "assets" {
2 | bucket = "${var.project_name_env}-assets"
3 | }
4 |
5 | # S3 user
6 |
7 | resource "aws_iam_user" "s3_user" {
8 | name = "${var.project_name_env}-s3-bucket-user"
9 | }
10 |
11 | data "aws_iam_policy_document" "s3_user" {
12 | statement {
13 | actions = [
14 | "s3:ListBucket",
15 | "s3:GetBucketLocation",
16 | "s3:ListBucketMultipartUploads"
17 | ]
18 | resources = [
19 | "arn:aws:s3:::${aws_s3_bucket.assets.bucket}"
20 | ]
21 | }
22 |
23 | statement {
24 | actions = [
25 | "s3:PutObject",
26 | "s3:GetObject",
27 | "s3:DeleteObject",
28 | "s3:ListMultipartUploadParts",
29 | "s3:AbortMultipartUpload"
30 | ]
31 | resources = [
32 | "arn:aws:s3:::${aws_s3_bucket.assets.bucket}/*"
33 | ]
34 | }
35 | }
36 |
37 | resource "aws_iam_policy" "s3_user" {
38 | name = "${var.project_name_env}-s3-bucket-user"
39 | policy = data.aws_iam_policy_document.s3_user.json
40 | }
41 |
42 | resource "aws_iam_user_policy_attachment" "s3_user" {
43 | user = aws_iam_user.s3_user.name
44 | policy_arn = aws_iam_policy.s3_user.arn
45 | }
46 |
--------------------------------------------------------------------------------
/.terraform/modules/main/variables.tf:
--------------------------------------------------------------------------------
1 | variable "region" {
2 | description = "AWS Region"
3 | }
4 |
5 | variable "project_name" {
6 | description = "Project name that will be visible in AWS resources"
7 | }
8 |
9 | variable "environment" {
10 | description = "Application environment"
11 | }
12 |
13 | variable "project_name_env" {
14 | description = "Project name with environment that will be visible in AWS resources names"
15 | }
16 |
17 | variable "log_retention_in_days" {
18 | description = "CloudWatch Logs retention period in days"
19 | }
20 |
--------------------------------------------------------------------------------
/.terraform/modules/main/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 0.12.17"
3 | }
4 |
--------------------------------------------------------------------------------
/.terraform/production/acm.tf:
--------------------------------------------------------------------------------
1 | resource "aws_acm_certificate" "this" {
2 | domain_name = module.variables.server_name
3 | validation_method = "DNS"
4 |
5 | tags = {
6 | Environment = "production"
7 | }
8 |
9 | lifecycle {
10 | create_before_destroy = true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.terraform/production/alb.tf:
--------------------------------------------------------------------------------
1 | resource "aws_lb" "this" {
2 | name = module.variables.project_name_env
3 | subnets = module.global.default_subnet_ids
4 | load_balancer_type = "application"
5 | security_groups = [aws_security_group.load_balancer.id]
6 | enable_deletion_protection = true
7 | }
8 |
9 | resource "aws_lb_listener" "http" {
10 | load_balancer_arn = aws_lb.this.arn
11 | port = "80"
12 | protocol = "HTTP"
13 |
14 |
15 | default_action {
16 | type = "forward"
17 | target_group_arn = aws_lb_target_group.this.arn
18 | }
19 | }
20 |
21 | resource "aws_lb_target_group" "this" {
22 | name = aws_lb.this.name
23 | port = "8080"
24 | protocol = "HTTP"
25 | vpc_id = module.global.default_vpc_id
26 | deregistration_delay = 30
27 |
28 | health_check {
29 | path = "/health_check"
30 | port = "80"
31 | protocol = "HTTP"
32 | timeout = 5
33 | interval = 30
34 | unhealthy_threshold = 2
35 | healthy_threshold = 2
36 | matcher = "200"
37 | }
38 |
39 | stickiness {
40 | type = "lb_cookie"
41 | enabled = true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.terraform/production/backend.tf:
--------------------------------------------------------------------------------
1 | # Backend can't use variables, values have to be hardcoded
2 |
3 | terraform {
4 | backend "remote" {
5 | organization = "boilerplate-api"
6 |
7 | workspaces {
8 | name = "production"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.terraform/production/ecs_application.tf:
--------------------------------------------------------------------------------
1 | # Task definition
2 |
3 | data "template_file" "container_definitions" {
4 | template = file("${path.module}/templates/container_definitions.json")
5 |
6 | vars = {
7 | project_name = module.variables.project_name
8 | environment = module.variables.environment
9 | server_name = module.variables.server_name
10 | app_port = module.variables.app_port
11 |
12 | web_server_ecr_repo = module.main.ecr_repositories.web_server.repository_url
13 | app_ecr_repo = module.main.ecr_repositories.app.repository_url
14 |
15 | log_group = module.main.aws_cloudwatch_log_group.name
16 | log_region = module.variables.region
17 |
18 | db_username = var.db_username
19 | db_password = var.db_password
20 | }
21 | }
22 |
23 | resource "aws_ecs_task_definition" "this" {
24 | family = module.variables.project_name_env
25 | network_mode = "bridge"
26 | cpu = module.variables.task_cpu
27 | memory = module.variables.task_memory
28 |
29 | container_definitions = data.template_file.container_definitions.rendered
30 |
31 | volume {
32 | name = "public"
33 | }
34 | }
35 |
36 | # Service
37 |
38 | resource "aws_ecs_service" "this" {
39 | name = module.variables.project_name_env
40 | cluster = module.ecs_cluster.cluster.id
41 | task_definition = aws_ecs_task_definition.this.arn
42 | desired_count = module.variables.min_task_count_application
43 | deployment_minimum_healthy_percent = module.variables.deployment_minimum_healthy_percent_application
44 | deployment_maximum_percent = module.variables.deployment_maximum_percent
45 |
46 | load_balancer {
47 | container_name = "web-server"
48 | container_port = 8080
49 | target_group_arn = aws_lb_target_group.this.arn
50 | }
51 |
52 | depends_on = [
53 | module.global.ecs_instance_iam_role
54 | ]
55 |
56 | lifecycle {
57 | ignore_changes = [desired_count]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.terraform/production/ecs_worker.tf:
--------------------------------------------------------------------------------
1 | # Task definition
2 |
3 | data "template_file" "worker_definitions" {
4 | template = file("${path.module}/templates/worker_definitions.json")
5 |
6 | vars = {
7 | project_name = module.variables.project_name
8 | environment = module.variables.environment
9 |
10 | app_ecr_repo = module.main.ecr_repositories.app.repository_url
11 |
12 | log_group = module.main.aws_cloudwatch_log_group.name
13 | log_region = module.variables.region
14 | }
15 | }
16 |
17 | resource "aws_ecs_task_definition" "worker_task_definition" {
18 | family = "${module.variables.project_name_env}-worker"
19 | network_mode = "bridge"
20 | cpu = module.variables.task_cpu
21 | memory = module.variables.task_memory
22 |
23 | volume {
24 | name = "public"
25 | }
26 |
27 | container_definitions = data.template_file.worker_definitions.rendered
28 | }
29 |
30 | # Service
31 |
32 | resource "aws_ecs_service" "worker-service" {
33 | name = "${module.variables.project_name_env}-worker"
34 | cluster = module.ecs_cluster.cluster.id
35 | task_definition = aws_ecs_task_definition.worker_task_definition.arn
36 | desired_count = module.variables.min_task_count_worker
37 | deployment_minimum_healthy_percent = module.variables.deployment_minimum_healthy_percent_worker
38 | deployment_maximum_percent = module.variables.deployment_maximum_percent
39 |
40 | depends_on = [
41 | module.global.ecs_instance_iam_role
42 | ]
43 |
44 | lifecycle {
45 | ignore_changes = [desired_count]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.terraform/production/elasticache.tf:
--------------------------------------------------------------------------------
1 | resource "aws_elasticache_cluster" "this" {
2 | cluster_id = module.variables.project_name_env
3 | engine = module.variables.elasticache_engine
4 | engine_version = module.variables.elasticache_engine_version
5 | node_type = module.variables.elasticache_node_type
6 | num_cache_nodes = 1
7 | security_group_ids = [aws_security_group.redis_server.id]
8 | }
9 |
--------------------------------------------------------------------------------
/.terraform/production/main.tf:
--------------------------------------------------------------------------------
1 | module "variables" {
2 | source = "../modules/variables"
3 | environment = "production"
4 | }
5 |
6 | module "main" {
7 | source = "../modules/main"
8 |
9 | region = module.variables.region
10 | project_name = module.variables.project_name
11 | environment = module.variables.environment
12 | project_name_env = module.variables.project_name_env
13 |
14 | log_retention_in_days = module.variables.log_retention_in_days
15 | }
16 |
17 | module "global" {
18 | source = "../modules/global"
19 | project_name_env = module.variables.project_name_env
20 | }
21 |
22 | module "ecs_cluster" {
23 | source = "../modules/ecs_cluster"
24 |
25 | name = module.variables.project_name_env
26 |
27 | instance_type = module.variables.instance_type
28 | swap_size = module.variables.swap_size
29 |
30 | number_of_instances = module.variables.min_task_count_cluster
31 | min_number_of_instances = module.variables.min_task_count_cluster
32 | max_number_of_instances = module.variables.max_task_count_cluster
33 |
34 | key_pair = module.main.aws_key_pair
35 | subnet_ids = module.global.default_subnet_ids
36 | cluster_instance_sg = aws_security_group.cluster_instance
37 | ecs_instance_iam_instance_profile = module.global.ecs_instance_iam_instance_profile
38 | }
39 |
--------------------------------------------------------------------------------
/.terraform/production/rds.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | db_identifier = module.variables.project_name_env
3 | }
4 |
5 | resource "aws_db_instance" "this" {
6 | identifier = local.db_identifier
7 | name = replace(local.db_identifier, "-", "_")
8 | engine = module.variables.rds_engine
9 | engine_version = module.variables.rds_engine_version
10 | instance_class = module.variables.rds_instance_class
11 | allocated_storage = module.variables.rds_allocated_storage
12 | storage_type = "gp2"
13 | username = replace(var.db_username, "-", "")
14 | password = var.db_password
15 | backup_retention_period = module.variables.rds_backup_retention_period
16 | performance_insights_enabled = true
17 | deletion_protection = true
18 | multi_az = false
19 | vpc_security_group_ids = [aws_security_group.db_server.id]
20 | }
21 |
--------------------------------------------------------------------------------
/.terraform/production/sg.tf:
--------------------------------------------------------------------------------
1 | resource "aws_security_group" "load_balancer" {
2 | name = "${module.variables.project_name_env}-load-balancer"
3 |
4 | ingress {
5 | from_port = 80
6 | to_port = 80
7 | protocol = "tcp"
8 | cidr_blocks = ["0.0.0.0/0"]
9 | }
10 |
11 | ingress {
12 | from_port = 443
13 | to_port = 443
14 | protocol = "tcp"
15 | cidr_blocks = ["0.0.0.0/0"]
16 | }
17 |
18 | egress {
19 | from_port = 0
20 | to_port = 0
21 | protocol = -1
22 | cidr_blocks = ["0.0.0.0/0"]
23 | }
24 | }
25 |
26 | resource "aws_security_group" "db_server" {
27 | name = "${module.variables.project_name_env}-db-server"
28 |
29 | ingress {
30 | from_port = 5432
31 | to_port = 5432
32 | protocol = "tcp"
33 | security_groups = [aws_security_group.cluster_instance.id]
34 | }
35 | }
36 |
37 | resource "aws_security_group" "redis_server" {
38 | name = "${module.variables.project_name_env}-redis-server"
39 |
40 | ingress {
41 | from_port = 6379
42 | to_port = 6379
43 | protocol = "tcp"
44 | security_groups = [aws_security_group.cluster_instance.id]
45 | }
46 | }
47 |
48 | resource "aws_security_group" "cluster_instance" {
49 | name = "${module.variables.project_name_env}-cluster-instance"
50 |
51 | ingress {
52 | from_port = 80
53 | to_port = 80
54 | protocol = "tcp"
55 | cidr_blocks = ["0.0.0.0/0"]
56 | }
57 |
58 | ingress {
59 | from_port = 443
60 | to_port = 443
61 | protocol = "tcp"
62 | cidr_blocks = ["0.0.0.0/0"]
63 | }
64 |
65 | ingress {
66 | from_port = 22
67 | to_port = 22
68 | protocol = "tcp"
69 | cidr_blocks = ["0.0.0.0/0"]
70 | }
71 |
72 | egress {
73 | from_port = 0
74 | to_port = 0
75 | protocol = -1
76 | cidr_blocks = ["0.0.0.0/0"]
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/.terraform/production/ssh_keys/boilerplate-api-production.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNmOmLNxo8GCqxzsc0PBPLIOEy3z+c5XXUBSWLPFQUn2xsRr2WVjcGBsxy4wOLc45Sna+5WWV2w4ggihNx/sij7ytKjkSTjA2V4DhNucVT4zNDpdE88mJVyt1ETfOX3S5m3ii9+Vf2rU22jpr5OP23qBFPgUPFn8lXcHYami0rrC9G3Wqpz23ER9oj7XXV/efW5XgZZQTL8DyF4iY+pvBjtAMKw0ZdQljKrUXw0MM7o8opFCuFLoxWoPcRgJZjxzbNUBjnSqoImMf2vjPh5bjadDMtdB1v0BL3kKjEnxOIvOwr5blowiMoF97h0Cv9ZJjhYlWfcO0FEJZQevl+oM8j vanechka@ivanich
2 |
--------------------------------------------------------------------------------
/.terraform/production/templates/worker_definitions.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "worker",
4 | "image": "${app_ecr_repo}:${environment}",
5 | "command": ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"],
6 | "environment": [
7 | {
8 | "name": "RAILS_ENV",
9 | "value": "${environment}"
10 | },
11 | {
12 | "name": "RACK_ENV",
13 | "value": "${environment}"
14 | },
15 | {
16 | "name": "RAILS_LOG_TO_STDOUT",
17 | "value": "true"
18 | }
19 | ],
20 | "mountPoints": [
21 | {
22 | "containerPath": "/home/www/${project_name}/public",
23 | "sourceVolume": "public"
24 | }
25 | ],
26 | "portMappings": [],
27 | "healthCheck": {
28 | "command": [
29 | "CMD-SHELL",
30 | "ps ax | grep -v grep | grep sidekiq || exit 1"
31 | ],
32 | "interval": 30,
33 | "retries": 3,
34 | "timeout": 5
35 | },
36 | "logConfiguration": {
37 | "logDriver": "awslogs",
38 | "options": {
39 | "awslogs-group": "${log_group}",
40 | "awslogs-region": "${log_region}",
41 | "awslogs-stream-prefix": "${environment}"
42 | }
43 | }
44 | }
45 | ]
46 |
--------------------------------------------------------------------------------
/.terraform/production/variables.tf:
--------------------------------------------------------------------------------
1 | variable "db_username" {
2 | description = "Database username"
3 | }
4 |
5 | variable "db_password" {
6 | description = "Database password"
7 | }
8 |
--------------------------------------------------------------------------------
/.terraform/staging/backend.tf:
--------------------------------------------------------------------------------
1 | # Backend can't use variables, values have to be hardcoded
2 |
3 | terraform {
4 | backend "remote" {
5 | organization = "boilerplate-api"
6 |
7 | workspaces {
8 | name = "staging"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.terraform/staging/main.tf:
--------------------------------------------------------------------------------
1 | module "variables" {
2 | source = "../modules/variables"
3 | environment = "staging"
4 | }
5 |
6 | module "main" {
7 | source = "../modules/main"
8 |
9 | region = module.variables.region
10 | project_name = module.variables.project_name
11 | environment = module.variables.environment
12 | project_name_env = module.variables.project_name_env
13 |
14 | log_retention_in_days = module.variables.log_retention_in_days
15 | }
16 |
17 | module "global" {
18 | source = "../modules/global"
19 | project_name_env = module.variables.project_name_env
20 | }
21 |
22 | module "ecs_cluster" {
23 | source = "../modules/ecs_cluster"
24 |
25 | name = module.variables.project_name_env
26 |
27 | instance_type = module.variables.instance_type
28 | swap_size = module.variables.swap_size
29 |
30 | number_of_instances = module.variables.min_task_count_application
31 | min_number_of_instances = module.variables.min_task_count_application
32 | max_number_of_instances = module.variables.max_task_count_application
33 |
34 | key_pair = module.main.aws_key_pair
35 | subnet_ids = module.global.default_subnet_ids
36 | cluster_instance_sg = aws_security_group.cluster_instance
37 | ecs_instance_iam_instance_profile = module.global.ecs_instance_iam_instance_profile
38 | }
39 |
--------------------------------------------------------------------------------
/.terraform/staging/sg.tf:
--------------------------------------------------------------------------------
1 | resource "aws_security_group" "load_balancer" {
2 | name = "${module.variables.project_name_env}-load-balancer"
3 |
4 | ingress {
5 | from_port = 80
6 | to_port = 80
7 | protocol = "tcp"
8 | cidr_blocks = ["0.0.0.0/0"]
9 | }
10 |
11 | ingress {
12 | from_port = 443
13 | to_port = 443
14 | protocol = "tcp"
15 | cidr_blocks = ["0.0.0.0/0"]
16 | }
17 |
18 | egress {
19 | from_port = 0
20 | to_port = 0
21 | protocol = -1
22 | cidr_blocks = ["0.0.0.0/0"]
23 | }
24 | }
25 |
26 | resource "aws_security_group" "cluster_instance" {
27 | name = "${module.variables.project_name_env}-cluster-instance"
28 |
29 | ingress {
30 | from_port = 80
31 | to_port = 80
32 | protocol = "tcp"
33 | cidr_blocks = ["0.0.0.0/0"]
34 | }
35 |
36 | ingress {
37 | from_port = 443
38 | to_port = 443
39 | protocol = "tcp"
40 | cidr_blocks = ["0.0.0.0/0"]
41 | }
42 |
43 | ingress {
44 | from_port = 22
45 | to_port = 22
46 | protocol = "tcp"
47 | cidr_blocks = ["0.0.0.0/0"]
48 | }
49 |
50 | egress {
51 | from_port = 0
52 | to_port = 0
53 | protocol = -1
54 | cidr_blocks = ["0.0.0.0/0"]
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.terraform/staging/ssh_keys/boilerplate-api-staging.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNmOmLNxo8GCqxzsc0PBPLIOEy3z+c5XXUBSWLPFQUn2xsRr2WVjcGBsxy4wOLc45Sna+5WWV2w4ggihNx/sij7ytKjkSTjA2V4DhNucVT4zNDpdE88mJVyt1ETfOX3S5m3ii9+Vf2rU22jpr5OP23qBFPgUPFn8lXcHYami0rrC9G3Wqpz23ER9oj7XXV/efW5XgZZQTL8DyF4iY+pvBjtAMKw0ZdQljKrUXw0MM7o8opFCuFLoxWoPcRgJZjxzbNUBjnSqoImMf2vjPh5bjadDMtdB1v0BL3kKjEnxOIvOwr5blowiMoF97h0Cv9ZJjhYlWfcO0FEJZQevl+oM8j vanechka@ivanich
2 |
--------------------------------------------------------------------------------
/.terraform/staging/ssl_certificates/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/.terraform/staging/ssl_certificates/.keep
--------------------------------------------------------------------------------
/.terraform/staging/variables.tf:
--------------------------------------------------------------------------------
1 | variable "db_username" {
2 | description = "Database username"
3 | }
4 |
5 | variable "db_password" {
6 | description = "Database password"
7 | }
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative 'config/application'
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/app/admin/admin_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ActiveAdmin.register AdminUser do
4 | permit_params :email, :password, :password_confirmation
5 |
6 | index do
7 | selectable_column
8 | id_column
9 | column :email
10 | column :current_sign_in_at
11 | column :sign_in_count
12 | column :created_at
13 | actions
14 | end
15 |
16 | filter :email
17 | filter :current_sign_in_at
18 | filter :sign_in_count
19 | filter :created_at
20 |
21 | form do |f|
22 | f.inputs do
23 | f.input :email
24 | f.input :password
25 | f.input :password_confirmation
26 | end
27 | f.actions
28 | end
29 |
30 | show do
31 | attributes_table do
32 | row :id
33 | row :created_at
34 | row :email
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/app/admin/dashboard.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ActiveAdmin.register_page 'Dashboard' do
4 | menu priority: 1, label: proc { I18n.t('active_admin.dashboard') }
5 |
6 | content title: proc { I18n.t('active_admin.dashboard') } do
7 | div class: 'blank_slate_container', id: 'dashboard_default_message' do
8 | span class: 'blank_slate' do
9 | span I18n.t('active_admin.dashboard_welcome.welcome')
10 | small I18n.t('active_admin.dashboard_welcome.call_to_action')
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/app/assets/config/manifest.js
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # module ApplicationCable
4 | # class Channel < ActionCable::Channel::Base
5 | # end
6 | # end
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # module ApplicationCable
4 | # class Connection < ActionCable::Connection::Base
5 | # end
6 | # end
7 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/contract/filtering_pre_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Contract
4 | FilteringPreValidation = Dry::Validation.Schema do
5 | required(:filter).filled(:hash?)
6 | optional(:match).filled(:str?, included_in?: JsonApi::Filtering::OPERATORS)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/contract/filtering_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Contract
4 | FilteringValidation = Dry::Validation.Schema do
5 | configure do
6 | config.type_specs = true
7 | option :available_filtering_columns
8 | option :column_type_dict
9 |
10 | def filtering_column_valid?(filter)
11 | available_filtering_columns.include?(filter.column)
12 | end
13 |
14 | def filtering_predicate_valid?(filter)
15 | column = column_type_dict[filter.column]
16 | JsonApi::Filtering::PREDICATES[column].include?(filter.predicate)
17 | end
18 |
19 | def filtering_value_valid?(filter)
20 | column = column_type_dict[filter.column]
21 | Types::JsonApi::TypeByColumn.call(column).try(filter.value).success?
22 | end
23 |
24 | def filters_uniq?(filters)
25 | filters = filters.map(&:column)
26 | filters.eql?(filters.uniq)
27 | end
28 | end
29 |
30 | required(:filter, Types::JsonApi::Filter).filled(:array?, :filters_uniq?) do
31 | each(:filtering_column_valid?, :filtering_predicate_valid?, :filtering_value_valid?)
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/contract/inclusion_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Contract
4 | InclusionValidation = Dry::Validation.Schema do
5 | configure do
6 | config.type_specs = true
7 | option :available_inclusion_options
8 |
9 | def inclusion_params_uniq?(jsonapi_inclusion_params)
10 | jsonapi_inclusion_params.eql?(jsonapi_inclusion_params.uniq)
11 | end
12 |
13 | def inclusion_params_valid?(jsonapi_inclusion_params)
14 | jsonapi_inclusion_params.difference(available_inclusion_options).empty?
15 | end
16 | end
17 |
18 | required(:include, Types::JsonApi::Include).filled(:inclusion_params_uniq?, :inclusion_params_valid?)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/contract/pagination_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Contract
4 | PaginationValidation = Dry::Validation.Form do
5 | optional(:page).maybe(:hash?) do
6 | schema do
7 | optional(:number).maybe(:int?, gteq?: JsonApi::Pagination::MINIMAL_VALUE)
8 | optional(:size).maybe(:int?, gteq?: JsonApi::Pagination::MINIMAL_VALUE)
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/contract/sorting_pre_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Contract
4 | SortingPreValidation = Dry::Validation.Schema do
5 | required(:sort).filled(:str?)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/contract/sorting_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Contract
4 | SortingValidation = Dry::Validation.Schema do
5 | configure do
6 | config.type_specs = true
7 | option :available_sortable_columns
8 |
9 | def sort_params_uniq?(jsonapi_sort_params)
10 | jsonapi_sort_params = jsonapi_sort_params.map(&:column)
11 | jsonapi_sort_params.eql?(jsonapi_sort_params.uniq)
12 | end
13 |
14 | def sort_params_valid?(jsonapi_sort_params)
15 | jsonapi_sort_params.all? do |jsonapi_sort_parameter|
16 | available_sortable_columns.include?(jsonapi_sort_parameter.column)
17 | end
18 | end
19 | end
20 |
21 | required(:sort, Types::JsonApi::Sort) { sort_params_uniq? & sort_params_valid? }
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/operation/filtering.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Operation
4 | class Filtering < ApplicationOperation
5 | step :filter_params_passed?, Output(:failure) => End(:success)
6 | step Macro::Contract::Schema(Api::V1::Lib::Contract::FilteringPreValidation, name: :uri_query)
7 | step Contract::Validate(name: :uri_query)
8 | step :matcher_options
9 | step :set_validation_dependencies
10 | step Macro::Contract::Schema(
11 | Api::V1::Lib::Contract::FilteringValidation,
12 | inject: %i[available_filtering_columns column_type_dict],
13 | name: :uri_query
14 | )
15 | step Contract::Validate(name: :uri_query), id: :filtering_validation
16 | step :filter_options
17 |
18 | def filter_params_passed?(_ctx, params:, **)
19 | params[:filter]
20 | end
21 |
22 | def matcher_options(ctx, **)
23 | ctx[:matcher_options] = ctx['contract.uri_query'].match || JsonApi::Filtering::Operators::MATCH_ALL
24 | end
25 |
26 | def set_validation_dependencies(ctx, available_columns:, **)
27 | ctx[:available_filtering_columns] = available_columns.select(&:filterable).map(&:name).to_set
28 | ctx[:column_type_dict] = available_columns.map { |column| [column.name, column.type] }.to_h
29 | end
30 |
31 | def filter_options(ctx, **)
32 | ctx[:filter_options] = ctx['contract.uri_query'].filter.map(&:to_h)
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/operation/inclusion.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Operation
4 | class Inclusion < ApplicationOperation
5 | step :inclusion_query_param_passed?, Output(:failure) => End(:success)
6 | step Macro::Contract::Schema(
7 | Api::V1::Lib::Contract::InclusionValidation,
8 | name: :uri_query,
9 | inject: %i[available_inclusion_options]
10 | )
11 | step Contract::Validate(name: :uri_query)
12 | step Macro::Assign(to: :inclusion_options, path: %w[contract.uri_query include])
13 |
14 | def inclusion_query_param_passed?(_ctx, params:, **)
15 | params[:include]
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/operation/pagination.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Operation
4 | class Pagination < ApplicationOperation
5 | step Macro::Inject(paginator: 'services.pagy')
6 | step Macro::Contract::Schema(Api::V1::Lib::Contract::PaginationValidation, name: :uri_query)
7 | step Contract::Validate(name: :uri_query), fail_fast: true
8 | step :pagy
9 | step :valid_page?
10 | fail Macro::AddContractError(name: :uri_query, page: [:number, [I18n.t('errors.pagination_overflow')]])
11 |
12 | def pagy(ctx, paginator:, model:, **)
13 | ctx[:pagy], ctx[:model] =
14 | paginator.call(
15 | model,
16 | page: ctx['contract.uri_query'].page.try(:number),
17 | items: ctx['contract.uri_query'].page.try(:size)
18 | )
19 | end
20 |
21 | def valid_page?(_ctx, pagy:, **)
22 | !pagy.overflow?
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/operation/perform_filtering.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Operation
4 | class PerformFiltering < ApplicationOperation
5 | step :filter_options_passed?, Output(:failure) => End(:success)
6 | pass :build_filter_query
7 | pass :filter_relation
8 |
9 | def filter_options_passed?(ctx, **)
10 | ctx[:filter_options]
11 | end
12 |
13 | def build_filter_query(ctx, filter_options:, **)
14 | ctx[:filter_query] = filter_options.map do |option|
15 | value = option[:value]
16 | value = value.split(',') if value.is_a?(String)
17 | value = value.first if value.is_a?(Array) && value.one?
18 |
19 | {
20 | "#{option[:column]}_#{option[:predicate]}": value
21 | }
22 | end.inject(:merge)
23 | end
24 |
25 | def filter_relation(ctx, filter_query:, relation:, **)
26 | ctx[:relation] = relation.ransack(filter_query).result
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/operation/perform_ordering.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Operation
4 | class PerformOrdering < ApplicationOperation
5 | step :order_options_passed?, Output(:failure) => End(:success)
6 | pass :order_relation
7 |
8 | def order_options_passed?(ctx, **)
9 | ctx[:order_options]
10 | end
11 |
12 | def order_relation(ctx, order_options:, relation:, **)
13 | ctx[:relation] = relation.reorder(order_options)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/operation/sorting.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Operation
4 | class Sorting < ApplicationOperation
5 | step :sort_params_passed?, Output(:failure) => End(:success)
6 | step Macro::Contract::Schema(Api::V1::Lib::Contract::SortingPreValidation, name: :uri_query)
7 | step Contract::Validate(name: :uri_query)
8 | step :set_validation_dependencies
9 | step Macro::Contract::Schema(
10 | Api::V1::Lib::Contract::SortingValidation,
11 | inject: %i[available_sortable_columns],
12 | name: :uri_query
13 | )
14 | step Contract::Validate(name: :uri_query), id: :sorting_validation
15 | step :order_options
16 |
17 | def sort_params_passed?(_ctx, params:, **)
18 | params[:sort]
19 | end
20 |
21 | def set_validation_dependencies(ctx, available_columns:, **)
22 | ctx[:available_sortable_columns] = available_columns.select(&:sortable).map(&:name).to_set
23 | end
24 |
25 | def order_options(ctx, **)
26 | ctx[:order_options] = ctx['contract.uri_query'].sort.map do |jsonapi_sort_parameter|
27 | { jsonapi_sort_parameter.column.to_sym => jsonapi_sort_parameter.order.to_sym }
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/serializer/account.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Serializer
4 | class Account < ApplicationSerializer
5 | set_type :account
6 | attributes :email, :created_at
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/lib/service/json_api.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Lib::Service::JsonApi
4 | Column = Struct.new(:name, :type, :sortable, :filterable, keyword_init: true) do
5 | def initialize(name:, type: :string, **args)
6 | super
7 | end
8 | end
9 |
10 | module ColumnsBuilder
11 | def self.call(*columns)
12 | columns.map { |column| Api::V1::Lib::Service::JsonApi::Column.new(column) }
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/account/profiles/operation/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Account::Profiles::Operation
4 | class Show < ApplicationOperation
5 | step Macro::Assign(to: :model, path: %i[current_account user])
6 | step Macro::Assign(to: :available_inclusion_options, value: %w[account])
7 | step Subprocess(Api::V1::Lib::Operation::Inclusion)
8 | step Macro::Renderer(serializer: Api::V1::Users::Account::Profiles::Serializer::Show)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/account/profiles/serializer/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Account::Profiles::Serializer
4 | class Show < ApplicationSerializer
5 | set_type 'user-profile'
6 | attributes :name
7 | belongs_to :account, serializer: Api::V1::Lib::Serializer::Account
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/contract/decrypt_email_token_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Contract
4 | DecryptEmailTokenValidation = Dry::Validation.Schema do
5 | required(:email_token).filled(:str?)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/operation/check_email_token_redis_equality.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Operation
4 | class CheckEmailTokenRedisEquality < ApplicationOperation
5 | step Macro::Inject(redis: 'adapters.redis')
6 | step :tokens_eql?
7 | fail Macro::AddContractError(base: 'errors.email_token.already_used')
8 |
9 | def tokens_eql?(_ctx, redis:, email_token:, **)
10 | email_token.eql?(redis.find_token(email_token))
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/operation/decrypt_email_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Operation
4 | class DecryptEmailToken < ApplicationOperation
5 | step Macro::Inject(jwt: 'services.email_token')
6 | step Macro::Contract::Schema(Api::V1::Users::Lib::Contract::DecryptEmailTokenValidation)
7 | step Contract::Validate(), fail_fast: true
8 | step Macro::Assign(to: :email_token, path: %w[contract.default email_token])
9 | step :set_payload
10 | fail Macro::AddContractError(base: 'errors.verification.invalid_email_token'), fail_fast: true
11 | step :set_model
12 | fail Macro::Semantic(failure: :not_found)
13 |
14 | def set_payload(ctx, jwt:, email_token:, **)
15 | ctx[:payload] = jwt.read(email_token)
16 | end
17 |
18 | def set_model(ctx, payload:, **)
19 | ctx[:model] = Account.find_by(id: payload[:account_id])
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/service/email_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Service
4 | class EmailToken
5 | ERROR_MESSAGE = 'Secret key is not assigned'
6 | TOKEN_LIFETIME = 24.hours
7 |
8 | class << self
9 | def create(payload, exp = TOKEN_LIFETIME.from_now.to_i)
10 | check_secret_key
11 | payload[:exp] = exp
12 | JWT.encode(payload, Constants::Shared::HMAC_SECRET)
13 | end
14 |
15 | def read(token)
16 | check_secret_key
17 | body = JWT.decode(token, Constants::Shared::HMAC_SECRET).first rescue false
18 | return body unless body
19 |
20 | HashWithIndifferentAccess.new(body)
21 | end
22 |
23 | private
24 |
25 | def check_secret_key
26 | raise ERROR_MESSAGE unless Constants::Shared::HMAC_SECRET
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/service/redis_adapter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Service
4 | class RedisAdapter
5 | attr_reader :storage, :token_name
6 |
7 | class << self
8 | def push_token(token)
9 | new(token).push
10 | end
11 |
12 | def find_token(token)
13 | new(token).find
14 | end
15 |
16 | def delete_token(token)
17 | new(token).delete
18 | end
19 | end
20 |
21 | def initialize(token)
22 | @storage = Redis.current
23 | @token = token
24 | payload = Api::V1::Users::Lib::Service::EmailToken.read(token) || {}
25 | @token_name = Api::V1::Users::Lib::Service::TokenNamespace.call(payload[:namespace], payload[:account_id])
26 | @token_exp = payload[:exp]
27 | end
28 |
29 | def push
30 | storage.setex(token_name, @token_exp, @token) if @token_exp
31 | end
32 |
33 | def find
34 | storage.get(token_name)
35 | end
36 |
37 | def delete
38 | storage.del(token_name)
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/service/session_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Service::SessionToken
4 | class << self
5 | def create(account_id:, namespace: nil)
6 | options =
7 | namespace ? { namespace: Api::V1::Users::Lib::Service::TokenNamespace.call(namespace, account_id) } : {}
8 | JWTSessions::Session.new(
9 | payload: { account_id: account_id, **options },
10 | refresh_payload: { account_id: account_id, **options }
11 | ).login
12 | end
13 |
14 | def destroy(refresh_token:)
15 | JWTSessions::Session.new.flush_by_token(refresh_token)
16 | end
17 |
18 | def destroy_all(namespace:)
19 | JWTSessions::Session.new(namespace: namespace).flush_namespaced
20 | end
21 |
22 | def refresh(payload:, refresh_token:)
23 | session = JWTSessions::Session.new(payload: payload)
24 | session.refresh(refresh_token) do
25 | session.flush_by_token(refresh_token)
26 | return false
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/lib/service/token_namespace.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Lib::Service
4 | module TokenNamespace
5 | def self.call(namespace, entity_id)
6 | "#{namespace}-#{entity_id}"
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/registrations/contract/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Registrations::Contract
4 | class Create < ApplicationContract
5 | property :email
6 | property :password
7 | property :password_confirmation, virtual: true
8 |
9 | validation :default do
10 | configure { config.namespace = :user_password }
11 |
12 | required(:email).filled(
13 | :str?,
14 | max_size?:
15 | Constants::Shared::EMAIL_MAX_LENGTH,
16 | format?: Constants::Shared::EMAIL_REGEX
17 | )
18 | required(:password).filled(:str?)
19 | required(:password_confirmation).filled(:str?)
20 |
21 | required(:password).filled(
22 | :str?,
23 | min_size?: Constants::Shared::PASSWORD_MIN_SIZE,
24 | format?: Constants::Shared::PASSWORD_REGEX
25 | ).confirmation
26 | end
27 |
28 | validation :email, if: :default do
29 | configure do
30 | def email_uniq?(value)
31 | !Account.exists?(email: value)
32 | end
33 | end
34 |
35 | required(:email, &:email_uniq?)
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/registrations/operation/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Registrations::Operation
4 | class Create < ApplicationOperation
5 | step Macro::Inject(jwt: 'services.email_token', worker: Api::V1::Users::Registrations::Worker::EmailConfirmation)
6 | step Model(Account, :new)
7 | step Contract::Build(constant: Api::V1::Users::Registrations::Contract::Create, name: 'registration')
8 | step Contract::Validate(name: 'registration')
9 | step Contract::Persist(name: 'registration')
10 | fail Macro::Semantic(failure: :bad_request)
11 | step :set_email_token
12 | step :send_confirmation_link
13 | step Macro::Renderer(serializer: Api::V1::Lib::Serializer::Account)
14 | pass Macro::Semantic(success: :created)
15 |
16 | def set_email_token(ctx, jwt:, model:, **)
17 | ctx[:email_token] = jwt.create(account_id: model.id)
18 | end
19 |
20 | def send_confirmation_link(_ctx, worker:, model:, email_token:, **)
21 | worker.perform_async(
22 | email: model.email,
23 | token: email_token,
24 | user_verification_path: Rails.application.config.user_verification_path
25 | )
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/registrations/worker/email_confirmation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Registrations::Worker
4 | class EmailConfirmation < ApplicationWorker
5 | def perform(email:, token:, user_verification_path:)
6 | UserMailer.confirmation(email, token, user_verification_path).deliver_now
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/reset_passwords/contract/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::ResetPasswords::Contract
4 | Create = Dry::Validation.Schema do
5 | required(:email).filled(
6 | :str?,
7 | max_size?: Constants::Shared::EMAIL_MAX_LENGTH,
8 | format?: Constants::Shared::EMAIL_REGEX
9 | )
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/reset_passwords/contract/update.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::ResetPasswords::Contract
4 | class Update < ApplicationContract
5 | property :password
6 | property :password_confirmation, virtual: true
7 |
8 | validation do
9 | configure { config.namespace = :user_password }
10 |
11 | required(:password).filled(:str?)
12 | required(:password_confirmation).filled(:str?)
13 |
14 | required(:password).filled(
15 | :str?,
16 | min_size?: Constants::Shared::PASSWORD_MIN_SIZE,
17 | format?: Constants::Shared::PASSWORD_REGEX
18 | ).confirmation
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/reset_passwords/operation/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::ResetPasswords::Operation
4 | class Create < ApplicationOperation
5 | step Macro::Inject(
6 | jwt: 'services.email_token',
7 | redis: 'adapters.redis',
8 | worker: Api::V1::Users::ResetPasswords::Worker::EmailResetPasswordUrl
9 | )
10 | step Macro::Contract::Schema(Api::V1::Users::ResetPasswords::Contract::Create)
11 | step Contract::Validate(), fail_fast: true
12 | step Model(Account, :find_by_email, :email)
13 | fail Macro::Semantic(failure: :not_found)
14 | fail Macro::AddContractError(email: 'errors.reset_password.email_not_found'), fail_fast: true
15 | step :set_email_token
16 | step :push_email_token_to_redis
17 | step :send_reset_password_url
18 | step Macro::Semantic(success: :accepted)
19 |
20 | def set_email_token(ctx, jwt:, model:, **)
21 | ctx[:email_token] = jwt.create(account_id: model.id, namespace: Constants::TokenNamespace::RESET_PASSWORD)
22 | end
23 |
24 | def push_email_token_to_redis(_ctx, redis:, email_token:, **)
25 | redis.push_token(email_token)
26 | end
27 |
28 | def send_reset_password_url(_ctx, worker:, model:, email_token:, **)
29 | worker.perform_async(
30 | email: model.email,
31 | token: email_token,
32 | user_reset_password_path: Rails.application.config.user_reset_password_path
33 | )
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/reset_passwords/operation/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::ResetPasswords::Operation
4 | class Show < ApplicationOperation
5 | step Subprocess(Api::V1::Users::Lib::Operation::DecryptEmailToken), fast_track: true
6 | step Subprocess(Api::V1::Users::Lib::Operation::CheckEmailTokenRedisEquality)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/reset_passwords/operation/update.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::ResetPasswords::Operation
4 | class Update < ApplicationOperation
5 | step Macro::Inject(
6 | redis: 'adapters.redis',
7 | session: 'services.session_token',
8 | namespace: 'services.token_namespace'
9 | )
10 | step Subprocess(Api::V1::Users::Lib::Operation::DecryptEmailToken), fast_track: true
11 | step Subprocess(Api::V1::Users::Lib::Operation::CheckEmailTokenRedisEquality)
12 | step Contract::Build(constant: Api::V1::Users::ResetPasswords::Contract::Update)
13 | step Contract::Validate()
14 | step Contract::Persist()
15 | step :send_notification
16 | step :destroy_redis_email_token
17 | step :destroy_all_user_sessions
18 |
19 | def send_notification(_ctx, model:, **)
20 | UserMailer.reset_password_successful(email: model.email).deliver_later
21 | end
22 |
23 | def destroy_redis_email_token(_ctx, redis:, email_token:, **)
24 | redis.delete_token(email_token)
25 | end
26 |
27 | def destroy_all_user_sessions(_ctx, session:, namespace:, model:, **)
28 | session.destroy_all(
29 | namespace: namespace.call(Constants::TokenNamespace::SESSION, model.id)
30 | )
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/reset_passwords/worker/email_reset_password_url.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::ResetPasswords::Worker
4 | class EmailResetPasswordUrl < ApplicationWorker
5 | def perform(email:, token:, user_reset_password_path:)
6 | UserMailer.reset_password(email, token, user_reset_password_path).deliver_now
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/sessions/contract/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Sessions::Contract
4 | Create = Dry::Validation.Schema do
5 | required(:email).filled(:str?)
6 | required(:password).filled(:str?)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/sessions/operation/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Sessions::Operation
4 | class Create < ApplicationOperation
5 | step Macro::Inject(session: 'services.session_token')
6 | step Macro::Contract::Schema(Api::V1::Users::Sessions::Contract::Create)
7 | step Contract::Validate(), fail_fast: true
8 | step Model(Account, :find_by_email, :email)
9 | step :authenticate
10 | fail Macro::Semantic(failure: :unauthorized)
11 | fail Macro::AddContractError(base: 'errors.session.wrong_credentials'), fail_fast: true
12 | step :set_user_tokens
13 | step Macro::Semantic(success: :created)
14 | step Macro::Renderer(serializer: Api::V1::Lib::Serializer::Account, meta: :tokens)
15 |
16 | def authenticate(ctx, model:, **)
17 | model.authenticate(ctx['contract.default'].password)
18 | end
19 |
20 | def set_user_tokens(ctx, session:, model:, **)
21 | ctx[:tokens] = session.create(account_id: model.id, namespace: Constants::TokenNamespace::SESSION)
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/sessions/operation/destroy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Sessions::Operation
4 | class Destroy < ApplicationOperation
5 | step Macro::Inject(session: 'services.session_token')
6 | step Rescue(JWTSessions::Errors::Unauthorized) {
7 | step :destroy_user_session
8 | }
9 | step Macro::Semantic(success: :destroyed)
10 |
11 | def destroy_user_session(_ctx, session:, found_token:, **)
12 | session.destroy(refresh_token: found_token)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/sessions/refreshes/operation/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Sessions::Refreshes::Operation
4 | class Create < ApplicationOperation
5 | step Macro::Inject(session: 'services.session_token')
6 | step Rescue(JWTSessions::Errors::Unauthorized) {
7 | step :refresh_user_tokens
8 | }
9 | fail Macro::Semantic(failure: :forbidden)
10 | step Macro::Semantic(success: :created)
11 | step Macro::Renderer(meta: :tokens)
12 |
13 | def refresh_user_tokens(ctx, session:, payload:, found_token:, **)
14 | ctx[:tokens] = session.refresh(payload: payload, refresh_token: found_token)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/concepts/api/v1/users/verifications/operation/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Verifications::Operation
4 | class Show < ApplicationOperation
5 | step Subprocess(Api::V1::Users::Lib::Operation::DecryptEmailToken), fast_track: true
6 | step :user_account_not_verified?
7 | fail Macro::AddContractError(base: 'errors.verification.user_account_already_verified')
8 | step :verify_user_account
9 | step :create_user
10 | step :send_notification
11 |
12 | def user_account_not_verified?(_ctx, model:, **)
13 | !model.verified?
14 | end
15 |
16 | def verify_user_account(_ctx, model:, **)
17 | model.toggle!(:verified)
18 | end
19 |
20 | def create_user(_ctx, model:, **)
21 | model.create_user
22 | end
23 |
24 | def send_notification(_ctx, model:, **)
25 | UserMailer.verification_successful(email: model.email).deliver_later
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/concepts/application_contract.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationContract < Reform::Form
4 | feature Reform::Form::Dry
5 | end
6 |
--------------------------------------------------------------------------------
/app/concepts/application_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationDecorator < Draper::Decorator
4 | end
5 |
--------------------------------------------------------------------------------
/app/concepts/application_operation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationOperation < Trailblazer::Operation
4 | end
5 |
--------------------------------------------------------------------------------
/app/concepts/application_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationSerializer
4 | include JSONAPI::Serializer
5 | end
6 |
--------------------------------------------------------------------------------
/app/concepts/application_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationWorker
4 | include Sidekiq::Worker
5 | end
6 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users/account/profiles_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Account
4 | class ProfilesController < AuthorizedApiController
5 | def show
6 | endpoint operation: Api::V1::Users::Account::Profiles::Operation::Show
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users/registrations_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users
4 | class RegistrationsController < ApiController
5 | def create
6 | endpoint operation: Api::V1::Users::Registrations::Operation::Create
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users/reset_passwords_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users
4 | class ResetPasswordsController < ApiController
5 | def create
6 | endpoint operation: Api::V1::Users::ResetPasswords::Operation::Create
7 | end
8 |
9 | def show
10 | endpoint operation: Api::V1::Users::ResetPasswords::Operation::Show
11 | end
12 |
13 | def update
14 | endpoint operation: Api::V1::Users::ResetPasswords::Operation::Update
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users/session/refreshes_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users::Session
4 | class RefreshesController < ApiController
5 | def create
6 | authorize_refresh_request!
7 | endpoint operation: Api::V1::Users::Sessions::Refreshes::Operation::Create,
8 | options: { found_token: found_token, payload: payload }
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users
4 | class SessionsController < ApiController
5 | def create
6 | endpoint operation: Api::V1::Users::Sessions::Operation::Create
7 | end
8 |
9 | def destroy
10 | authorize_refresh_request!
11 | endpoint operation: Api::V1::Users::Sessions::Operation::Destroy, options: { found_token: found_token }
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users/verifications_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api::V1::Users
4 | class VerificationsController < ApiController
5 | def show
6 | endpoint operation: Api::V1::Users::Verifications::Operation::Show
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/api_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApiController < ActionController::API
4 | include Authentication
5 | include SimpleEndpoint::Controller
6 | include DefaultEndpoint
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/authorized_api_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AuthorizedApiController < ApiController
4 | before_action :authorize_access_request!
5 |
6 | private
7 |
8 | def endpoint_options
9 | { current_account: current_account, params: params }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/concerns/authentication.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Authentication
4 | extend ActiveSupport::Concern
5 | include JWTSessions::RailsAuthorization
6 |
7 | included do
8 | rescue_from JWTSessions::Errors::Unauthorized do
9 | render(
10 | jsonapi: Service::JsonApi::HashErrorSerializer.call(base: [I18n.t('errors.session.invalid_token')]),
11 | status: :unauthorized
12 | )
13 | end
14 | end
15 |
16 | def current_account
17 | @current_account ||= Account.find_by(id: payload['account_id'])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | end
5 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: Rails.configuration.default_sender_email
5 | layout 'mailer'
6 | end
7 |
--------------------------------------------------------------------------------
/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserMailer < ApplicationMailer
4 | def confirmation(email, token, path)
5 | @email = email
6 | @confirmation_url = URI.parse("#{path}?email_token=#{token}").to_s
7 | mail(to: email, subject: I18n.t('user_mailer.confirmation.subject'))
8 | end
9 |
10 | def verification_successful(email)
11 | @email = email
12 | mail(to: email, subject: I18n.t('user_mailer.verification_successful.subject'))
13 | end
14 |
15 | def reset_password(email, token, path)
16 | @email = email
17 | @reset_password_url = URI.parse("#{path}?email_token=#{token}").to_s
18 | mail(to: email, subject: I18n.t('user_mailer.reset_password.subject'))
19 | end
20 |
21 | def reset_password_successful(email)
22 | @email = email
23 | mail(to: email, subject: I18n.t('user_mailer.reset_password_successful.subject'))
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/models/account.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Account < ApplicationRecord
4 | has_secure_password
5 | has_one :user, dependent: :destroy
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/admin_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AdminUser < ApplicationRecord
4 | # Include default devise modules. Others available are:
5 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
6 | devise :database_authenticatable,
7 | :recoverable, :rememberable, :validatable
8 | end
9 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class User < ApplicationRecord
4 | belongs_to :account
5 | end
6 |
--------------------------------------------------------------------------------
/app/uploaders/application_uploader.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationUploader < Shrine
4 | plugin :activerecord
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/views/user_mailer/confirmation.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= t('.title') %>
3 |
4 |
5 |
6 | <%= link_to t('.confirm'), @confirmation_url %>
7 | <%= t('.message') %>:
8 | <%= @email %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/user_mailer/confirmation.text.erb:
--------------------------------------------------------------------------------
1 | <%= t('.title') %>
2 | ===============================================
3 |
4 | <%= t('.email') %>: <%= @email %>
5 | <%= t('.confirmation_url') %>: <%= @confirmation_url %>
6 |
--------------------------------------------------------------------------------
/app/views/user_mailer/reset_password.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= t('.title') %>
3 |
4 |
5 |
6 | <%= link_to t('.reset'), @reset_password_url %>
7 | <%= t('.message') %>:
8 | <%= @email %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/user_mailer/reset_password.text.erb:
--------------------------------------------------------------------------------
1 | <%= t('.title') %>
2 | ===============================================
3 |
4 | <%= t('.reset') %> <%= t('.message') %>: <%= @email %>
5 | <%= @reset_password_url %>
6 |
--------------------------------------------------------------------------------
/app/views/user_mailer/reset_password_successful.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= t('.title') %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/user_mailer/reset_password_successful.text.erb:
--------------------------------------------------------------------------------
1 | <%= t('.title') %>
2 | ===============================================
3 |
--------------------------------------------------------------------------------
/app/views/user_mailer/verification_successful.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= t('.title') %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/user_mailer/verification_successful.text.erb:
--------------------------------------------------------------------------------
1 | <%= t('.title') %>
2 | ===============================================
3 |
--------------------------------------------------------------------------------
/app/webpacker/packs/active_admin.js:
--------------------------------------------------------------------------------
1 | // Load Active Admin's styles into Webpacker,
2 | // see `active_admin.scss` for customization.
3 | import "../stylesheets/active_admin";
4 |
5 | import "@activeadmin/activeadmin";
6 |
--------------------------------------------------------------------------------
/app/webpacker/packs/active_admin/print.scss:
--------------------------------------------------------------------------------
1 | /* Active Admin Print Stylesheet */
2 | @import "~@activeadmin/activeadmin/src/scss/print";
3 |
--------------------------------------------------------------------------------
/app/webpacker/stylesheets/active_admin.scss:
--------------------------------------------------------------------------------
1 | // SASS variable overrides must be declared before loading up Active Admin's styles.
2 | //
3 | // To view the variables that Active Admin provides, take a look at
4 | // `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the
5 | // Active Admin source.
6 | //
7 | // For example, to change the sidebar width:
8 | // $sidebar-width: 242px;
9 |
10 | // Active Admin's got SASS!
11 | @import "~@activeadmin/activeadmin/src/scss/mixins";
12 | @import "~@activeadmin/activeadmin/src/scss/base";
13 |
14 | // Overriding any non-variable SASS must be done after the fact.
15 | // For example, to change the default status-tag color:
16 | //
17 | // .status_tag { background: #6090DB; }
18 |
--------------------------------------------------------------------------------
/app/workers/application_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nocov:
4 | class ApplicationWorker
5 | include Sidekiq::Worker
6 | end
7 | # :nocov:
8 |
--------------------------------------------------------------------------------
/app/workers/sentry_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SentryWorker < ApplicationWorker
4 | def perform(event)
5 | Raven.send_event(event)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | var validEnv = ['development', 'test', 'production']
3 | var currentEnv = api.env()
4 | var isDevelopmentEnv = api.env('development')
5 | var isProductionEnv = api.env('production')
6 | var isTestEnv = api.env('test')
7 |
8 | if (!validEnv.includes(currentEnv)) {
9 | throw new Error(
10 | 'Please specify a valid `NODE_ENV` or ' +
11 | '`BABEL_ENV` environment variables. Valid values are "development", ' +
12 | '"test", and "production". Instead, received: ' +
13 | JSON.stringify(currentEnv) +
14 | '.'
15 | )
16 | }
17 |
18 | return {
19 | presets: [
20 | isTestEnv && [
21 | '@babel/preset-env',
22 | {
23 | targets: {
24 | node: 'current'
25 | }
26 | }
27 | ],
28 | (isProductionEnv || isDevelopmentEnv) && [
29 | '@babel/preset-env',
30 | {
31 | forceAllTransforms: true,
32 | useBuiltIns: 'entry',
33 | corejs: 3,
34 | modules: false,
35 | exclude: ['transform-typeof-symbol']
36 | }
37 | ]
38 | ].filter(Boolean),
39 | plugins: [
40 | 'babel-plugin-macros',
41 | '@babel/plugin-syntax-dynamic-import',
42 | isTestEnv && 'babel-plugin-dynamic-import-node',
43 | '@babel/plugin-transform-destructuring',
44 | [
45 | '@babel/plugin-proposal-class-properties',
46 | {
47 | loose: true
48 | }
49 | ],
50 | [
51 | '@babel/plugin-proposal-object-rest-spread',
52 | {
53 | useBuiltIns: true
54 | }
55 | ],
56 | [
57 | '@babel/plugin-transform-runtime',
58 | {
59 | helpers: false,
60 | regenerator: true,
61 | corejs: false
62 | }
63 | ],
64 | [
65 | '@babel/plugin-transform-regenerator',
66 | {
67 | async: false
68 | }
69 | ]
70 | ].filter(Boolean)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/bin/docker:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | system("docker-compose run --rm server_app #{ARGV.join(' ')}")
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | begin
5 | load File.expand_path('spring', __dir__)
6 | rescue LoadError => e
7 | raise unless e.message.include?('spring')
8 | end
9 | APP_PATH = File.expand_path('../config/application', __dir__)
10 | require_relative '../config/boot'
11 | require 'rails/commands'
12 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | begin
5 | load File.expand_path('spring', __dir__)
6 | rescue LoadError => e
7 | raise unless e.message.include?('spring')
8 | end
9 | require_relative '../config/boot'
10 | require 'rake'
11 | Rake.application.run
12 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'fileutils'
5 |
6 | # path to your application root.
7 | APP_ROOT = File.expand_path('..', __dir__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | FileUtils.chdir APP_ROOT do
14 | # This script is a way to setup or update your development environment automatically.
15 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
16 | # Add necessary setup steps to this file.
17 |
18 | puts '== Installing dependencies =='
19 | system! 'gem install bundler --conservative'
20 | system('bundle check') || system!('bundle install')
21 |
22 | # puts "\n== Copying sample files =="
23 | # unless File.exist?('config/database.yml')
24 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
25 | # end
26 |
27 | puts "\n== Preparing database =="
28 | system! 'bin/rails db:prepare'
29 |
30 | puts "\n== Removing old logs and tempfiles =="
31 | system! 'bin/rails log:clear tmp:clear'
32 |
33 | puts "\n== Restarting application server =="
34 | system! 'bin/rails restart'
35 | end
36 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | # This file loads Spring without using Bundler, in order to be fast.
5 | # It gets overwritten when you run the `spring binstub` command.
6 |
7 | unless defined?(Spring)
8 | require 'rubygems'
9 | require 'bundler'
10 |
11 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
12 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' }
13 | if spring
14 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
15 | gem 'spring', spring.version
16 | require 'spring/binstub'
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/bin/webpack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/webpack_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::WebpackRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/bin/webpack-dev-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/dev_server_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::DevServerRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'boot'
4 |
5 | require 'rails'
6 | require 'active_model/railtie'
7 | require 'active_job/railtie'
8 | require 'active_record/railtie'
9 | require 'action_controller/railtie'
10 | require 'action_mailer/railtie'
11 |
12 | Bundler.require(*Rails.groups)
13 |
14 | module BoilerplateRailsApi
15 | class Application < Rails::Application
16 | config.load_defaults 6.0
17 | config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.yml').to_s]
18 | config.eager_load_paths << Rails.root.join('lib')
19 | config.api_only = true
20 | config.active_job.queue_adapter = :sidekiq
21 | config.test_framework = :rspec
22 |
23 | config.middleware.use ActionDispatch::Flash
24 | config.middleware.use Rack::MethodOverride
25 | config.middleware.use ActionDispatch::Cookies
26 | config.middleware.use ActionDispatch::Session::CookieStore
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
4 |
5 | require 'bundler/setup' # Set up gems listed in the Gemfile.
6 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
7 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: boilerplate_rails_api_production
11 |
--------------------------------------------------------------------------------
/config/credentials/credentials.yml:
--------------------------------------------------------------------------------
1 | aws: Do not use for Test and Development
2 | access_key_id:
3 | secret_access_key:
4 | s3:
5 | region: A region where we have stored a bucket
6 | bucket: S3 bucket name
7 |
8 | redis:
9 | db: The name of container in staging and production (redis://service_name:6379).
10 | redis://localhost:6379 for test and development.
11 |
12 | smtp:
13 | address: Domain address of email service
14 | user_name: An email
15 | password: A password from email box
16 |
17 | db:
18 | host: Domain of RDS instance
19 | port: 5432
20 | database: Database name from RDS
21 | user: User name for access to DB on RDS
22 | password: Password to RDS DB
23 | pool: 5
24 |
25 | sentry_dsn: Not used. Used for notify in a SLACK channel.
26 |
--------------------------------------------------------------------------------
/config/credentials/development.yml.enc:
--------------------------------------------------------------------------------
1 | Cfuej3vrC56sKkzLpHx3AkFXRpWkJGS+HTULvYSX/Mof9JNq6Clb4A1Bt1uVBqzsinVZVKtKVzDVlszd+McyTzaN6VpEhhdh6bLOVnNrFBKInfasQDqOHGzBhnlRIxQplszbJNzQK9X1MMlZMRIKfbxmZnfytmTEr7VLUYjZ0V6fjLantGana8yaY8N/5+/K23dAL7kbsRLT0/CN4VmnP8O5TYRzA9m0tUdT9ZnZAGlPg3GnAs0ZkifB9lUUkE3bc7Ew5g9ZFtPEuQq6PwheTnA/YiR+f546RxlskNI=--S1jlmdaflYm63Guq--OGx3SXdTXln47x0LLSLGCg==
--------------------------------------------------------------------------------
/config/credentials/production.yml.enc:
--------------------------------------------------------------------------------
1 | wsjpbGIVEulAtw20Dq87Yw5EEfoc/f0pX1ZxaEUn6qYvv8shJTEMuVzDNUof8Rqslb3bMavfMx2SPK53AE542RTpH5RdnVkERK0XYQYQugd8xxuQmxicxXYYvY2kuJ9TG3yup1q2twJUsaK9X6aFjkWrn4XFhmSHnI8KiE7lljFf0y3YfNKSGEzYg6S0i0mzZzGdujtDp9LU/dtXxpSKUyQnr4+xEjccfL8lQWewK/lSAE26ISJ4FeLFrWxc2Ke23hsmrQbwSc4qKtuMG1FXrT5kig164HI0TUUk8GVtSpM0/Nna7YupUS6SyTdCjdUaNixAqQHbMcoMBV8PIkSQRg==--v6GUK8BFsUPYGvxV--ZegcvljujSPl94y6qrKxGg==
--------------------------------------------------------------------------------
/config/credentials/staging.yml.enc:
--------------------------------------------------------------------------------
1 | vhwEOhTQ0eQ4u2B7qx2zMNqzrcxS2qJZEH9KWFqsWUlYLE94WDftVtLQZMG1QAbdAx3K97k+KZ9DfczVz+NC51wBiWPycoBaiB0UF4obI5TKv3Tjy+wmIH6HAuJ24XQv91oxWdHC7JsFV55hANFbOMKQSVfnV7oP+M5NfOQluWAApuP6aPEHVsWyLysc4CWQXHhRIA3fsBTqfxNr01JLicubHvOOvcsvDwPu+EMG1ZvxpuzyrhRuUhTHVcX9+lbzbxlUOR7EioAAUXyQvxqD4l/dsbp5SJwoklQPr5dp0hQpN0F00ZiKLXwTkkNbyFVzP0LAU56wS7Hr44s=--peg5QxKY3TlK5lA9--jUW2ebCAuTvL2EP0HwRxYA==
--------------------------------------------------------------------------------
/config/credentials/test.yml.enc:
--------------------------------------------------------------------------------
1 | 0tiY9V12p1Kpwncf8FFlTkKGpwhz1+sgKMqKLmCn1AY4H9JnEii6k1D3TIRZHvSbThRiniwoCKQle3QPQTDR4y8wT3RvU5QWx5CIaq23f2vzXdlwRQhhLM3lNrq7OXyOSC2tYzGgUX7RNTSm1+3rdkR4j/vIls05EKfKgyB7JX2zkXeJP4E0NsLcFtE1SFr2pNqIS41G+3vYglTj/21hukgUiRwq5SRX5gXngbFLtIKBxq1eHGBGBlH5eKYppmtVUOyQ3GbYjtqM8yahhpqjaV9clSU=--cAuDYkIUuWOSO3Ql--4jGa9qKxWSFzRR1ZwpRZbg==
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | encoding: unicode
4 | pool: 5
5 | timeout: 5000
6 | host: <%= ENV['DB_HOST'] || Rails.application.credentials.dig(:db, :host) %>
7 | username: <%= ENV['DB_USERNAME'] || Rails.application.credentials.dig(:db, :user) %>
8 | password: <%= ENV['DB_PASSWORD'] || Rails.application.credentials.dig(:db, :password) %>
9 | database: <%= Rails.application.credentials.dig(:db, :database) %>
10 |
11 | development:
12 | <<: *default
13 | database: boilerplate_development
14 | staging:
15 | <<: *default
16 | database: boilerplate_staging
17 | production:
18 | <<: *default
19 | database: boilerplate
20 | test:
21 | <<: *default
22 | database: boilerplate_test
23 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # ActiveSupport::Reloader.to_prepare do
6 | # ApplicationController.renderer.defaults.merge!(
7 | # http_host: 'example.org',
8 | # https: false
9 | # )
10 | # end
11 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
7 |
8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
9 | # Rails.backtrace_cleaner.remove_silencers!
10 |
--------------------------------------------------------------------------------
/config/initializers/constants/shared.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Constants
4 | module Shared
5 | EMAIL_REGEX = /\A(.+)@([a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,63})\z/i.freeze
6 | PASSWORD_REGEX = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[-_!@#$%\^&*])/.freeze
7 | PASSWORD_MIN_SIZE = 8
8 | EMAIL_MAX_LENGTH = 255
9 | HMAC_SECRET = Rails.env.test? ? 'test' : Rails.application.credentials.secret_key_base
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/config/initializers/constants/tokens_namespace.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Constants
4 | module TokenNamespace
5 | SESSION = 'user-account'
6 | RESET_PASSWORD = 'reset-password'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/config/initializers/cors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Avoid CORS issues when API is called from the frontend app.
6 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
7 |
8 | # Read more: https://github.com/cyu/rack-cors
9 |
10 | Rails.application.config.middleware.insert_before 0, Rack::Cors do
11 | allow do
12 | # origins 'localhost:3000'
13 |
14 | resource '*',
15 | headers: :any,
16 | methods: %i[get post put patch delete options head]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config/initializers/dry_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Dry::Validation::Schema.configure do |config|
4 | config.messages = :i18n
5 | config.input_processor = :sanitizer
6 | end
7 |
8 | Dry::Validation::Schema::Form.configure do |config|
9 | config.messages = :i18n
10 | end
11 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure sensitive parameters which will be filtered from the log file.
6 | Rails.application.config.filter_parameters += [:password]
7 |
--------------------------------------------------------------------------------
/config/initializers/generators.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.config.generators do |g|
4 | g.orm :active_record, primary_key_type: :uuid
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Add new inflection rules using the following format. Inflections
6 | # are locale specific, and you may define rules for as many different
7 | # locales as you wish. All of these examples are active by default:
8 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
9 | # inflect.plural /^(ox)$/i, '\1en'
10 | # inflect.singular /^(ox)en/i, '\1'
11 | # inflect.irregular 'person', 'people'
12 | # inflect.uncountable %w( fish sheep )
13 | # end
14 |
15 | # These inflection rules are supported but not enabled by default:
16 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
17 | # inflect.acronym 'RESTful'
18 | # end
19 |
--------------------------------------------------------------------------------
/config/initializers/json_api/filtering.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module JsonApi
4 | module Filtering
5 | module Operators
6 | MATCH_ALL = 'all_filters'
7 | MATCH_ANY = 'any_filters'
8 | end
9 |
10 | OPERATORS = [Operators::MATCH_ALL, Operators::MATCH_ANY].freeze
11 |
12 | module Predicates
13 | IN = 'in'
14 | NOT_IN = 'not_in'
15 | EQUAL = 'eq'
16 | EQUAL_RELATIVE = 'eq_rel'
17 | NOT_EQUAL = 'not_eq'
18 | NOT_EQUAL_RELATIVE = 'not_eq_rel'
19 | CONTAINS = 'cont'
20 | NOT_CONTAINS = 'not_cont'
21 | GREATER_THAN = 'gt'
22 | GREATER_THAN_OR_EQUAL = 'gteq'
23 | LESS_THAN = 'lt'
24 | TRUE = 'true'
25 | FALSE = 'false'
26 | NULL = 'null'
27 | NOT_NULL = 'not_null'
28 | GREATER_THAN_RELATIVE = 'gt_rel'
29 | LESS_THAN_RELATIVE = 'lt_rel'
30 | end
31 |
32 | PREDICATES = {
33 | string: [
34 | Predicates::IN,
35 | Predicates::NOT_IN,
36 | Predicates::EQUAL,
37 | Predicates::NOT_EQUAL,
38 | Predicates::CONTAINS,
39 | Predicates::NOT_CONTAINS
40 | ].to_set,
41 | number: [
42 | Predicates::IN,
43 | Predicates::NOT_IN,
44 | Predicates::EQUAL,
45 | Predicates::NOT_EQUAL,
46 | Predicates::GREATER_THAN,
47 | Predicates::GREATER_THAN_OR_EQUAL,
48 | Predicates::NULL,
49 | Predicates::NOT_NULL
50 | ].to_set,
51 | boolean: [
52 | Predicates::TRUE,
53 | Predicates::FALSE
54 | ].to_set,
55 | date: [
56 | Predicates::EQUAL,
57 | Predicates::NOT_EQUAL,
58 | Predicates::GREATER_THAN,
59 | Predicates::LESS_THAN,
60 | Predicates::EQUAL_RELATIVE,
61 | Predicates::NOT_EQUAL_RELATIVE,
62 | Predicates::GREATER_THAN_RELATIVE,
63 | Predicates::LESS_THAN_RELATIVE,
64 | Predicates::NULL,
65 | Predicates::NOT_NULL
66 | ].to_set
67 | }.freeze
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/config/initializers/json_api/pagination.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module JsonApi
4 | module Pagination
5 | MINIMAL_VALUE = 1
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/config/initializers/json_api/sorting.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module JsonApi
4 | module Sorting
5 | JSONAPI_SORT_PATTERN = /\A(-)?(.+)/.freeze
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/config/initializers/jwt_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | JWTSessions.encryption_key = Constants::Shared::HMAC_SECRET
4 | JWTSessions.token_store = Rails.application.config.token_store
5 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | JSONAPI_MEDIA_TYPE = 'application/vnd.api+json'
4 |
5 | Mime::Type.register(JSONAPI_MEDIA_TYPE, :jsonapi)
6 |
7 | ActionController::Renderers.add(:jsonapi) do |json, options|
8 | json = json.to_json(options) unless json.is_a?(String)
9 | self.content_type ||= Mime[:jsonapi]
10 | self.response_body = json
11 | end
12 |
--------------------------------------------------------------------------------
/config/initializers/oj.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Oj.optimize_rails
4 |
--------------------------------------------------------------------------------
/config/initializers/pagy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'pagy/extras/overflow'
4 | require 'pagy/extras/array'
5 |
6 | Pagy::VARS[:items] = 25
7 | Pagy::VARS[:overflow] = :empty_page
8 |
--------------------------------------------------------------------------------
/config/initializers/ransack.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Ransack.configure do |config|
4 | config.add_predicate 'date_equals',
5 | arel_predicate: 'eq',
6 | formatter: proc(&:to_date),
7 | validator: proc(&:present?),
8 | type: :string
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/redis.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Redis.current =
4 | Rails.env.test? ? MockRedis.new : Redis.new(url: (ENV['REDIS_DB'] || Rails.application.credentials.redis[:db]))
5 |
--------------------------------------------------------------------------------
/config/initializers/reform.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'reform'
4 | require 'reform/form/dry'
5 | require 'reform/form/coercion'
6 |
7 | module PatchErrorCompiler
8 | def call(fields, reform_errors, form)
9 | @validator.with(form: form).call(fields).errors.each do |field, dry_error|
10 | dry_error.each do |attr_error|
11 | reform_errors.add(field, attr_error)
12 | end
13 | end
14 | end
15 | end
16 |
17 | Reform::Form::Dry::Validations::Group.prepend(PatchErrorCompiler)
18 |
--------------------------------------------------------------------------------
/config/initializers/rswag_api.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rswag::Api.configure do |c|
4 | # Specify a root folder where Swagger JSON files are located
5 | # This is used by the Swagger middleware to serve requests for API descriptions
6 | # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
7 | # that it's configured to generate files in the same folder
8 | c.swagger_root = Rails.root.join('swagger').to_s
9 |
10 | # Inject a lamda function to alter the returned Swagger prior to serialization
11 | # The function will have access to the rack env for the current request
12 | # For example, you could leverage this to dynamically assign the "host" property
13 | #
14 | # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/rswag_ui.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rswag::Ui.configure do |c|
4 | # List the Swagger endpoints that you want to be documented through the swagger-ui
5 | # The first parameter is the path (absolute or relative to the UI host) to the corresponding
6 | # endpoint and the second is a title that will be displayed in the document selector
7 | # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints,
8 | # then the list below should correspond to the relative paths for those endpoints
9 |
10 | c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs'
11 |
12 | # Add Basic Auth in case your API is private
13 | # c.basic_auth_enabled = true
14 | # c.basic_auth_credentials 'username', 'password'
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/sentry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Raven.configure do |config|
4 | config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
5 | config.dsn = Rails.application.credentials[:sentry_dsn]
6 | config.environments = %w[production staging]
7 | config.async = lambda { |event|
8 | SentryWorker.perform_async(event)
9 | }
10 | end
11 |
--------------------------------------------------------------------------------
/config/initializers/shrine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'shrine'
4 |
--------------------------------------------------------------------------------
/config/initializers/sidekiq.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'sidekiq/web'
4 |
5 | Sidekiq.configure_client do |config|
6 | config.redis = { url: ENV['REDIS_DB'] || Rails.application.credentials.redis[:db] }
7 | end
8 |
9 | Sidekiq.configure_server do |config|
10 | config.redis = { url: ENV['REDIS_DB'] || Rails.application.credentials.redis[:db] }
11 | end
12 |
--------------------------------------------------------------------------------
/config/initializers/trailblazer_macro.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Dir[Rails.root.join('lib/macro/**/*.rb')].sort.each { |file| require file }
4 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # This file contains settings for ActionController::ParamsWrapper which
6 | # is enabled by default.
7 |
8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
9 | ActiveSupport.on_load(:action_controller) do
10 | wrap_parameters format: []
11 | end
12 |
13 | # To enable root element in JSON for ActiveRecord objects.
14 | # ActiveSupport.on_load(:active_record) do
15 | # self.include_root_in_json = true
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/zeitwerk.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.autoloaders.main.ignore(Rails.root.join('app/admin'))
4 | Rails.autoloaders.main.ignore(Rails.root.join('lib/macro'))
5 | Rails.autoloaders.main.ignore(Rails.root.join('app/channels/application_cable'))
6 |
--------------------------------------------------------------------------------
/config/locales/en.errors.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | errors:
4 | email_token:
5 | already_used: email token is already used
6 | email_uniq?: email not unique
7 | filtering_column_valid?: invalid filtering column
8 | filtering_predicate_valid?: invalid filtering predicate
9 | filtering_value_valid?: invalid filtering value
10 | filters_uniq?: filters not unique
11 | inclusion_params_uniq?: inclusion params not unique
12 | inclusion_params_valid?: can't include such association
13 | pagination_overflow: is out of limits
14 | reset_password:
15 | email_not_found: Email not found
16 | rules:
17 | user_password:
18 | eql?: should match to password
19 | session:
20 | invalid_token: invalid_token
21 | wrong_credentials: Invalid email or password
22 | sort_params_uniq?: sort params not unique
23 | sort_params_valid?: invalid sort params
24 | verification:
25 | invalid_email_token: invalid email token
26 |
--------------------------------------------------------------------------------
/config/locales/en.user_mailer.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | user_mailer:
4 | confirmation:
5 | confirm: Confirm
6 | confirmation_url: Confirmation url
7 | email: Email
8 | message: my email
9 | subject: Email confirmation
10 | title: Email verification
11 | reset_password:
12 | message: current user account password for
13 | reset: Reset
14 | subject: Reset password
15 | title: Reset user account password
16 | reset_password_successful:
17 | subject: Your user account password has been changed
18 | title: Your user account password has been changed
19 | verification_successful:
20 | subject: Your account has been verified successfully
21 | title: Your account has been verified successfully
22 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Puma can serve each request in a thread from an internal thread pool.
4 | # The `threads` method setting takes two numbers: a minimum and maximum.
5 | # Any libraries that use thread pools should be configured to match
6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
7 | # and maximum; this matches the default thread size of Active Record.
8 | #
9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
11 | threads min_threads_count, max_threads_count
12 |
13 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
14 | #
15 | port ENV.fetch('PORT') { 3000 }
16 |
17 | # Specifies the `environment` that Puma will run in.
18 | #
19 | environment ENV.fetch('RAILS_ENV') { 'development' }
20 |
21 | # Specifies the number of `workers` to boot in clustered mode.
22 | # Workers are forked web server processes. If using threads and workers together
23 | # the concurrency of the application would be max `threads` * `workers`.
24 | # Workers do not work on JRuby or Windows (both of which do not support
25 | # processes).
26 | #
27 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
28 |
29 | # Use the `preload_app!` method when specifying a `workers` number.
30 | # This directive tells Puma to first boot the application and load code
31 | # before forking the application. This takes advantage of Copy On Write
32 | # process behavior so workers use less memory.
33 | #
34 | # preload_app!
35 |
36 | # Allow puma to be restarted by `rails restart` command.
37 | plugin :tmp_restart
38 |
--------------------------------------------------------------------------------
/config/rails_best_practices.yml:
--------------------------------------------------------------------------------
1 | AddModelVirtualAttributeCheck: { }
2 | AlwaysAddDbIndexCheck: { }
3 | #CheckSaveReturnValueCheck: { }
4 | #CheckDestroyReturnValueCheck: { }
5 | DefaultScopeIsEvilCheck: { }
6 | DryBundlerInCapistranoCheck: { }
7 | #HashSyntaxCheck: { }
8 | IsolateSeedDataCheck: { }
9 | KeepFindersOnTheirOwnModelCheck: { }
10 | LawOfDemeterCheck: { }
11 | #LongLineCheck: { max_line_length: 80 }
12 | MoveCodeIntoControllerCheck: { }
13 | MoveCodeIntoHelperCheck: { array_count: 3 }
14 | MoveCodeIntoModelCheck: { use_count: 2 }
15 | MoveFinderToNamedScopeCheck: { }
16 | MoveModelLogicIntoModelCheck: {
17 | use_count: 4,
18 | ignored_files: ['app/controllers/concerns/default_endpoint.rb']
19 | }
20 | NeedlessDeepNestingCheck: { nested_count: 2 }
21 | NotRescueExceptionCheck: { }
22 | NotUseDefaultRouteCheck: { }
23 | NotUseTimeAgoInWordsCheck: { }
24 | OveruseRouteCustomizationsCheck: { customize_count: 3 }
25 | ProtectMassAssignmentCheck: { }
26 | RemoveEmptyHelpersCheck: { }
27 | #RemoveTabCheck: { }
28 | RemoveTrailingWhitespaceCheck: { }
29 | RemoveUnusedMethodsInControllersCheck: { except_methods: [
30 | AuthorizedApiController#endpoint_options
31 | ] }
32 | RemoveUnusedMethodsInHelpersCheck: { except_methods: [] }
33 | RemoveUnusedMethodsInModelsCheck: { except_methods: [] }
34 | ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
35 | ReplaceInstanceVariableWithLocalVariableCheck: { }
36 | RestrictAutoGeneratedRoutesCheck: { }
37 | SimplifyRenderInControllersCheck: { }
38 | SimplifyRenderInViewsCheck: { }
39 | #UseBeforeFilterCheck: { customize_count: 2 }
40 | UseModelAssociationCheck: { }
41 | UseMultipartAlternativeAsContentTypeOfEmailCheck: { }
42 | #UseParenthesesInMethodDefCheck: { }
43 | UseObserverCheck: { }
44 | UseQueryAttributeCheck: { }
45 | UseSayWithTimeInMigrationsCheck: { }
46 | UseScopeAccessCheck: { }
47 | UseTurboSprocketsRails3Check: { }
48 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.routes.draw do
4 | mount Rswag::Ui::Engine => '/api-docs'
5 | mount Rswag::Api::Engine => '/api-docs'
6 | devise_for :admin_users, ActiveAdmin::Devise.config
7 | ActiveAdmin.routes(self)
8 | authenticate :admin_user do
9 | mount Sidekiq::Web => '/sidekiq'
10 | end
11 |
12 | get 'health', to: proc { [200, {}, ['success']] }
13 | root to: 'admin/dashboard#index'
14 |
15 | namespace :api do
16 | namespace :v1 do
17 | namespace :users do
18 | resource :registration, only: :create
19 | resource :verification, only: :show
20 | resource :session, only: %i[create destroy] do
21 | scope module: :session do
22 | resource :refresh, only: :create
23 | end
24 | end
25 | resource :reset_password, only: %i[create show update]
26 | namespace :account do
27 | resource :profile, only: :show
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/config/sidekiq.yml:
--------------------------------------------------------------------------------
1 | ---
2 | :concurrency: 1
3 | :queues:
4 | - default
5 | - mailers
6 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Spring.watch(
4 | '.ruby-version',
5 | '.rbenv-vars',
6 | 'tmp/restart.txt',
7 | 'tmp/caching-dev.txt'
8 | )
9 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/config/webpack/environment.js:
--------------------------------------------------------------------------------
1 | const { environment } = require('@rails/webpacker')
2 | const jquery = require('./plugins/jquery')
3 |
4 | environment.plugins.prepend('jquery', jquery)
5 | module.exports = environment
6 |
--------------------------------------------------------------------------------
/config/webpack/plugins/jquery.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 |
3 | module.exports = new webpack.ProvidePlugin({
4 | "$":"jquery",
5 | "jQuery":"jquery",
6 | "window.jQuery":"jquery"
7 | });
8 |
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/db/migrate/20190813110526_enable_uuid.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EnableUuid < ActiveRecord::Migration[6.0]
4 | def change
5 | enable_extension 'pgcrypto'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20190813110527_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :users, id: :uuid do |t|
4 | t.uuid :account_id, index: true
5 | t.string :name
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20190813124813_create_accounts.rb:
--------------------------------------------------------------------------------
1 | class CreateAccounts < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :accounts, id: :uuid do |t|
4 | t.string :email, index: { unique: true }
5 | t.string :password_digest
6 | t.boolean :verified, default: false
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20200831072010_devise_create_admin_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DeviseCreateAdminUsers < ActiveRecord::Migration[6.0]
4 | def change
5 | create_table :admin_users, id: :uuid do |t|
6 | t.string :email, null: false, default: ""
7 | t.string :encrypted_password, null: false, default: ""
8 | t.string :reset_password_token
9 | t.datetime :reset_password_sent_at
10 | t.datetime :remember_created_at
11 | t.timestamps null: false
12 | end
13 |
14 | add_index :admin_users, :email, unique: true
15 | add_index :admin_users, :reset_password_token, unique: true
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file should contain all the record creation needed to seed the database with its default values.
4 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
5 | #
6 | # Examples:
7 | #
8 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
9 | # Character.create(name: 'Luke', movie: movies.first)
10 |
--------------------------------------------------------------------------------
/dip.yml:
--------------------------------------------------------------------------------
1 | # Required minimum dip version
2 | version: '4.1'
3 |
4 | environment:
5 | COMPOSE_EXT: development
6 |
7 | compose:
8 | files:
9 | - docker-compose.yml
10 | project_name: boilerplate
11 |
12 | interaction:
13 | bash:
14 | description: Open the Bash shell in app's container
15 | service: server_app
16 | command: bash
17 | compose:
18 | run_options: [no-deps]
19 |
20 | bundle:
21 | description: Run Bundler commands
22 | service: server_app
23 | command: bundle
24 |
25 | rspec:
26 | description: Run Rspec commands
27 | service: server_app
28 | environment:
29 | RAILS_ENV: test
30 | command: bundle exec rspec
31 |
32 | rails:
33 | description: Run Rails commands
34 | service: server_app
35 | command: bundle exec rails
36 | subcommands:
37 | s:
38 | description: Run Rails server at http://localhost:3000
39 | service: server_app
40 | compose:
41 | run_options: [service-ports, use-aliases]
42 |
43 | sidekiq:
44 | description: Run sidekiq in background
45 | service: server_worker_app
46 | compose:
47 | method: up
48 | run_options: [detach]
49 |
50 | psql:
51 | description: Run Postgres psql console
52 | service: server_app
53 | default_args: boilerplate_rails_api_development
54 | command: psql -h db -p 5432 -U postgres
55 |
56 | provision:
57 | - dip compose down --volumes
58 | - dip compose up
59 |
--------------------------------------------------------------------------------
/lib/container.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Container
4 | extend Dry::Container::Mixin
5 |
6 | namespace 'adapters' do
7 | register('redis') { Api::V1::Users::Lib::Service::RedisAdapter }
8 | end
9 |
10 | namespace 'services' do
11 | register('pagy') { Service::Pagy }
12 | register('email_token') { Api::V1::Users::Lib::Service::EmailToken }
13 | register('session_token') { Api::V1::Users::Lib::Service::SessionToken }
14 | register('token_namespace') { Api::V1::Users::Lib::Service::TokenNamespace }
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/macro/add_contract_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.AddContractError(name: :default, **args)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | error, message = args.first
8 | ctx["contract.#{name}"].errors.add(
9 | error, message.is_a?(Array) ? message : I18n.t(message)
10 | )
11 | }
12 | )
13 | { task: task, id: "#{self.name}/#{__method__}_id_#{task.object_id}".underscore }
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/macro/assign.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Assign(to:, path: [], value: nil, try: false)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | method_name = try ? :try : :public_send
8 | ctx[to] = value || path.drop(1).inject(ctx[path.first], method_name)
9 | }
10 | )
11 | { task: task, id: "#{name}/#{__method__}_#{path.join('.')}_to_#{to}_id_#{task.object_id}".underscore }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/macro/decorate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Decorate(decorator: nil, from: :model, to: :model, **)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | model = ctx[from]
8 | ctx[to] = (decorator || ctx[:decorator]).public_send(
9 | (model.is_a?(Enumerable) ? :decorate_collection : :decorate), model
10 | )
11 | }
12 | )
13 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/macro/inject.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Inject(**deps)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | deps.each { |key, value| ctx[key] = value.is_a?(String) ? Container[value] : value }
8 | }
9 | )
10 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/macro/links_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.LinksBuilder(resource_path: nil, ids: [], **)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | pagy = ctx[:pagy]
8 | resource_ids = ids.map { |id| ctx[:params][id] }
9 | ctx[:links] =
10 | if resource_path && pagy
11 | Service::JsonApi::Paginator.call(
12 | resource_path: Rails.application.routes.url_helpers.public_send(*[resource_path, *resource_ids].compact),
13 | pagy: pagy
14 | )
15 | end
16 | }
17 | )
18 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/macro/model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Model(entity:, connections: [], find_by_key: :id, params_key: :id, assign: false)
5 | task = ->((ctx, flow_options), **) {
6 | if assign
7 | Macro::Assign(to: :model, path: [*entity, *connections], try: true)[:task].call(ctx, **{ options: [] })
8 | else
9 | ctx[:model] = Model.find_relation(ctx[entity], connections, find_by_key, ctx[:params][params_key])
10 | end
11 |
12 | ctx['result.model'] = Model.result(ctx[:model])
13 |
14 | [Model.direction(ctx[:model].present?), [ctx, flow_options]]
15 | }
16 |
17 | { task: task, id: "#{entity}_model_id#{task.object_id}" }
18 | end
19 |
20 | module Model
21 | def self.direction(result)
22 | return Trailblazer::Activity::Right if result
23 |
24 | Trailblazer::Activity::Left
25 | end
26 |
27 | def self.result(model)
28 | Trailblazer::Operation::Result.new(model.present?, model)
29 | end
30 |
31 | def self.find_relation(entity, connections, find_by_key, params_key)
32 | connections.inject(entity, :public_send).find_by(find_by_key => params_key)
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/macro/model_remove.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.ModelRemove(path: [], type: :destroy)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | model = ctx[path.first]
8 | types = %i[destroy delete destroy! delete!]
9 | call_chain = path[1..]
10 | return unless types.include?(type)
11 |
12 | call_chain.empty? ? model.public_send(type) : call_chain.push(type).inject(model, :try)
13 | }
14 | )
15 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/macro/policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Policy(policy_class, rule, policy_params: nil, name: :default, model: :model, **)
5 | task = ->((ctx, flow_options), **) {
6 | policy_namespace = :"macro.policy.#{name}"
7 | ctx[policy_namespace] = policy_class.new(ctx[:current_user], ctx[model])
8 | result = if policy_params
9 | ctx[policy_namespace].public_send(rule, ctx[:policy_params])
10 | else
11 | ctx[policy_namespace].public_send(rule)
12 | end
13 | unless result
14 | message = "#{policy_class.name.demodulize.underscore}.#{rule}" || 'default'
15 | current_errors = ctx[:errors] || {}
16 | ctx[:errors] = current_errors.deep_merge(
17 | {
18 | policy: [I18n.t("policy.errors.#{message}")]
19 | }
20 | )
21 |
22 | ctx[:semantic_failure] = :forbidden
23 | end
24 |
25 | signal = result ? Trailblazer::Activity::Right : Trailblazer::Activity::Left
26 | [signal, [ctx, flow_options]]
27 | }
28 |
29 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/macro/renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Renderer(serializer: ApplicationSerializer, meta: nil, **)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | ctx[:renderer] =
8 | {
9 | serializer: serializer,
10 | include: ctx[:inclusion_options],
11 | links: ctx[:links],
12 | meta: meta ? ctx[meta] : nil
13 | }.compact
14 | }
15 | )
16 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/macro/semantic.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Macro
4 | def self.Semantic(success: nil, failure: nil, **)
5 | task = Trailblazer::Activity::TaskBuilder::Binary(
6 | ->(ctx, **) {
7 | ctx[:semantic_success], ctx[:semantic_failure] = success, failure
8 | true
9 | }
10 | )
11 | { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/service/json_api/base_error_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | class BaseErrorSerializer
6 | ERRORS_SOURCES = { contract_errors_matcher: 'contract.' }.freeze
7 |
8 | private_class_method :new
9 |
10 | def self.call(result)
11 | new(result).jsonapi_errors_hash
12 | end
13 |
14 | def initialize(result, errors_source = nil)
15 | @errors = custom_errors(result, errors_source) || contract_errors(result)
16 | end
17 |
18 | def jsonapi_errors_hash
19 | { errors: parse_errors }
20 | end
21 |
22 | private
23 |
24 | attr_reader :errors
25 |
26 | # :reek:UtilityFunction
27 | def custom_errors(result, errors_source)
28 | result[errors_source]&.errors&.messages
29 | end
30 |
31 | # :reek:UtilityFunction
32 | def contract_errors(result)
33 | contracts = result.keys.select do |result_key|
34 | result_key.start_with?(ERRORS_SOURCES[:contract_errors_matcher]) &&
35 | result[result_key].try(:errors)
36 | end.compact
37 |
38 | contracts.map { |contract| result[contract].errors.messages }.reduce(:merge)
39 | end
40 |
41 | def parse_errors; end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/service/json_api/error_data_structure_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | module ErrorDataStructureParser
6 | private
7 |
8 | def plain_errors?(errors)
9 | errors.all?(String)
10 | end
11 |
12 | def compose_errors(field, messages)
13 | { source: { pointer: "/#{field}" }, detail: messages.join(', ') }
14 | end
15 |
16 | def parse_nested_errors_hash(errors_hash, pointer, memo)
17 | errors_hash.each do |key, error|
18 | new_pointer = "#{pointer}/#{key}"
19 |
20 | if error.is_a?(Hash)
21 | parse_nested_errors_hash(error, new_pointer, memo)
22 | else
23 | memo << compose_errors(new_pointer, error)
24 | end
25 | end
26 | end
27 |
28 | def parse_nested_errors_arrays_array(errors_arrays_array, pointer, memo)
29 | errors_arrays_array.each do |errors_array|
30 | new_pointer = "#{pointer}/#{errors_array.first}"
31 | nested_errors = errors_array.second
32 |
33 | if plain_errors?(nested_errors)
34 | memo << compose_errors(new_pointer, nested_errors)
35 | else
36 | parse_nested_errors_hash(nested_errors, new_pointer, memo)
37 | end
38 | end
39 | end
40 |
41 | def parse_errors
42 | errors.each_with_object([]) do |(field, error_value), memo|
43 | if plain_errors?(error_value)
44 | memo << compose_errors(field, error_value)
45 | else
46 | parse_nested_errors_arrays_array(error_value, field, memo)
47 | end
48 | end
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/service/json_api/hash_error_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | class HashErrorSerializer < Service::JsonApi::BaseErrorSerializer
6 | include Service::JsonApi::ErrorDataStructureParser
7 |
8 | def initialize(**result)
9 | @errors = result
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/service/json_api/paginator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | class Paginator
6 | QUERY_PAGE_PARAMETER = 'page[number]'
7 |
8 | class << self
9 | def call(resource_path:, pagy:)
10 | {
11 | self: page_path(resource_path, pagy.page),
12 | first: resource_path,
13 | next: page_path(resource_path, pagy.next),
14 | prev: page_path(resource_path, pagy.prev),
15 | last: page_path(resource_path, pagy.pages)
16 | }
17 | end
18 |
19 | private
20 |
21 | def page_path(resource_path, page)
22 | return if page.blank?
23 |
24 | "#{resource_path}?#{QUERY_PAGE_PARAMETER}=#{page}"
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/service/json_api/resource_error_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | class ResourceErrorSerializer < Service::JsonApi::BaseErrorSerializer
6 | include Service::JsonApi::ErrorDataStructureParser
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/service/json_api/resource_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | class ResourceSerializer
6 | private_class_method :new
7 |
8 | def self.call(result)
9 | new(result).call
10 | end
11 |
12 | def initialize(result)
13 | renderer = result[:renderer]
14 | @serializer = renderer.delete(:serializer)
15 | @options = renderer
16 | @object = result[:model]
17 | end
18 |
19 | def call
20 | serializer.new(object, options)
21 | end
22 |
23 | private
24 |
25 | attr_reader :serializer, :options, :object
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/service/json_api/uri_query_error_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | module JsonApi
5 | class UriQueryErrorSerializer < Service::JsonApi::BaseErrorSerializer
6 | ERRORS_SOURCE = 'contract.uri_query'
7 |
8 | def initialize(result)
9 | super(result, Service::JsonApi::UriQueryErrorSerializer::ERRORS_SOURCE)
10 | end
11 |
12 | private
13 |
14 | def compose_nested_errors(field, attribute_errors)
15 | attribute_errors.map do |nested_field, errors|
16 | {
17 | source: { pointer: "#{field}[#{nested_field}]" },
18 | detail: errors.join(', ')
19 | }
20 | end
21 | end
22 |
23 | def parse_errors
24 | errors.flat_map do |field, attribute_errors|
25 | if attribute_errors.flatten.any? { |item| !item.is_a?(String) }
26 | compose_nested_errors(field, attribute_errors)
27 | else
28 | { source: { pointer: field.to_s }, detail: attribute_errors.join }
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/service/pagy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Service
4 | class Pagy
5 | class << self
6 | include ::Pagy::Backend
7 |
8 | def call(collection, page:, items:, **)
9 | pagy_method = collection.is_a?(Array) ? :pagy_array : :pagy
10 | send(pagy_method, collection, page: page, items: items)
11 | end
12 |
13 | private
14 |
15 | define_method(:params) { {} }
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/tasks/factory_bot_linter.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nocov:
4 | namespace :factory_bot do
5 | desc 'Verify that all FactoryBot factories are valid'
6 | task lint: :environment do
7 | if Rails.env.test?
8 | ActiveRecord::Base.connection.transaction do
9 | FactoryBot.lint
10 | raise ActiveRecord::Rollback
11 | end
12 | else
13 | system("bundle exec rake factory_bot:lint RAILS_ENV='test'")
14 | fail if $?.exitstatus.nonzero? # rubocop:disable Style/SignalException, Style/SpecialGlobalVars
15 | end
16 | end
17 | end
18 | # :nocov:
19 |
--------------------------------------------------------------------------------
/lib/tasks/generate_api_documentation.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nocov:
4 | namespace :api do
5 | namespace :doc do
6 | desc 'Generate API Blueprint documentation'
7 | task :apib, :version do |_task, arg|
8 | require 'rspec/core/rake_task'
9 |
10 | version = arg[:version]
11 | apib_file = "public/api/docs/#{version}/index.apib"
12 | RSpec::Core::RakeTask.new(:api_spec) do |t|
13 | t.pattern = "spec/requests/api/#{version}"
14 | t.rspec_opts = "-f Dox::Formatter --order defined --tag dox --out #{apib_file}"
15 | end
16 |
17 | Rake::Task['api_spec'].invoke
18 | end
19 |
20 | desc 'Generate API HTML documentation'
21 | task :html, [:version] => [:apib] do |_task, arg|
22 | version = arg[:version]
23 | apib_file = "public/api/docs/#{version}/index.apib"
24 | html_file = "public/api/docs/#{version}/index.html"
25 |
26 | system("snowboard html -o #{html_file} #{apib_file}")
27 | end
28 |
29 | def generate_docs(version)
30 | Rake::Task['api:doc:html'].invoke(version)
31 | end
32 |
33 | desc 'Generate API Blueprint and HTML documentation for the v1'
34 | task :v1 do
35 | generate_docs('v1')
36 | end
37 | end
38 | end
39 | # :nocov:
40 |
--------------------------------------------------------------------------------
/lib/types/json_api.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Types
4 | include Dry::Types.module
5 |
6 | FilterObject = Struct.new(:column, :predicate, :value)
7 | SortObject = Struct.new(:column, :order)
8 |
9 | module JsonApi
10 | module TypeByColumn
11 | def self.call(column)
12 | {
13 | string: ::Types::String | ::Types::Array.of(::Types::String),
14 | number: ::Types::Form::Int |
15 | ::Types::Form::Decimal |
16 | ::Types::Array.of(::Types::Int) |
17 | ::Types::Array.of(::Types::Decimal),
18 | boolean: ::Types::Form::True | ::Types::Form::False | ::Types::Form::Nil,
19 | date: ::Types::Form::Date | ::Types::Form::Int
20 | }[column]
21 | end
22 | end
23 |
24 | Filter = Types::Array.constructor do |parameter|
25 | parameter.map do |key, value|
26 | column, predicate = key.split('-')
27 | Types::FilterObject.new(column, predicate, value)
28 | end
29 | end
30 |
31 | Sort = Types::Array.constructor do |parameter|
32 | parameter.split(',').map do |sort_object|
33 | order, column = sort_object.scan(::JsonApi::Sorting::JSONAPI_SORT_PATTERN).flatten
34 | Types::SortObject.new(column, order ? :desc : :asc)
35 | end
36 | end
37 |
38 | Include = Types::Array.constructor { |parameter| parameter.split(',') }
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/log/.keep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@activeadmin/activeadmin": "^2.8.0",
4 | "@rails/webpacker": "5.2.1"
5 | },
6 | "devDependencies": {
7 | "webpack-dev-server": "^3.11.2"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('postcss-flexbugs-fixes'),
5 | require('postcss-preset-env')({
6 | autoprefixer: {
7 | flexbox: 'no-2009'
8 | },
9 | stage: 3
10 | })
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/lib/operation/inclusion_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Lib::Operation::Inclusion do
4 | subject(:result) do
5 | described_class.call(params: params, available_inclusion_options: available_inclusion_options)
6 | end
7 |
8 | let(:valid_inclusion_parameter) { 'valid_inclusion_parameter' }
9 | let(:inclusion_params) { valid_inclusion_parameter }
10 | let(:available_inclusion_options) { [valid_inclusion_parameter] }
11 | let(:params) { { include: inclusion_params } }
12 |
13 | describe 'Success' do
14 | context 'when inclusion parameter not passing' do
15 | let(:params) { {} }
16 |
17 | it 'skips all steps' do
18 | expect(result['contract.uri_query']).to be_nil
19 | expect(result[:inclusion_options]).to be_nil
20 | expect(result).to be_success
21 | end
22 | end
23 |
24 | context 'when valid inclusion parameter passing' do
25 | it 'returns succesful result' do
26 | expect(result['contract.uri_query']).to be_present
27 | expect(result['result.contract.uri_query']).to be_success
28 | expect(result[:inclusion_options]).to eq([inclusion_params])
29 | expect(result).to be_success
30 | end
31 | end
32 | end
33 |
34 | describe 'Failure' do
35 | include_examples 'nested inclusion errors'
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/lib/operation/perform_ordering_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Lib::Operation::PerformOrdering do
4 | subject(:operation) { described_class.call(params) }
5 |
6 | let(:params) { { order_options: order_options, relation: relation } }
7 | let(:relation) { User.all }
8 |
9 | describe 'Success' do
10 | context 'when ordering options not passed' do
11 | let(:order_options) { nil }
12 |
13 | it 'return success result' do
14 | expect(operation[:relation]).to eq(relation)
15 | expect(operation).to be_success
16 | end
17 | end
18 |
19 | context 'when order options is passed' do
20 | let(:order_options) { { name: :desc } }
21 |
22 | before { create_list(:user, 3) }
23 |
24 | it 'returns ordered relation' do
25 | expect(operation[:relation].to_a).to eq(User.all.order(name: :desc).to_a)
26 | expect(operation).to be_success
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/account/profiles/show_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Account::Profiles::Operation::Show do
4 | subject(:result) { described_class.call(params: params, current_account: account) }
5 |
6 | let(:account) { create(:account, :with_user) }
7 | let(:inclusion_params) { 'account' }
8 | let(:params) { { include: inclusion_params } }
9 |
10 | describe 'Success' do
11 | it 'prepares data for user profile rendering' do
12 | expect(Api::V1::Lib::Operation::Inclusion).to receive(:call).and_call_original
13 | expect(result[:model]).to eq(account.user)
14 | expect(result[:available_inclusion_options]).to eq(%w[account])
15 | expect(result).to be_success
16 | end
17 | end
18 |
19 | describe 'Failure' do
20 | context 'when inclusion errors' do
21 | include_examples 'nested inclusion errors'
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/lib/operation/check_email_token_redis_equality_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Lib::Operation::CheckEmailTokenRedisEquality do
4 | subject(:result) { described_class.call(ctx) }
5 |
6 | let(:ctx) { { 'contract.default' => ApplicationContract.new({}), email_token: email_token } }
7 | let(:account) { create(:account) }
8 | let(:email_token) { create_token(:email, account: account) }
9 | let(:email_token1) { create_token(:email, account: account) }
10 |
11 | before { Api::V1::Users::Lib::Service::RedisAdapter.push_token(email_token) }
12 |
13 | describe '.call' do
14 | describe 'Success' do
15 | it 'check tokens equality' do
16 | expect(Api::V1::Users::Lib::Service::RedisAdapter).to receive(:find_token).with(email_token).and_call_original
17 | expect(result).to be_success
18 | end
19 | end
20 |
21 | describe 'Failure' do
22 | include_examples 'has email token equality errors'
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/lib/operation/decrypt_email_token_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Lib::Operation::DecryptEmailToken do
4 | subject(:result) { described_class.call(params: params) }
5 |
6 | let(:account) { create(:account) }
7 | let(:email_token) { create_token(:email, account: account) }
8 |
9 | describe 'Success' do
10 | let(:params) { { email_token: email_token } }
11 |
12 | it 'decrypts email token, set model' do
13 | expect(Api::V1::Users::Lib::Service::EmailToken).to receive(:read).and_call_original
14 | expect(result[:model]).to be_persisted
15 | expect(result).to be_success
16 | end
17 | end
18 |
19 | describe 'Failure' do
20 | context 'when email token errors' do
21 | include_examples 'has email token decryption errors'
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/lib/service/email_token_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Lib::Service::EmailToken do
4 | let(:account) { instance_double('Account', id: rand(1..42)) }
5 | let(:account_id) { account.id }
6 | let(:payload) { { account_id: account_id } }
7 |
8 | describe 'defined constants' do
9 | it { expect(described_class).to be_const_defined(:ERROR_MESSAGE) }
10 | it { expect(described_class).to be_const_defined(:TOKEN_LIFETIME) }
11 | it { expect(described_class::TOKEN_LIFETIME).to eq(24.hours) }
12 | end
13 |
14 | context 'when hmac secret exists' do
15 | describe '.create' do
16 | it 'creates email token from payload' do
17 | expect(described_class.create(payload)).to be_an_instance_of(String)
18 | end
19 | end
20 |
21 | describe '.read' do
22 | let(:token) { create_token(:email, account: account, namespace: :namespace) }
23 |
24 | context 'with valid token' do
25 | it 'includes token payload' do
26 | expect(described_class.read(token)).to include(account_id: account_id)
27 | expect(described_class.read(token)).to include(:exp)
28 | expect(described_class.read(token)).to include(:namespace)
29 | end
30 | end
31 |
32 | context 'with invalid token' do
33 | it 'returns false' do
34 | expect(described_class.read('invalid_token')).to be(false)
35 | end
36 | end
37 | end
38 | end
39 |
40 | context 'when hmac secret not exists' do
41 | let(:error_expectation) { raise_error(RuntimeError, /Secret key is not assigned/) }
42 |
43 | before { stub_const('Constants::Shared::HMAC_SECRET', nil) }
44 |
45 | it 'raises runtime error' do
46 | expect { described_class.create(payload) }.to error_expectation
47 | expect { described_class.read(payload) }.to error_expectation
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/lib/service/token_namespace_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Lib::Service::TokenNamespace do
4 | describe '.call' do
5 | subject(:service) { described_class.call(namespace, entity_id) }
6 |
7 | let(:namespace) { :namespace }
8 | let(:entity_id) { rand(1..42) }
9 |
10 | it 'returns interpolated token namespace' do
11 | expect(service).to eq("#{namespace}-#{entity_id}")
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/registrations/worker/email_confirmation_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Registrations::Worker::EmailConfirmation, type: :worker do
4 | describe '.perform' do
5 | subject(:worker) do
6 | described_class.new.perform(
7 | email: email,
8 | token: token,
9 | user_verification_path: user_verification_path
10 | )
11 | end
12 |
13 | let(:email) { FFaker::Internet.email }
14 | let(:token) { create_token(:email) }
15 | let(:user_verification_path) { FFaker::InternetSE.slug }
16 | let(:mailer_instance) { instance_double('MailMessage', :deliver_now) }
17 |
18 | it 'calls UserMailer' do
19 | expect(UserMailer)
20 | .to receive(:confirmation).with(email, token, user_verification_path).and_return(mailer_instance)
21 | expect(mailer_instance).to receive(:deliver_now)
22 | worker
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/reset_passwords/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::ResetPasswords::Operation::Create do
4 | subject(:result) { described_class.call(params: params) }
5 |
6 | let(:params) { {} }
7 | let(:email) { FFaker::Internet.email }
8 |
9 | before { create(:account, email: email) }
10 |
11 | describe 'Success' do
12 | let(:params) { { email: email } }
13 |
14 | it 'creates and email reset password token' do
15 | expect(Api::V1::Users::Lib::Service::EmailToken).to receive(:create).and_call_original
16 | expect(Api::V1::Users::Lib::Service::RedisAdapter).to receive(:push_token).and_call_original
17 | expect(Api::V1::Users::ResetPasswords::Worker::EmailResetPasswordUrl).to receive(:perform_async).and_call_original
18 | expect(result[:semantic_success]).to eq(:accepted)
19 | expect(result).to be_success
20 | end
21 | end
22 |
23 | describe 'Failure' do
24 | context 'without params' do
25 | let(:errors) { { email: [I18n.t('errors.key?')] } }
26 |
27 | include_examples 'has validation errors'
28 | end
29 |
30 | context 'with empty params' do
31 | let(:params) { { email: '' } }
32 | let(:errors) { { email: [I18n.t('errors.filled?')] } }
33 |
34 | include_examples 'has validation errors'
35 | end
36 |
37 | context 'with wrong params type' do
38 | let(:params) { { email: true } }
39 | let(:errors) { { email: [I18n.t('errors.str?')] } }
40 |
41 | include_examples 'has validation errors'
42 | end
43 |
44 | context 'when account not found' do
45 | let(:errors) { { email: [I18n.t('errors.reset_password.email_not_found')] } }
46 | let(:params) { { email: "_#{email}" } }
47 |
48 | include_examples 'has validation errors'
49 |
50 | it 'sets semantic failure not_found' do
51 | expect(result[:semantic_failure]).to eq(:not_found)
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/reset_passwords/show_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::ResetPasswords::Operation::Show do
4 | subject(:result) { described_class.call(params: params) }
5 |
6 | let(:account) { create(:account) }
7 | let(:email_token) { create_token(:email, account: account) }
8 | let(:params) { { email_token: email_token } }
9 |
10 | describe 'Success' do
11 | before { Api::V1::Users::Lib::Service::RedisAdapter.push_token(email_token) }
12 |
13 | it 'decrypts email token, set model' do
14 | expect(Api::V1::Users::Lib::Operation::DecryptEmailToken).to receive(:call).and_call_original
15 | expect(Api::V1::Users::Lib::Operation::CheckEmailTokenRedisEquality).to receive(:call).and_call_original
16 | expect(result).to be_success
17 | end
18 | end
19 |
20 | describe 'Failure' do
21 | context 'when email token decryption errors' do
22 | include_examples 'has email token decryption errors'
23 | end
24 |
25 | context 'when email token redis equality errors' do
26 | include_examples 'has email token equality errors'
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/reset_passwords/worker/email_reset_password_url_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::ResetPasswords::Worker::EmailResetPasswordUrl, type: :worker do
4 | describe '.perform' do
5 | subject(:worker) do
6 | described_class.new.perform(
7 | email: email,
8 | token: token,
9 | user_reset_password_path: user_reset_password_path
10 | )
11 | end
12 |
13 | let(:email) { FFaker::Internet.email }
14 | let(:token) { create_token(:email) }
15 | let(:user_reset_password_path) { FFaker::InternetSE.slug }
16 | let(:mailer_instance) { instance_double('MailMessage', :deliver_now) }
17 |
18 | it 'calls UserMailer' do
19 | expect(UserMailer)
20 | .to receive(:reset_password).with(email, token, user_reset_password_path).and_return(mailer_instance)
21 | expect(mailer_instance).to receive(:deliver_now)
22 | worker
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/sessions/destroy_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Sessions::Operation::Destroy do
4 | subject(:result) { described_class.call(found_token: refresh_token) }
5 |
6 | describe 'Success' do
7 | let(:account) { create(:account) }
8 | let(:refresh_token) { create_token(:refresh, account: account) }
9 |
10 | it 'clears user session' do
11 | expect(Api::V1::Users::Lib::Service::SessionToken).to receive(:destroy).and_call_original
12 | expect(result[:semantic_success]).to eq(:destroyed)
13 | expect(result).to be_success
14 | end
15 | end
16 |
17 | describe 'Failure' do
18 | context 'with invalid refresh token' do
19 | let(:refresh_token) { 'invalid_token' }
20 |
21 | it do
22 | expect(result[:semantic_success]).to be_nil
23 | expect(result).to be_failure
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/sessions/refreshes/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Sessions::Refreshes::Operation::Create do
4 | subject(:result) { described_class.call(found_token: refresh_token, payload: payload) }
5 |
6 | let(:account) { create(:account) }
7 | let(:payload) { { 'account_id' => account.id } }
8 |
9 | describe 'Success' do
10 | let(:refresh_token) { create_token(:refresh, :expired, account: account) }
11 |
12 | it 'refreshes user session' do
13 | expect(Api::V1::Users::Lib::Service::SessionToken).to receive(:refresh).and_call_original
14 | expect(result[:tokens]).to include(:access, :access_expires_at, :csrf)
15 | expect(result[:semantic_success]).to eq(:created)
16 | expect(result[:renderer]).to include(:serializer, :meta)
17 | expect(result).to be_success
18 | end
19 | end
20 |
21 | describe 'Failure' do
22 | shared_examples 'operation fails' do
23 | it 'operation fails' do
24 | expect(result[:tokens]).to eq(tokens_expectation)
25 | expect(result[:semantic_failure]).to eq(:forbidden)
26 | expect(result).to be_failure
27 | end
28 | end
29 |
30 | context 'with invalid refresh token' do
31 | let(:refresh_token) { 'invalid_token' }
32 | let(:tokens_expectation) { nil }
33 |
34 | include_examples 'operation fails'
35 | end
36 |
37 | context 'with unexpired refresh token' do
38 | let(:refresh_token) { create_token(:refresh, :unexpired, account: account) }
39 | let(:tokens_expectation) { false }
40 |
41 | include_examples 'operation fails'
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/concepts/api/v1/users/verifications/show_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::Verifications::Operation::Show do
4 | subject(:result) { described_class.call(params: params) }
5 |
6 | let(:account) { create(:account) }
7 | let(:email_token) { create_token(:email, account: account) }
8 | let(:params) { { email_token: email_token } }
9 |
10 | describe 'Success' do
11 | it 'verifies user account' do
12 | expect(Api::V1::Users::Lib::Operation::DecryptEmailToken).to receive(:call).and_call_original
13 | expect(UserMailer).to receive(:verification_successful).with(email: account.email).and_call_original
14 | expect { result }
15 | .to change { account.reload.verified }.from(false).to(true)
16 | .and change { User.exists?(account_id: account.id) }.from(false).to(true)
17 | expect(result).to be_success
18 | end
19 | end
20 |
21 | describe 'Failure' do
22 | context 'when email token errors' do
23 | let(:params) { {} }
24 |
25 | include_examples 'has email token decryption errors'
26 | end
27 |
28 | context 'when user account was already verified' do
29 | let(:errors) { { base: [I18n.t('errors.verification.user_account_already_verified')] } }
30 |
31 | before { account.toggle!(:verified) } # rubocop:disable Rails/SkipsModelValidations
32 |
33 | include_examples 'has validation errors'
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/concepts/application_contract_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationContract do
4 | it { expect(described_class).to be < Reform::Form }
5 | it { expect(described_class.ancestors).to include(Reform::Form::Dry) }
6 | end
7 |
--------------------------------------------------------------------------------
/spec/concepts/application_decorator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationDecorator do
4 | it { expect(described_class).to be < Draper::Decorator }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/concepts/application_operation_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationOperation do
4 | it { expect(described_class).to be < Trailblazer::Operation }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/concepts/application_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationSerializer do
4 | it { expect(described_class.ancestors).to include(JSONAPI::Serializer) }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/concepts/application_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationWorker do
4 | it { expect(described_class.ancestors).to include(Sidekiq::Worker) }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/constants/token_namespace_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Constants::TokenNamespace do
4 | describe 'Constants::TokenNamespace::SESSION' do
5 | it { expect(described_class).to be_const_defined(:SESSION) }
6 | it { expect(described_class::SESSION).to eq('user-account') }
7 | end
8 |
9 | describe 'Constants::TokenNamespace::RESET_PASSWORD' do
10 | it { expect(described_class).to be_const_defined(:RESET_PASSWORD) }
11 | it { expect(described_class::RESET_PASSWORD).to eq('reset-password') }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/controllers/api/v1/users/sessions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Api::V1::Users::SessionsController, type: :controller do
4 | describe '#DELETE #destroy' do
5 | before { post :destroy }
6 |
7 | it { expect(response.body).to include('base', I18n.t('errors.session.invalid_token')) }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/controllers/api_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApiController, type: :controller do
4 | describe 'class settings' do
5 | it { expect(described_class).to be < ActionController::API }
6 | it { is_expected.to be_a(DefaultEndpoint) }
7 | it { is_expected.to be_a(Authentication) }
8 | it { is_expected.to be_a(JWTSessions::RailsAuthorization) }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/controllers/application_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationController, type: :controller do
4 | it { expect(described_class).to be < ActionController::Base }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/factories/accounts.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :account do
5 | email { FFaker::Internet.unique.email }
6 | password { FFaker::Internet.password }
7 | password_confirmation { password }
8 | user { nil }
9 |
10 | trait :with_user do
11 | user
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/factories/admin_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :admin_user do
5 | email { FFaker::Internet.unique.email }
6 | password { FFaker::Internet.password }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/factories/users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :user do
5 | name { FFaker::Name.name }
6 | account
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/features/admin/admin_users/edit_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Admin->Edit', type: :feature do
4 | let(:admin) { create(:admin_user) }
5 | let(:attributes) { attributes_for(:admin_user) }
6 | let(:ensure_sign_in_and_visit) do
7 | sign_in(admin)
8 | visit(edit_admin_admin_user_path(admin))
9 | end
10 |
11 | before do
12 | ensure_sign_in_and_visit
13 |
14 | fill_in('admin_user[email]', with: admin.email)
15 | fill_in('admin_user[password]', with: attributes[:password])
16 | fill_in('admin_user[password_confirmation]', with: attributes[:password])
17 | click_button('commit')
18 | end
19 |
20 | it 'has log in with new password' do
21 | fill_in(I18n.t('active_admin.devise.email.title'), with: admin.email)
22 | fill_in(I18n.t('active_admin.devise.password.title'), with: attributes[:password])
23 | click_button(I18n.t('active_admin.devise.login.title'))
24 |
25 | expect(page).to have_css('.flash_notice', text: I18n.t('devise.sessions.signed_in'))
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/features/admin/admin_users/index_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Admin->Index', type: :feature do
4 | let(:admin) { create(:admin_user) }
5 | let(:ensure_sign_in_and_visit) do
6 | sign_in(admin)
7 | visit(admin_admin_users_path)
8 | end
9 |
10 | before { ensure_sign_in_and_visit }
11 |
12 | it 'shows admins table' do
13 | within('#index_table_admin_users tbody') do
14 | expect(page).to have_css('tr', count: 1)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/features/admin/admin_users/show_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Admin->Show', type: :feature do
4 | let(:admin) { create(:admin_user) }
5 | let(:ensure_sign_in_and_visit) do
6 | sign_in(admin)
7 | visit(admin_admin_user_path(admin))
8 | end
9 |
10 | before { ensure_sign_in_and_visit }
11 |
12 | it 'shows admin panel' do
13 | within("#attributes_table_admin_user_#{admin.id}") do
14 | expect(page).to have_css('.row-id', text: admin.id)
15 | expect(page).to have_css('.row-created_at', text: I18n.l(admin.created_at, format: :long))
16 | expect(page).to have_css('.row-email', text: admin.email)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/features/admin/check_root_path_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'RootPath' do
4 | context 'when visit root path' do
5 | before { visit(root_path) }
6 |
7 | it 'has redirect to admin login page' do
8 | expect(page).to have_current_path(new_admin_user_session_path)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/features/admin/sign_in_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'SignIn' do
4 | before { visit(new_admin_user_session_path) }
5 |
6 | context 'with the valid email & password' do
7 | let(:admin) { create(:admin_user) }
8 |
9 | before do
10 | fill_in(I18n.t('active_admin.devise.email.title'), with: admin.email)
11 | fill_in(I18n.t('active_admin.devise.password.title'), with: admin.password)
12 | click_button(I18n.t('active_admin.devise.login.title'))
13 | end
14 |
15 | it 'signed in successfully' do
16 | expect(page).to have_css('.flash_notice', text: I18n.t('devise.sessions.signed_in'))
17 | end
18 | end
19 |
20 | context 'with the invalid email or password' do
21 | let(:admin) { attributes_for(:admin_user) }
22 |
23 | before do
24 | fill_in(I18n.t('active_admin.devise.email.title'), with: admin[:email])
25 | fill_in(I18n.t('active_admin.devise.password.title'), with: admin[:password])
26 | click_button(I18n.t('active_admin.devise.login.title'))
27 | end
28 |
29 | it 'shows error messages' do
30 | expect(page).to have_css('.flash_alert', text: I18n.t('devise.failure.invalid', authentication_keys: 'Email'))
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/features/admin/sign_out_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'SignOut' do
4 | let(:admin) { create(:admin_user) }
5 | let(:ensure_sign_in_and_visit) do
6 | sign_in(admin)
7 | visit(edit_admin_admin_user_path(admin))
8 | end
9 |
10 | before { ensure_sign_in_and_visit }
11 |
12 | context 'when a logged in user log out' do
13 | before do
14 | within('#utility_nav') { click_link(I18n.t('active_admin.logout')) }
15 | end
16 |
17 | it 'sign out successfully' do
18 | expect(page).to have_css('#admin_user_email_input', text: I18n.t('active_admin.devise.email.title'))
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/jobs/application_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationJob do
4 | it { expect(described_class).to be < ActiveJob::Base }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/lib/macro/assign_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Macro do
4 | describe '.Assign' do
5 | subject(:result) { described_class::Assign(**params)[:task].call(ctx, **flow_options) }
6 |
7 | let(:params) { {} }
8 |
9 | let(:object) { instance_double('SomeObject', some_method: 'method context') }
10 | let(:ctx) { { object: object } }
11 | let(:flow_options) { { options: [] } }
12 |
13 | context 'when path passed' do
14 | let(:to) { :model }
15 | let(:params) { { to: to, path: %i[object some_method] } }
16 |
17 | it 'assigns object.some_method context into specified ctx key' do
18 | result
19 | expect(ctx[to]).to eq(object.some_method)
20 | end
21 | end
22 |
23 | context 'when value passed' do
24 | let(:to) { :model }
25 | let(:value) { :value }
26 | let(:params) { { to: to, path: %i[object some_method], value: value } }
27 |
28 | it 'assigns specified value into specified ctx key' do
29 | result
30 | expect(ctx[to]).to eq(value)
31 | end
32 | end
33 |
34 | context 'when try is set up' do
35 | let(:to) { :model }
36 | let(:object) { nil }
37 | let(:params) { { to: to, path: %i[object not_existing_method], try: true } }
38 |
39 | it 'uses safe call' do
40 | expect(object).to receive(:try).with(:not_existing_method).and_call_original
41 | result
42 | expect(ctx[to]).to be_nil
43 | end
44 | end
45 |
46 | it 'has uniqueness id' do
47 | params = { to: :path }
48 | expect(described_class::Assign(params)[:id]).not_to eq(described_class::Assign(params)[:id])
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/lib/macro/inject_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Macro do
4 | describe '.Inject' do
5 | subject(:result) { described_class::Inject(**deps)[:task].call(ctx, **flow_options) }
6 |
7 | let(:ctx) { {} }
8 | let(:deps) { { service: dependency } }
9 | let(:dependency_class) { 'DependencyClass' }
10 | let(:flow_options) { { options: [] } }
11 |
12 | context 'when passed string as value' do
13 | let(:dependency) { 'service.name' }
14 |
15 | it 'sets to context dependencies through dry container' do
16 | expect(Container).to receive(:[]).with(dependency).and_return(dependency_class)
17 | expect { result }
18 | .to change { ctx[:service] }
19 | .from(nil)
20 | .to(dependency_class)
21 | end
22 | end
23 |
24 | context 'when passed not string as value' do
25 | let(:dependency) { :service_name }
26 |
27 | it 'sets to context dependencies directly from value' do
28 | expect { result }
29 | .to change { ctx[:service] }
30 | .from(nil)
31 | .to(dependency)
32 | end
33 | end
34 |
35 | describe 'macro id' do
36 | it 'has formatted id' do
37 | expect(described_class::Inject({})[:id]).to macro_id_with('inject')
38 | end
39 |
40 | it 'has uniqueness id' do
41 | expect(described_class::Inject()[:id]).not_to eq(described_class::Inject()[:id])
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/lib/macro/semantic_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Macro do
4 | describe '.Semantic' do
5 | subject(:result) { operation.call({}) }
6 |
7 | shared_examples 'sets semantic into context' do
8 | it { expect(result[semantic_marker]).to eq(:semantic_context) }
9 | end
10 |
11 | context 'when success value passed' do
12 | let(:semantic_marker) { :semantic_success }
13 | let(:operation) do
14 | Class.new(Trailblazer::Operation) do
15 | step Macro::Semantic(success: :semantic_context)
16 | end
17 | end
18 |
19 | include_examples 'sets semantic into context'
20 | end
21 |
22 | context 'when failure value passed' do
23 | let(:semantic_marker) { :semantic_failure }
24 | let(:operation) do
25 | Class.new(Trailblazer::Operation) do
26 | step Macro::Semantic(failure: :semantic_context)
27 | end
28 | end
29 |
30 | include_examples 'sets semantic into context'
31 | end
32 |
33 | describe 'macro id' do
34 | it 'has formatted id' do
35 | expect(described_class::Semantic({})[:id]).to macro_id_with('semantic')
36 | end
37 |
38 | it 'has uniqueness id' do
39 | expect(described_class::Semantic()[:id]).not_to eq(described_class::Semantic()[:id])
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/lib/service/json_api/hash_error_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Service::JsonApi::HashErrorSerializer do
4 | describe 'class settings' do
5 | it { expect(described_class).to be < Service::JsonApi::BaseErrorSerializer }
6 | end
7 |
8 | describe '.call' do
9 | subject(:service) { described_class.call(error_data_structure) }
10 |
11 | include_examples 'error data structure'
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/lib/service/json_api/paginator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Service::JsonApi::Paginator do
4 | subject(:paginator) { described_class.call(resource_path: resource_path, pagy: pagy) }
5 |
6 | describe 'defined constants' do
7 | it { expect(described_class).to be_const_defined(:QUERY_PAGE_PARAMETER) }
8 | end
9 |
10 | describe '.call' do
11 | let(:resource_path) { 'resource_path' }
12 | let(:query_page_parameter) { Service::JsonApi::Paginator::QUERY_PAGE_PARAMETER }
13 |
14 | context 'when only 1 page available' do
15 | let(:pagy) { OpenStruct.new(page: 1, next: nil, prev: nil, pages: 1) }
16 |
17 | it 'returns object with page links' do
18 | expect(paginator).to eq(
19 | self: "#{resource_path}?#{query_page_parameter}=1",
20 | first: resource_path,
21 | next: nil,
22 | prev: nil,
23 | last: "#{resource_path}?#{query_page_parameter}=1"
24 | )
25 | end
26 | end
27 |
28 | context 'when has several pages' do
29 | let(:pagy) { OpenStruct.new(page: 2, next: 3, prev: 1, pages: 3) }
30 |
31 | it 'returns object with page links' do
32 | expect(paginator).to eq(
33 | self: "#{resource_path}?#{query_page_parameter}=2",
34 | first: resource_path,
35 | next: "#{resource_path}?#{query_page_parameter}=3",
36 | prev: "#{resource_path}?#{query_page_parameter}=1",
37 | last: "#{resource_path}?#{query_page_parameter}=3"
38 | )
39 | end
40 | end
41 |
42 | context 'when resource path not passed' do
43 | let(:pagy) { instance_double('Pagy', blank?: true) }
44 |
45 | it 'sets nill for blank pages' do
46 | %i[page next prev pages].each { |method| allow(pagy).to receive(method) }
47 | expect(paginator.compact).to eq(first: resource_path)
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/lib/service/json_api/resource_error_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Service::JsonApi::ResourceErrorSerializer do
4 | describe 'class settings' do
5 | it { expect(described_class).to be < Service::JsonApi::BaseErrorSerializer }
6 | it { expect(described_class::ERRORS_SOURCES).to eq(Service::JsonApi::BaseErrorSerializer::ERRORS_SOURCES) }
7 | end
8 |
9 | describe '.call' do
10 | subject(:service) { described_class.call('contract.default' => contract) }
11 |
12 | let(:contract) { instance_double('Contract') }
13 |
14 | before { allow(contract).to receive_message_chain(:errors, :messages) { error_data_structure } }
15 |
16 | include_examples 'error data structure'
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/lib/service/json_api/resource_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Service::JsonApi::ResourceSerializer do
4 | describe '.call' do
5 | class TestSerializer # rubocop:disable RSpec/LeakyConstantDeclaration
6 | include JSONAPI::Serializer
7 | set_type :test_object
8 | attributes :test_attribute
9 | end
10 |
11 | subject(:jsonapi_serializer) { described_class.call(result_instance) }
12 |
13 | let(:test_object) { instance_double('TestObject', id: 1, test_attribute: 'test_attribute') }
14 | let(:result_instance) do
15 | {
16 | renderer:
17 | {
18 | serializer: TestSerializer,
19 | meta: 'meta'
20 | },
21 | model: test_object
22 | }
23 | end
24 |
25 | it 'return test serializer instance' do
26 | expect(jsonapi_serializer).to be_an_instance_of(TestSerializer)
27 | expect(jsonapi_serializer.to_hash).to include(
28 | data:
29 | {
30 | attributes: { test_attribute: 'test_attribute' },
31 | id: '1',
32 | type: :test_object
33 | },
34 | meta: 'meta'
35 | )
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/lib/service/pagy_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Service::Pagy do
4 | subject(:pagy_service) { described_class.call(collection, call_options) }
5 |
6 | let(:call_options) { { **params } }
7 | let(:params) { { page: 2, items: 1 } }
8 |
9 | context 'when active relation collection' do
10 | let(:collection) { Set.new(%w[1 2]) }
11 | let(:pagy_method) { :pagy }
12 |
13 | it 'prepares params class variable and calls pagy' do
14 | expect(described_class).to receive(pagy_method).with(collection, params)
15 | pagy_service
16 | end
17 | end
18 |
19 | context 'when array collection' do
20 | let(:collection) { %w[1 2] }
21 | let(:pagy_method) { :pagy_array }
22 |
23 | it 'prepares params class variable and calls pagy' do
24 | expect(described_class).to receive(pagy_method).with(collection, params)
25 | pagy_service
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/mailers/application_mailer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationMailer, type: :mailer do
4 | it { expect(described_class).to be < ActionMailer::Base }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/models/account_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Account, type: :model do
4 | describe 'fields' do
5 | it { is_expected.to have_db_column(:email).of_type(:string) }
6 | it { is_expected.to have_db_column(:password_digest).of_type(:string) }
7 | it { is_expected.to have_db_column(:verified).of_type(:boolean).with_options(default: false) }
8 | it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
9 | it { is_expected.to have_db_column(:updated_at).of_type(:datetime) }
10 | end
11 |
12 | describe 'model relations' do
13 | it { is_expected.to have_one(:user).dependent(:destroy) }
14 | end
15 |
16 | it { is_expected.to have_secure_password }
17 | end
18 |
--------------------------------------------------------------------------------
/spec/models/application_record_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationRecord, type: :model do
4 | it { expect(described_class).to be < ActiveRecord::Base }
5 | it { expect(described_class.abstract_class).to be(true) }
6 | end
7 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe User, type: :model do
4 | describe 'model fields' do
5 | it { is_expected.to have_db_column(:name).of_type(:string) }
6 | it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
7 | it { is_expected.to have_db_column(:updated_at).of_type(:datetime) }
8 | end
9 |
10 | describe 'model relations' do
11 | it { is_expected.to belong_to(:account) }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 | require 'spec_helper'
5 |
6 | ENV['RAILS_ENV'] ||= 'test'
7 | require File.expand_path('../config/environment', __dir__)
8 |
9 | %w[api_doc support].each do |dir|
10 | Dir[Rails.root.join('spec', dir, '**', '*.rb')].sort.each do |file|
11 | require file unless file[/\A.+_spec\.rb\z/]
12 | end
13 | end
14 |
15 | abort('The Rails environment is running in production mode!') if Rails.env.production?
16 |
17 | require 'rspec/rails'
18 |
19 | begin
20 | ActiveRecord::Migration.maintain_test_schema!
21 | rescue ActiveRecord::PendingMigrationError => e
22 | puts e.to_s.strip
23 | exit 1
24 | end
25 |
26 | Rails.application.load_tasks
27 | Rake::Task['factory_bot:lint'].invoke
28 |
29 | RSpec.configure do |config|
30 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
31 | config.use_transactional_fixtures = true
32 | config.infer_spec_type_from_file_location!
33 | config.filter_rails_from_backtrace!
34 | config.infer_base_class_for_anonymous_controllers = false
35 | config.mock_with :rspec do |mocks|
36 | mocks.allow_message_expectations_on_nil = true
37 | end
38 |
39 | config.include FactoryBot::Syntax::Methods
40 | config.include ActiveSupport::Testing::TimeHelpers
41 | config.include Devise::Test::IntegrationHelpers, type: :feature
42 | config.include Warden::Test::Helpers, type: :feature
43 | config.include Helpers::RootHelpers
44 | config.include Helpers::OperationHelpers, type: :operation
45 | config.include Helpers::RequestHelpers, type: :request
46 | end
47 |
48 | require 'swagger_helper'
49 |
--------------------------------------------------------------------------------
/spec/requests/api/v1/users/session/refreshes_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Api::V1::Users::Session::Refreshes', type: :request do
4 | let(:account) { create(:account) }
5 |
6 | describe 'POST #create' do
7 | path '/api/v1/users/session/refresh' do
8 | post('create refresh') do
9 | tags 'Users'
10 | consumes 'application/json'
11 | parameter name: :'X-Refresh-Token', in: :header, type: :string
12 |
13 | response(403, 'when unexpired access token') do
14 | let(:'X-Refresh-Token') { create_token(:refresh, :unexpired, account: account) }
15 |
16 | run_test! do
17 | expect(response).to be_forbidden
18 | expect(response.body).to be_empty
19 | end
20 | end
21 |
22 | response(401, 'when user unauthenticated') do
23 | let(:'X-Refresh-Token') { nil }
24 |
25 | include_examples 'renders unauthenticated errors'
26 | end
27 |
28 | response(201, 'successful renders refreshed tokens bundle in meta') do
29 | let(:'X-Refresh-Token') { create_token(:refresh, :expired, account: account) }
30 |
31 | run_test! do
32 | expect(response).to be_created
33 | expect(response).to match_json_schema('v1/users/session/refresh/create')
34 | end
35 | end
36 | end
37 | end
38 |
39 | describe 'N+1', :n_plus_one do
40 | let(:headers) { { 'X-Refresh-Token': refresh_token } }
41 | let(:refresh_token) { create_token(:refresh, :expired, account: account) }
42 |
43 | populate { |n| create_list(:account, n) }
44 |
45 | specify do
46 | expect { post '/api/v1/users/session/refresh', headers: headers, as: :json }
47 | .to perform_constant_number_of_queries
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/requests/api/v1/users/verifications_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Api::V1::Users::Verifications', type: :request do
4 | let(:account) { create(:account) }
5 | let(:email_token) { create_token(:email, account: account) }
6 | let(:confirmation_url) { "/api/v1/users/verification?email_token=#{email_token}" }
7 |
8 | describe 'GET #show' do
9 | path '/api/v1/users/verification?email_token={email_token}' do
10 | get('show verification') do
11 | tags 'Users'
12 | produces 'application/json'
13 | parameter name: :email_token, in: :path, type: :string
14 |
15 | response(404, 'when user account not found') do
16 | let(:not_exising_account) { instance_double('Account', id: account.id.next) }
17 | let(:email_token) { create_token(:email, account: not_exising_account) }
18 |
19 | include_examples 'returns not found status'
20 | end
21 |
22 | response(422, 'when wrong email token') do
23 | let(:email_token) { 'invalid_token' }
24 |
25 | include_examples 'renders unprocessable entity errors'
26 | end
27 |
28 | response(204, 'successful verifies user account') do
29 | run_test!
30 | end
31 | end
32 | end
33 |
34 | describe 'N+1', :n_plus_one do
35 | before { get confirmation_url }
36 |
37 | populate { |n| create_list(:account, n) }
38 |
39 | specify do
40 | expect { get confirmation_url }.to perform_constant_number_of_queries
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.configure do |config|
4 | config.expect_with :rspec do |expectations|
5 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6 | end
7 |
8 | config.mock_with :rspec do |mocks|
9 | mocks.verify_partial_doubles = true
10 | end
11 |
12 | config.shared_context_metadata_behavior = :apply_to_host_groups
13 | config.filter_run_when_matching :focus
14 | config.example_status_persistence_file_path = 'spec/examples.txt'
15 | config.disable_monkey_patching!
16 | config.default_formatter = 'doc' if config.files_to_run.one?
17 | config.profile_examples = 10
18 | config.order = :random
19 |
20 | Kernel.srand config.seed
21 | end
22 |
--------------------------------------------------------------------------------
/spec/support/config/capybara.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'capybara/rails'
4 | require 'capybara/rspec'
5 | require 'webdrivers/chromedriver'
6 |
7 | Capybara.register_driver(:chrome) do |app|
8 | capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
9 | # Enables access to logs with `page.driver.manage.get_log(:browser)`
10 | loggingPrefs: {
11 | browser: 'ALL',
12 | client: 'ALL',
13 | driver: 'ALL',
14 | server: 'ALL'
15 | }
16 | )
17 |
18 | options = Selenium::WebDriver::Chrome::Options.new
19 | options.add_argument('window-size=1920,1080')
20 |
21 | # Run headless by default unless CHROME_HEADLESS specified
22 | options.add_argument('headless') unless /^(false|no|0)$/.match?(ENV['CHROME_HEADLESS'])
23 |
24 | Capybara::Selenium::Driver.new(
25 | app,
26 | browser: :chrome,
27 | desired_capabilities: capabilities,
28 | options: options
29 | )
30 | end
31 |
32 | Capybara.configure do |config|
33 | config.default_driver = :chrome
34 | config.javascript_driver = :chrome
35 | config.server = :puma, { Silent: true }
36 | end
37 |
--------------------------------------------------------------------------------
/spec/support/config/ffaker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.configure do |config|
4 | config.before(:all) { FFaker::Random.seed = config.seed }
5 | config.before { FFaker::Random.reset! }
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/config/json_matchers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | JsonMatchers.schema_root = 'spec/support/schemas'
4 |
--------------------------------------------------------------------------------
/spec/support/config/n_plus_one_control.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'n_plus_one_control/rspec'
4 |
--------------------------------------------------------------------------------
/spec/support/config/shoulda_matchers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Shoulda::Matchers.configure do |config|
4 | config.integrate do |with|
5 | with.test_framework :rspec
6 | with.library :rails
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/config/sidekiq.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Sidekiq.configure do |config|
4 | config.clear_all_enqueued_jobs = true
5 | config.enable_terminal_colours = true
6 | config.warn_when_jobs_not_processed_by_sidekiq = false
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/helpers/operation_helpers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Helpers
4 | module OperationHelpers
5 | def create_available_columns(*columns)
6 | Api::V1::Lib::Service::JsonApi::ColumnsBuilder.call(*columns)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/helpers/request_helpers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Helpers
4 | module RequestHelpers
5 | def auth_header(account)
6 | "Bearer #{create_token(:access, account: account)}"
7 | end
8 |
9 | def resource_type(response)
10 | body = JSON.parse(response.body)
11 | dig_params = body['data'].is_a?(Array) ? ['data', 0, 'type'] : %w[data type]
12 | body.dig(*dig_params)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/support/helpers/root_helpers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Helpers
4 | module RootHelpers
5 | # :reek:TooManyStatements
6 | def create_token(type, access_token = :unexpired, account: nil, exp: nil, namespace: nil) # rubocop:disable Metrics/AbcSize
7 | payload, current_time = { account_id: account&.id || rand(1..42), namespace: namespace }, Time.now.to_i
8 |
9 | time =
10 | if access_token.eql?(:expired)
11 | allow(JWTSessions).to receive(:access_exp_time).and_return(0)
12 | current_time - 10
13 | else
14 | exp || 24.hours.from_now.to_i
15 | end
16 |
17 | return JWT.encode(payload.merge(exp: time), Constants::Shared::HMAC_SECRET) if type.eql?(:email)
18 |
19 | JWTSessions::Session.new(
20 | access_claims: { exp: current_time },
21 | payload: payload
22 | ).login[type]
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/support/matchers/macro_id_with.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define(:macro_id_with) do |macro_name|
4 | match { |macro_id| /\Amacro\/#{macro_name}_id_\d+\z/.match?(macro_id) }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/matchers/not_change.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define_negated_matcher :not_change, :change
4 |
--------------------------------------------------------------------------------
/spec/support/schemas/errors.json:
--------------------------------------------------------------------------------
1 | {
2 | "definitions": {},
3 | "$schema": "http://json-schema.org/draft-07/schema#",
4 | "$id": "http://example.com/errors.json",
5 | "type": "object",
6 | "title": "The Errors Schema",
7 | "required": [
8 | "errors"
9 | ],
10 | "properties": {
11 | "errors": {
12 | "$id": "#/properties/errors",
13 | "type": "array",
14 | "title": "The Errors Schema",
15 | "items": {
16 | "$id": "#/properties/errors/items",
17 | "type": "object",
18 | "title": "The Items Schema",
19 | "required": [
20 | "detail",
21 | "source"
22 | ],
23 | "properties": {
24 | "detail": {
25 | "$id": "#/properties/errors/items/properties/detail",
26 | "type": "string",
27 | "title": "The Detail Schema",
28 | "default": "",
29 | "examples": [
30 | "must be filled"
31 | ],
32 | "pattern": "^(.*)$"
33 | },
34 | "source": {
35 | "$id": "#/properties/errors/items/properties/source",
36 | "type": "object",
37 | "title": "The Source Schema",
38 | "required": [
39 | "pointer"
40 | ],
41 | "properties": {
42 | "pointer": {
43 | "$id": "#/properties/errors/items/properties/source/properties/pointer",
44 | "type": "string",
45 | "title": "The Pointer Schema",
46 | "default": "",
47 | "examples": [
48 | "/data/attributes/first-name"
49 | ],
50 | "pattern": "^(.*)$"
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/operation/has_email_token_decryption_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'has email token decryption errors' do
4 | context 'without params' do
5 | let(:params) { {} }
6 | let(:errors) { { email_token: [I18n.t('errors.key?')] } }
7 |
8 | include_examples 'has validation errors'
9 | end
10 |
11 | context 'with empty params' do
12 | let(:params) { { email_token: '' } }
13 | let(:errors) { { email_token: [I18n.t('errors.filled?')] } }
14 |
15 | include_examples 'has validation errors'
16 | end
17 |
18 | context 'with wrong params type' do
19 | let(:params) { { email_token: true } }
20 | let(:errors) { { email_token: [I18n.t('errors.str?')] } }
21 |
22 | include_examples 'has validation errors'
23 | end
24 |
25 | context 'with invalid email token' do
26 | let(:params) { { email_token: 'invalid_token' } }
27 | let(:errors) { { base: [I18n.t('errors.verification.invalid_email_token')] } }
28 |
29 | include_examples 'has validation errors'
30 | end
31 |
32 | context 'when account not found' do
33 | let(:params) { { email_token: email_token } }
34 |
35 | before { account.destroy }
36 |
37 | it 'sets semantic failure not_found' do
38 | expect(result[:semantic_failure]).to eq(:not_found)
39 | expect(result).to be_failure
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/operation/has_email_token_equality_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'has email token equality errors' do
4 | shared_examples 'email token equality errors' do
5 | let(:errors) { { base: [I18n.t('errors.email_token.already_used')] } }
6 |
7 | include_examples 'has validation errors'
8 | end
9 |
10 | context 'when email token not eql email token in redis' do
11 | before do
12 | Api::V1::Users::Lib::Service::RedisAdapter.push_token(
13 | create_token(:email, account: account, exp: 1.hour.from_now.to_i)
14 | )
15 | end
16 |
17 | it_behaves_like 'email token equality errors'
18 | end
19 |
20 | context 'when email token not exists in redis' do
21 | before { Api::V1::Users::Lib::Service::RedisAdapter.delete_token(email_token) }
22 |
23 | it_behaves_like 'email token equality errors'
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/operation/has_validation_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'has validation errors' do |namespace: :default|
4 | it 'has validation errors' do
5 | expect(result["contract.#{namespace}"].errors.messages).to eq(errors)
6 | expect(result).to be_failure
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/operation/nested_inclusion_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'nested inclusion errors' do
4 | shared_examples 'not successful operation' do
5 | it 'sets inclusion validation errors' do
6 | expect(result['contract.uri_query'].errors.messages).to eq(errors)
7 | expect(result).to be_failure
8 | end
9 | end
10 |
11 | context 'with invalid inclusion parameter' do
12 | let(:inclusion_params) { 'invalid_inclusion_parameter' }
13 | let(:errors) { { include: [I18n.t('errors.inclusion_params_valid?')] } }
14 |
15 | include_examples 'not successful operation'
16 | end
17 |
18 | context 'with not uniq inclusion parameters' do
19 | let(:params) { { include: "#{inclusion_params},#{inclusion_params}" } }
20 | let(:errors) { { include: [I18n.t('errors.inclusion_params_uniq?')] } }
21 |
22 | include_examples 'not successful operation'
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/request/renders_bad_request_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'renders bad_request errors' do
4 | run_test! do |response|
5 | expect(response).to be_bad_request
6 | expect(response).to match_json_schema('errors')
7 | expect(JSON.parse(response.body)).to match_array(bad_request_error) if defined? bad_request_error
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/request/renders_unauthenticated_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'renders unauthenticated errors' do
4 | run_test! do
5 | expect(response).to be_unauthorized
6 | expect(response).to match_json_schema('errors')
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/request/renders_unprocessable_entity_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'renders unprocessable entity errors' do
4 | run_test! do |response|
5 | expect(response).to be_unprocessable
6 | expect(response).to match_json_schema('errors')
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/request/renders_uri_query_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'renders uri query errors' do
4 | run_test! do |response|
5 | expect(response).to be_bad_request
6 | expect(response).to match_json_schema('errors')
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/request/returns_not_found_status.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'returns not found status' do
4 | run_test! do |response|
5 | expect(response).to be_not_found
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/swagger_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 | RSpec.configure do |config|
5 | # Specify a root folder where Swagger JSON files are generated
6 | # NOTE: If you're using the rswag-api to serve API descriptions, you'll need
7 | # to ensure that it's configured to serve Swagger from the same folder
8 | config.swagger_root = Rails.root.join('swagger').to_s
9 |
10 | # Define one or more Swagger documents and provide global metadata for each one
11 | # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
12 | # be generated at the provided relative path under swagger_root
13 | # By default, the operations defined in spec files are added to the first
14 | # document below. You can override this behavior by adding a swagger_doc tag to the
15 | # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
16 | config.swagger_docs = {
17 | 'v1/swagger.yaml' => {
18 | openapi: '3.0.1',
19 | info: {
20 | title: 'API V1',
21 | version: 'v1'
22 | },
23 | paths: {},
24 | servers: [
25 | {
26 | url: 'https://{defaultHost}',
27 | variables: {
28 | defaultHost: {
29 | default: 'localhost:3000'
30 | }
31 | }
32 | }
33 | ]
34 | }
35 | }
36 |
37 | # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
38 | # The swagger_docs configuration option has the filename including format in
39 | # the key, this may want to be changed to avoid putting yaml in json files.
40 | # Defaults to json. Accepts ':json' and ':yaml'.
41 | config.swagger_format = :yaml
42 | end
43 |
--------------------------------------------------------------------------------
/spec/uploaders/application_uploader_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationUploader do
4 | it { expect(described_class).to be < Shrine }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/application_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe ApplicationWorker, type: :worker do
4 | it { expect(described_class.included_modules).to include(Sidekiq::Worker) }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/sentry_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SentryWorker, type: :worker do
4 | subject(:worker) { described_class.new }
5 |
6 | let(:event) { 'event' }
7 |
8 | it 'adds job to a queue' do
9 | described_class.perform_async(event)
10 | expect(described_class).to have_enqueued_sidekiq_job(event)
11 | end
12 |
13 | it 'calls Raven.send_event with the correct arguments' do
14 | allow(Raven).to receive(:send_event)
15 | expect(Raven).to receive(:send_event).with(event)
16 | worker.perform(event)
17 | end
18 |
19 | it { is_expected.to be_processed_in('default') }
20 | it { is_expected.to be_retryable(true) }
21 | end
22 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/storage/.keep
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/tmp/.keep
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/boilerplate/8edf94f693f08dc8ca83a35dfb6156cbe176d6f9/vendor/.keep
--------------------------------------------------------------------------------