From f9fe036d551686e34451a08bcbb345b3f9aadf54 Mon Sep 17 00:00:00 2001 From: Jan Kaczorowski Date: Tue, 4 Feb 2025 23:18:38 +0100 Subject: [PATCH] progress good progress with Zdrofit API client added new zdrofit endpoints spec mess home page TWcss choosing clubs progress progress progress changes in the client; changes in the UI better dropdowns better dropdowns datetime solidqueue seemingly working, untested yet solid queue job httplog progress good progress varia initial kamal setup local gem build issue possible fix Dockerfile changes more header files to Dockerfile more header files to Dockerfile 2 more header files to Dockerfile 3 more header files to Dockerfile 4 more header files to Dockerfile 5 more header files to Dockerfile 6 more header files to Dockerfile 7 more header files to Dockerfile 8 ruby 3.4.1 compatibility issues discovered; downgrading to Ruby 3.3.1 instead rm tmp more header files to Dockerfile 9 more header files to Dockerfile 10 changes changes 2 changes 3 changes 4 changes 5 adj adj23 pass AILS_MASTER_KEY as secret to builder in deploy.yaml test test test3 test4 test5 test6 secrets fix? SECRET_KEY_BASE_DUMMY=1 trying to fix tailwind build issue tailwind-css-rails-3-3-1 tailwind conf for 3.3.1 trying to fix tailwind for Docker SECRET_KEY_BASE removed from env.secret in deploy.yml try to fix trying to fix built gems injection to final image changes fixing nio4 issue Dockefile change stunts to make local gem work trying to fix bcrypt build issue trying to fix bcrypt build issue, pt 2 local gem struggle trying to revent Dockefile changes ffrom last commit that might have screwed up bckupt again some more headerfiles Dockerfile changes maybe will work more changes: ' trying to make Zeitwerk ignore local gem trying pop fixes for assets zm new server IP trying to fix tailwind assets anohter change changes boo npm install tailwindcss ch dockerfiel chag unpredictibility fix changes stale cont to debug tailwind test 3 tw test tw test 2 tw 3 tw 4 tm 6 adding tailwindcss-rails changes changes2 gradient fix? progress --- .gitignore | 4 + .kamal/secrets | 2 +- .ruby-version | 2 +- Dockerfile | 102 ++-- Gemfile | 21 +- Gemfile.lock | 84 ++- README.md | 25 +- .../stylesheets/application.tailwind.css | 3 + app/controllers/home_controller.rb | 82 +++ app/jobs/booking_checker_job.rb | 7 + app/jobs/class_booker_job.rb | 8 + app/models/zdrofit_class_booking.rb | 15 + app/models/zdrofit_user.rb | 21 +- app/services/class_booker.rb | 35 ++ app/views/home/_weekly_classes.html.erb | 29 ++ app/views/home/dashboard.html.erb | 491 ++++++++++++++++++ app/views/home/index.html.erb | 80 +++ app/views/layouts/application.html.erb | 5 +- config/application.rb | 8 + config/cable.yml | 7 +- config/credentials.yml.enc | 2 +- config/database.yml | 34 +- config/deploy.yml | 37 +- config/environments/development.rb | 24 + config/environments/production.rb | 2 +- config/initializers/zeitwerk.rb | 1 + config/recurring.yml | 18 + config/routes.rb | 8 +- config/tailwind.config.js | 12 + config/tailwind.rb | 7 + db/cable_schema.rb | 14 +- db/cache_schema.rb | 14 +- ...205212548_create_zdrofit_class_bookings.rb | 15 + ...4113_add_last_location_to_zdrofit_users.rb | 6 + ...0207221640_fix_typo_in_table_occurrence.rb | 5 + db/queue_schema.rb | 68 ++- db/schema.rb | 20 +- lib/zdrofit_client/.gitignore | 2 + lib/zdrofit_client/lib/zdrofit_client.rb | 21 + .../lib/zdrofit_client/api_call.rb | 36 ++ .../zdrofit_client/api_calls/book_class.rb | 15 + .../api_calls/cancel_booking.rb | 12 + .../api_calls/get_calendar_filters.rb | 12 + .../api_calls/get_class_tickets.rb | 15 + .../zdrofit_client/api_calls/get_identity.rb | 9 + .../api_calls/get_my_calendar.rb | 9 + .../api_calls/get_personal_id_info.rb | 16 + .../api_calls/get_phone_info.rb | 15 + .../api_calls/get_profile_for_edit.rb | 12 + .../api_calls/list_available_clubs.rb | 9 + .../api_calls/list_weekly_classes.rb | 50 ++ .../summarize_booking_cancellation.rb | 12 + .../lib/zdrofit_client/client.rb | 81 +++ .../lib/zdrofit_client/version.rb | 3 + lib/zdrofit_client/zdrofit_client.gemspec | 13 + .../zdrofit_class_weekly_bookings.rb | 7 + spec/factories/zdrofit_users.rb | 6 + spec/lib/zdrofit_client_spec.rb | 46 ++ .../zdrofit_class_weekly_booking_spec.rb | 5 + spec/rails_helper.rb | 9 + spec/support/factorybot.rb | 3 + spec/support/vcr.rb | 25 + tailwind.config.js | 11 + test/fixtures/zdrofit_users.yml | 7 - 64 files changed, 1635 insertions(+), 144 deletions(-) create mode 100644 app/assets/stylesheets/application.tailwind.css create mode 100644 app/controllers/home_controller.rb create mode 100644 app/jobs/booking_checker_job.rb create mode 100644 app/jobs/class_booker_job.rb create mode 100644 app/models/zdrofit_class_booking.rb create mode 100644 app/services/class_booker.rb create mode 100644 app/views/home/_weekly_classes.html.erb create mode 100644 app/views/home/dashboard.html.erb create mode 100644 app/views/home/index.html.erb create mode 100644 config/initializers/zeitwerk.rb create mode 100644 config/tailwind.config.js create mode 100644 config/tailwind.rb create mode 100644 db/migrate/20250205212548_create_zdrofit_class_bookings.rb create mode 100644 db/migrate/20250205224113_add_last_location_to_zdrofit_users.rb create mode 100644 db/migrate/20250207221640_fix_typo_in_table_occurrence.rb create mode 100644 lib/zdrofit_client/.gitignore create mode 100644 lib/zdrofit_client/lib/zdrofit_client.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_call.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/book_class.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/cancel_booking.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_calendar_filters.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_class_tickets.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_identity.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_my_calendar.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_personal_id_info.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_phone_info.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/get_profile_for_edit.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/list_available_clubs.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/list_weekly_classes.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/api_calls/summarize_booking_cancellation.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/client.rb create mode 100644 lib/zdrofit_client/lib/zdrofit_client/version.rb create mode 100644 lib/zdrofit_client/zdrofit_client.gemspec create mode 100644 spec/factories/zdrofit_class_weekly_bookings.rb create mode 100644 spec/factories/zdrofit_users.rb create mode 100644 spec/lib/zdrofit_client_spec.rb create mode 100644 spec/models/zdrofit_class_weekly_booking_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/support/factorybot.rb create mode 100644 spec/support/vcr.rb create mode 100644 tailwind.config.js delete mode 100644 test/fixtures/zdrofit_users.yml diff --git a/.gitignore b/.gitignore index 1303e39..e2d4393 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ /app/assets/builds/* !/app/assets/builds/.keep +/test/fixtures/zdrofit_users.yml + +# Ignore VCR cassettes +/spec/vcr_cassettes/.DS_Store diff --git a/.kamal/secrets b/.kamal/secrets index 9a771a3..16a6996 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -14,4 +14,4 @@ KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD # Improve security by using a password manager. Never check config/master.key into git! -RAILS_MASTER_KEY=$(cat config/master.key) +RAILS_MASTER_KEY=$(cat config/master.key) \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index 408069a..4a85a55 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.4.1 +ruby-3.3.1 diff --git a/Dockerfile b/Dockerfile index 8427271..038652e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,65 +8,109 @@ # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html # Make sure RUBY_VERSION matches the Ruby version in .ruby-version -ARG RUBY_VERSION=3.4.1 +ARG RUBY_VERSION=3.3.1 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails # Install base packages -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update -qq && \ + apt-get install -y ca-certificates && \ + apt-get install --no-install-recommends -y \ + curl \ + libjemalloc2 \ + libjemalloc-dev \ + libgmp-dev \ + libvips \ + sqlite3 && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives -# Set production environment +# Build stage +FROM base AS build + +# Install build dependencies +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update -qq && \ + apt-get install -y ca-certificates && \ + apt-get install --no-install-recommends -y \ + build-essential \ + git \ + pkg-config \ + ruby-dev \ + make \ + gcc \ + nodejs \ + libssl-dev \ + zlib1g-dev \ + clang \ + llvm \ + npm + +# Install tailwindcss +# Set safe compiler flags +ENV CFLAGS="-O2 -march=x86-64-v2" +ENV CXXFLAGS="-O2 -march=x86-64-v2" +ENV LDFLAGS="-Wl,--no-as-needed" \ + CC="clang" \ + CXX="clang++" + +# Configure bundler ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development" - -# Throw-away build stage to reduce size of final image -FROM base AS build + BUNDLE_BUILD__BCRYPT_PBKDF="--with-cflags='${CFLAGS}'" \ + BUNDLE_BUILD__ED25519="--with-cflags='${CFLAGS}'" \ + BUNDLE_WITHOUT="development test" \ + PORT=80 -# Install packages needed to build gems -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git pkg-config && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives - -# Install application gems +# Install gems COPY Gemfile Gemfile.lock ./ -RUN bundle install && \ - rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ - bundle exec bootsnap precompile --gemfile +COPY lib/zdrofit_client lib/zdrofit_client +RUN --mount=type=cache,target=/usr/local/bundle/cache \ + bundle config build.nokogiri --use-system-libraries && \ + bundle config set --local path '/usr/local/bundle' && \ + bundle config set --local deployment 'true' && \ + bundle config set --local without 'development test' && \ + bundle config jobs 1 && \ + bundle config retry 3 && \ + bundle install && \ + rm -rf ~/.bundle/ /usr/local/bundle/ruby/*/cache /usr/local/bundle/ruby/*/bundler/gems/*/.git # Copy application code -COPY . . +COPY --chown=1000:1000 . . +RUN rm -rf tmp + +# Install tailwindcss +RUN npm install tailwindcss @tailwindcss/cli # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ -# Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile - - - - -# Final stage for app image -FROM base +# Precompile assets +RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile # Copy built artifacts: gems, application -COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" -COPY --from=build /rails /rails +# COPY --from=build --chown=1000:1000 /usr/local/bundle /usr/local/bundle +# COPY --from=build --chown=1000:1000 /rails /rails # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp + mkdir -p tmp db log storage && \ + chown -R rails:rails /usr/local/bundle tmp db log storage + USER 1000:1000 +# Keep container running (testing) +# CMD ["tail", "-f", "/dev/null"] + # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start server via Thruster by default, this can be overwritten at runtime EXPOSE 80 CMD ["./bin/thrust", "./bin/rails", "server"] + + diff --git a/Gemfile b/Gemfile index caffd6e..d60accc 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 8.0.1" +gem "nio4r", "2.5.9" # The modern asset pipeline for Rails [https://github.com/rails/propshaft] gem "propshaft" # Use sqlite3 as the database for Active Record @@ -18,9 +19,7 @@ gem "stimulus-rails" gem "tailwindcss-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" - -# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem "net-smtp", "~> 0.5.0" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] @@ -42,15 +41,25 @@ gem "thruster", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" +gem "httparty" +gem "awesome_print" +gem "dotenv" +gem "zdrofit_client", path: "lib/zdrofit_client" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" - + gem "debug", platforms: %i[mri x64_mingw mingw], require: "debug/prelude" + gem "httplog" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] gem "brakeman", require: false # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] gem "rubocop-rails-omakase", require: false + + gem "rspec-rails" + gem "vcr" + gem "webmock" + gem "factory_bot_rails" end group :development do @@ -63,3 +72,5 @@ group :test do gem "capybara" gem "selenium-webdriver" end + +gem "levenshtein", "~> 0.2.2" diff --git a/Gemfile.lock b/Gemfile.lock index 743780e..4450f0e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,9 @@ +PATH + remote: lib/zdrofit_client + specs: + zdrofit_client (0.1.0) + httparty + GEM remote: https://rubygems.org/ specs: @@ -75,8 +81,10 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) + awesome_print (1.9.2) base64 (0.2.0) bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) benchmark (0.4.0) bigdecimal (3.1.9) bindex (0.8.1) @@ -96,22 +104,40 @@ GEM xpath (~> 3.2) concurrent-ruby (1.3.5) connection_pool (2.5.0) + crack (1.0.0) + bigdecimal + rexml crass (1.0.6) + csv (3.3.2) date (3.4.1) debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.5.1) dotenv (3.1.7) drb (2.2.1) ed25519 (1.3.0) erubi (1.13.1) et-orbi (1.2.11) tzinfo + factory_bot (6.5.1) + activesupport (>= 6.1.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) + railties (>= 5.0.0) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + hashdiff (1.1.2) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) + httplog (1.7.0) + rack (>= 2.0) + rainbow (>= 2.0.0) i18n (1.14.7) concurrent-ruby (~> 1.0) importmap-rails (2.1.0) @@ -139,6 +165,7 @@ GEM thor (~> 1.3) zeitwerk (>= 2.6.18, < 3.0) language_server-protocol (3.17.0.4) + levenshtein (0.2.2) logger (1.6.5) loofah (2.24.0) crass (~> 1.0.2) @@ -153,6 +180,8 @@ GEM mini_mime (1.1.5) minitest (5.25.4) msgpack (1.7.5) + multi_xml (0.7.1) + bigdecimal (~> 3.1) net-imap (0.5.5) date net-protocol @@ -164,9 +193,10 @@ GEM net-ssh (>= 2.6.5, < 8.0.0) net-sftp (4.0.0) net-ssh (>= 5.0.0, < 8.0.0) - net-smtp (0.5.0) + net-smtp (0.5.1) + net-protocol net-ssh (7.3.0) - nio4r (2.7.4) + nio4r (2.5.9) nokogiri (1.18.2-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.18.2-aarch64-linux-musl) @@ -247,6 +277,23 @@ GEM reline (0.6.0) io-console (~> 0.5) rexml (3.4.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (7.1.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.2) rubocop (1.71.2) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -316,15 +363,13 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.2) - tailwindcss-rails (4.0.0) + tailwindcss-rails (3.3.1) railties (>= 7.0.0) - tailwindcss-ruby (~> 4.0) - tailwindcss-ruby (4.0.3) - tailwindcss-ruby (4.0.3-aarch64-linux-gnu) - tailwindcss-ruby (4.0.3-aarch64-linux-musl) - tailwindcss-ruby (4.0.3-arm64-darwin) - tailwindcss-ruby (4.0.3-x86_64-linux-gnu) - tailwindcss-ruby (4.0.3-x86_64-linux-musl) + tailwindcss-ruby (~> 3.0) + tailwindcss-ruby (3.4.17-aarch64-linux) + tailwindcss-ruby (3.4.17-arm-linux) + tailwindcss-ruby (3.4.17-arm64-darwin) + tailwindcss-ruby (3.4.17-x86_64-linux) thor (1.3.2) thruster (0.1.10) thruster (0.1.10-aarch64-linux) @@ -341,11 +386,17 @@ GEM unicode-emoji (4.0.4) uri (1.0.2) useragent (0.16.11) + vcr (6.3.1) + base64 web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webmock (3.24.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) websocket-driver (0.7.7) base64 @@ -361,22 +412,32 @@ PLATFORMS aarch64-linux-musl arm-linux-gnu arm-linux-musl + arm64-darwin-23 arm64-darwin-24 x86_64-linux x86_64-linux-gnu x86_64-linux-musl DEPENDENCIES + awesome_print bootsnap brakeman capybara debug + dotenv + factory_bot_rails + httparty + httplog importmap-rails jbuilder kamal + levenshtein (~> 0.2.2) + net-smtp (~> 0.5.0) + nio4r (= 2.5.9) propshaft puma (>= 5.0) rails (~> 8.0.1) + rspec-rails rubocop-rails-omakase selenium-webdriver solid_cable @@ -388,7 +449,10 @@ DEPENDENCIES thruster turbo-rails tzinfo-data + vcr web-console + webmock + zdrofit_client! BUNDLED WITH 2.6.3 diff --git a/README.md b/README.md index 7db80e4..7768bf8 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,25 @@ # README -This README would normally document whatever steps are necessary to get the -application up and running. +## Zdrofit booker -Things you may want to cover: +Zdrofit is a major gym network in Poland. It provides premium gym space and fitness classes with reputable trainers. However, attending the classes often requires booking, with the best trainers' classes being fully booked within 10-15 minutes from the time booking opens. -* Ruby version +I wrote an app to automate the process to make sure the user gets booked just in time! -* System dependencies -* Configuration +## Purpose -* Database creation +Auto-booking Zdrofit fitness classes without you having to watch them and manually subscribe to each and every one of them with your mobile app -* Database initialization -* How to run the test suite +## Scope -* Services (job queues, cache servers, search engines, etc.) +- Zdrofit API client (as a local gem, in lib folder) +- Scheduler on top of SQLite and SolidQueue +- simple TailwindCSS powered UI -* Deployment instructions +## Caveats -* ... +Because I'm not using the most official API, but rather peripheral one, mimicking user actions, I have no ability to use the most apporpriate course of interaction - OAUTH. Instead, I'm relying on saving username and password in the DB (encrypted, but still), for the purpose of jobs being able to use them later, when an app has to book some Zdrofit class. + +If you have trust issues - understandable - i suggest you just pull this project, and host anywhere. The slimmest VPS you can find will likely get the job done. \ No newline at end of file diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/app/assets/stylesheets/application.tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..dcc405c --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,82 @@ +class HomeController < ApplicationController + before_action :authenticate_user!, except: %i[index login] + + def index + end + + def login + @user = ZdrofitUser.find_or_create_by(email: params[:email]) do |user| + user.pass = params[:password] + end + + # Test the credentials by trying to login to Zdrofit + begin + @user.zdrofit_client = @user.zdrofit_api_client + session[:user_id] = @user.id + redirect_to dashboard_path, notice: "Successfully logged in" + rescue => e + flash[:error] = "Invalid credentials: #{e.message}" + redirect_to root_path + end + end + + def dashboard + @user = ZdrofitUser.find(session[:user_id]) + @client = @user.zdrofit_api_client + @clubs = @client.list_available_clubs + rescue => e + flash[:error] = "Failed to fetch clubs: #{e.message}" + @clubs = [] + end + + def weekly_classes + @user = ZdrofitUser.find(session[:user_id]) + client = @user.zdrofit_api_client + @classes = client.list_weekly_classes( + club_id: params[:club_id], + date: 5.days.from_now.strftime("%F") + ) + render json: @classes + rescue => e + render json: { error: e.message }, status: :unprocessable_entity + end + + def book + @user = ZdrofitUser.find(session[:user_id]) + + begin + # Create booking record with next_occurrence + ZdrofitClassBooking.create!( + zdrofit_user: @user, + class_id: params[:class_id], + club_id: params[:club_id], + next_occurrence: params[:next_occurrence], + class_name: params[:class_name], + trainer_name: params[:trainer_name] + ) + + render json: { success: true } + rescue => e + render json: { success: false, error: e.message } + end + end + + def update_location + @user = ZdrofitUser.find(session[:user_id]) + @user.update_last_location( + city_id: params[:city_id], + club_id: params[:club_id] + ) + head :ok + rescue => e + render json: { error: e.message }, status: :unprocessable_entity + end + + private + + def authenticate_user! + return unless session[:user_id].blank? + + redirect_to index_path and return + end +end diff --git a/app/jobs/booking_checker_job.rb b/app/jobs/booking_checker_job.rb new file mode 100644 index 0000000..889f291 --- /dev/null +++ b/app/jobs/booking_checker_job.rb @@ -0,0 +1,7 @@ +class BookingCheckerJob < ApplicationJob + queue_as :default + + def perform + Rails.logger.info "foo" + end +end diff --git a/app/jobs/class_booker_job.rb b/app/jobs/class_booker_job.rb new file mode 100644 index 0000000..7c98ef5 --- /dev/null +++ b/app/jobs/class_booker_job.rb @@ -0,0 +1,8 @@ +class ClassBookerJob < ApplicationJob + queue_as :default + + def perform(booking_id) + booking = ZdrofitClassBooking.find(booking_id) + ClassBooker.call(booking) + end +end diff --git a/app/models/zdrofit_class_booking.rb b/app/models/zdrofit_class_booking.rb new file mode 100644 index 0000000..0a9b091 --- /dev/null +++ b/app/models/zdrofit_class_booking.rb @@ -0,0 +1,15 @@ +class ZdrofitClassBooking < ApplicationRecord + belongs_to :zdrofit_user + + def booking_time + next_occurrence - 2.days + 1.minute + end + + after_create :book_class + + private + + def book_class + ClassBookerJob.set(wait_until: booking_time).perform_later(id) + end +end diff --git a/app/models/zdrofit_user.rb b/app/models/zdrofit_user.rb index 45ba07e..ba1bf4d 100644 --- a/app/models/zdrofit_user.rb +++ b/app/models/zdrofit_user.rb @@ -1,4 +1,21 @@ class ZdrofitUser < ApplicationRecord - encrypts :email - encrypts :pass + encrypts :email, deterministic: true + encrypts :pass, deterministic: true + + has_many :zdrofit_class_bookings + + attr_accessor :zdrofit_client + + def zdrofit_api_client + client = ZdrofitClient::Client.new(email, pass) + client.login + client + end + + def update_last_location(city_id:, club_id:) + update( + last_city_id: city_id, + last_club_id: club_id + ) + end end diff --git a/app/services/class_booker.rb b/app/services/class_booker.rb new file mode 100644 index 0000000..f685b26 --- /dev/null +++ b/app/services/class_booker.rb @@ -0,0 +1,35 @@ +# t.integer "class_id" +# t.integer "club_id" +# t.datetime "next_occurrence" +# t.integer "zdrofit_user_id", null: false +# t.string "status" +# t.string "mode" +# t.string "class_name" +# t.string "trainer_name" +# t.datetime "created_at", null: false +# t.datetime "updated_at", null: false + +class ClassBooker + def initialize(booking) + @booking = booking + @user = booking.user + end + + def self.call(booking) + new(booking).call + end + + def call + zdrofit_api_client = @user.zdrofit_api_client + zdrofit_api_client.book_class(@booking.class_id) + booking.update!(status: "booked", next_occurrence: booking.next_occurrence + 1.week) + ClassBookerJob.set(wait_until: booking.booking_time).perform_later(booking.id) + rescue => e + booking.update!(status: "failed") + raise e + end + + private + + attr_reader :booking, :user +end diff --git a/app/views/home/_weekly_classes.html.erb b/app/views/home/_weekly_classes.html.erb new file mode 100644 index 0000000..5da72ea --- /dev/null +++ b/app/views/home/_weekly_classes.html.erb @@ -0,0 +1,29 @@ +
+ + + + + + + + + + <% @classes['CalendarData'].each do |zone| %> + <% zone_name = zone['ZoneName'] %> + <% zone['ClassesPerHour'].each do |cph| %> + <% hour = cph['Hour'] %> + <% cph['ClassesPerDay'].flatten.each do |obj| %> + + + + + <% end %> + <% end %> + <% end %> + +
IdClass
+ <%= obj['Id'] %> + + <%= obj['Name'] %> +
+
\ No newline at end of file diff --git a/app/views/home/dashboard.html.erb b/app/views/home/dashboard.html.erb new file mode 100644 index 0000000..c00d75d --- /dev/null +++ b/app/views/home/dashboard.html.erb @@ -0,0 +1,491 @@ +<%# Background container %> +
+ +<%# Fixed Header Image with blur %> +
+ <%# Blurred background image %> +
+ People exercising in gym +
+ <%# Gradient overlay %> +
+
+ +<%# Content container %> +
+ <%# Spacer to push content below fixed header %> +
+ + <%# Title Section with backdrop blur %> +
+
+

+ Welcome, <%= @user.email %>! +

+
+ + <%# Content Section %> +
+
+
+
+
+
+

Available Clubs

+ + <% if flash[:error] %> + + <% end %> + + <%# Update the styles %> + + + <%# Update the dropdowns HTML %> +
+ <%# Cities dropdown %> +
+ +
+ + <%# Clubs dropdown %> +
+ +
+
+ +
+

+ Total clubs available: <%= @clubs.size %> +

+
+ + <%# Move the table container here %> + +
+ +
+

+ Select a club from the dropdown to view available classes and make reservations. +

+
+
+
+
+
+
+
+ +<%# Remove the table container from here %> + +<%# Update the JavaScript %> +<%= javascript_tag do %> + // Fix the club ID retrieval in bookClassesWeekly function + function bookClassesWeekly(classId, button, obj) { + // Debug logs + console.log('Selected club item:', document.querySelector('#clubs-container .dropdown-item[data-selected="true"]')); + console.log('Last club ID:', <%= raw(@user.last_club_id.to_json) %>); + + // Try getting club ID from the last selected club + const lastClubId = <%= raw(@user.last_club_id.to_json) %>; + const clubId = lastClubId; + + if (!clubId) { + alert('Please select a club first'); + return; + } + + // Disable the button and show loading state + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Booking...'; + + fetch('/book', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + class_id: classId, + club_id: clubId, + next_occurrence: obj.StartTime, + class_name: obj.Name, + trainer_name: obj.Trainer + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + button.classList.remove('bg-indigo-600', 'hover:bg-indigo-700'); + button.classList.add('bg-green-600', 'hover:bg-green-700'); + button.textContent = 'Scheduled for booking!'; + } else { + throw new Error(data.error); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + alert(`Failed to book class: ${error.message}`); + }); + } + + document.addEventListener('DOMContentLoaded', function() { + // Add renderClassesTable function + function renderClassesTable(data) { + const tableBody = document.getElementById('classes-table-body'); + tableBody.innerHTML = ''; + + console.log('Rendering table with data:', data); + + if (!data.CalendarData || !Array.isArray(data.CalendarData)) { + console.error('Invalid data structure:', data); + tableBody.innerHTML = ` + + No classes found or invalid data structure + + `; + return; + } + + let hasClasses = false; + + data.CalendarData.forEach(zone => { + if (!zone.ClassesPerHour) return; + + zone.ClassesPerHour.forEach(cph => { + if (!cph.ClassesPerDay) return; + + cph.ClassesPerDay.flat().forEach(obj => { + if (!obj || !obj.Id || !obj.Name) return; + + hasClasses = true; + const row = document.createElement('tr'); + + // Format the date + const startTime = new Date(obj.StartTime); + const formattedDate = startTime.toLocaleString('sv', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }).replace(',', ''); + + row.innerHTML = ` + + ${formattedDate} + + + ${obj.Name} + + + ${obj.Trainer} + + + + + + + + `; + tableBody.appendChild(row); + + // Add click event listener to the button + const button = row.querySelector('.book-button'); + button.addEventListener('click', function() { + bookClassesWeekly(obj.Id, this, obj); + }); + + // Add click event listener for the debug button + const debugButton = row.querySelector('.debug-button'); + debugButton.addEventListener('click', function() { + console.log('Class object:', JSON.stringify(obj, null, 2)); + }); + }); + }); + }); + + if (!hasClasses) { + tableBody.innerHTML = ` + + No classes found for this club + + `; + } + } + + // Add loadWeeklyClasses function + function loadWeeklyClasses(clubId) { + const weeklyClassesContainer = document.getElementById('weekly-classes-container'); + weeklyClassesContainer.classList.remove('hidden'); + + const tableBody = document.getElementById('classes-table-body'); + tableBody.innerHTML = ` + + Loading classes... + + `; + + fetch(`/weekly_classes?club_id=${clubId}`) + .then(response => response.json()) + .then(data => { + if (data.error) { + throw new Error(data.error); + } + console.log('Received data:', data); + renderClassesTable(data); + }) + .catch(error => { + tableBody.innerHTML = ` + + +
+ Failed to load classes: ${error.message} +
+ + + `; + }); + } + + // Initialize dropdowns if there's a last selected city + const lastCity = <%= raw(@user.last_city_id.to_json) %>; + const lastClubId = <%= raw(@user.last_club_id.to_json) %>; + + if (lastCity) { + // Show clubs container + const clubsContainer = document.getElementById('clubs-container'); + clubsContainer.classList.remove('hidden'); + + // Filter club options + const clubItems = document.querySelectorAll('#clubs-container .dropdown-item'); + clubItems.forEach(item => { + const showOption = item.dataset.city === lastCity; + item.closest('li').style.display = showOption ? '' : 'none'; + }); + + // If there's a last selected club, set it up + if (lastClubId) { + const selectedClubItem = document.querySelector(`#clubs-container .dropdown-item[data-value="${lastClubId}"]`); + if (selectedClubItem) { + // Set the club dropdown text + const clubButton = document.getElementById('club-dropdown'); + clubButton.querySelector('span').textContent = selectedClubItem.textContent; + + // Load the classes + loadWeeklyClasses(lastClubId); + } + } + } + + // Update the setupDropdown function + function setupDropdown(buttonId, onSelect) { + const button = document.getElementById(buttonId); + const menu = button.nextElementSibling; + const selectedText = button.querySelector('span'); + const arrow = button.querySelector('svg'); + + function closeAllDropdowns() { + document.querySelectorAll('.dropdown-menu').forEach(menu => { + menu.classList.add('hidden'); + menu.classList.remove('show'); + const btn = menu.previousElementSibling; + if (btn) { + btn.setAttribute('aria-expanded', 'false'); + const arrow = btn.querySelector('svg'); + if (arrow) arrow.style.transform = ''; + } + }); + } + + button.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + const isExpanded = button.getAttribute('aria-expanded') === 'true'; + + if (isExpanded) { + closeAllDropdowns(); + } else { + closeAllDropdowns(); + menu.classList.remove('hidden'); + menu.classList.add('show'); + button.setAttribute('aria-expanded', 'true'); + arrow.style.transform = 'rotate(180deg)'; + } + }); + + menu.querySelectorAll('.dropdown-item').forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Remove data-selected from all items + menu.querySelectorAll('.dropdown-item').forEach(i => i.removeAttribute('data-selected')); + // Set data-selected on clicked item + item.setAttribute('data-selected', 'true'); + + selectedText.textContent = item.textContent; + closeAllDropdowns(); + onSelect(item.dataset.value); + }); + }); + + // Close when clicking outside + document.addEventListener('click', (e) => { + if (!button.contains(e.target) && !menu.contains(e.target)) { + closeAllDropdowns(); + } + }); + } + + // Setup dropdowns + setupDropdown('city-dropdown', (cityValue) => { + const clubsContainer = document.getElementById('clubs-container'); + clubsContainer.classList.toggle('hidden', !cityValue); + + // Reset club dropdown text + const clubButton = document.getElementById('club-dropdown'); + clubButton.querySelector('span').textContent = 'Select a club...'; + + const clubItems = document.querySelectorAll('#clubs-container .dropdown-item'); + clubItems.forEach(item => { + const showOption = !cityValue || item.dataset.city === cityValue; + item.closest('li').style.display = showOption ? '' : 'none'; + }); + + fetch('/update_location', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + city_id: cityValue, + club_id: null + }) + }); + }); + + setupDropdown('club-dropdown', (clubId) => { + if (!clubId) return; + + const cityValue = document.querySelector('.selected-city').textContent; + + fetch('/update_location', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + city_id: cityValue, + club_id: clubId + }) + }); + + loadWeeklyClasses(clubId); + }); + }); +<% end %> \ No newline at end of file diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..798bd73 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,80 @@ +<%# Background container %> +
+ +<%# Fixed Header Image with blur %> +
+ <%# Blurred background image %> +
+ People exercising in gym +
+ <%# Gradient overlay %> +
+
+ +<%# Content container %> +
+ <%# Spacer to push content below fixed header %> +
+ + <%# Title Section with backdrop blur %> +
+
+

+ Zdrofit Booker +

+
+ + <%# Add this right after the title section and before the login form %> + <% if flash[:error] %> +
+ +
+ <% end %> + + <%# Login Form Section %> +
+
+
+
+
+
+

Login to Your Account

+ + <%= form_tag login_path, method: :post, class: "space-y-6" do %> +
+ +
+ <%= email_field_tag :email, nil, class: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" %> +
+
+ +
+ +
+ <%= password_field_tag :password, nil, class: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" %> +
+
+ +
+ <%= submit_tag "Login", class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> +
+ <% end %> +
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +

+
+
+
+
+
+
+
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1e970f6..f94b556 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -18,12 +18,13 @@ <%# Includes all stylesheet files in app/assets/stylesheets %> - <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "tailwind.css", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> -
+
<%= yield %>
diff --git a/config/application.rb b/config/application.rb index 6ca1aa8..e86cbc6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,5 +23,13 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + + config.active_record.encryption.primary_key = "Q2hXD9K9eYX39yYKqZQ3qxz4STbmkWVv" + config.active_record.encryption.deterministic_key = "aWvhWL6Kx4MxRZ2yNTCKpnxy4PKpbGxn" + config.active_record.encryption.key_derivation_salt = "xQ3UCpkFQXdgNwpWvXpNqXpndW62tVLr" + config.active_record.encryption.encrypt_fixtures = true + + # Add the builds directory to the asset paths + config.assets.paths << Rails.root.join("app/assets/builds") end end diff --git a/config/cable.yml b/config/cable.yml index b9adc5a..7866c8d 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -3,7 +3,12 @@ # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view # to make the web console appear. development: - adapter: async + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day test: adapter: test diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index d84daee..b5e9c79 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -EoMlBGFW+tUtZt1/aCwqcjqutx6hp60iEeulYFrbalwmkkTwZIyN0cNCH3Z4gE8BOZJ6gRFeWwA1NyLlz0qcT0yruFhjtSi0zTMj9alIU04J9BHR3HlcKpumqrzc5TQRpAvrb02SG1/TG0uGpiSnaDQ+ta0+WOGKeexrUW8HbcsWFACrUNsTLxqwVEVIMK5Et3+Yc/jDRIIHhymixcoy8+sDMvA8c/bOSza36UOm2eRtZVaBx/6+cwYiIu1laV+849Bm9GAT1TV1kfj+T23BtaJ4AiEjcGCHj48O9ia6EsUfR3t5rPqNWhhFd1rGe0AQuFdwU3+T9N37YIYDZcQitmqDrR/ev2jrFmNtHsE4OedPbXLIyrGtl2/Cw8kETjAh0QOe7Il4tWm808rVienbp7X128+D9UksffJB+mPVtj7HKFFtd8T/FpLQKfNgSmZ5Pa8NjM+2EKzC1IiwQg/pP4N7rz1lSVkaDz2bXi7K5k6/vK4KD9C9+PBf--tKS2zSbrRFUrh+hv--Z0qrjmS3MR/ejtJmXiIV0g== \ No newline at end of file +g72PfVheNptCIN+Yi+RY0Cd4kuYvccJZYa18rcsunDO2t2KOP+Nn/VNxsfY6laDLO0zIdwvWAe03Q6Po/9FVTFrwIjtHNjeUCVbAXyehXFljtV/AqGySnIRw+0G+oDWPLrdl7T6qoNRem+/o/5I5cU0EbvLcD5fEIqbPuhYTb2EtpItHWLrgTlaGi4+jjkFwvv8kYphupW/RcXn9ccGRRRj6A7KrOWftLZv2N7zzUSak9HQuxdbKvr58OvIL9SURLaUcPWXkwh/VwGJ0CLwh4Z9HHBbYDsJHIaE9Q0dLvkcVubZs6lu8PoV63PwFMQCePhPBjsK71ZNW73+FYxhLR1pldo5r1SqRee74dOfQIdSyX5jkvrz/j0j1nm8wX5M47l/WUvrnSex8T6DBCOQooZkxry2CyxvwqxshzR5JajKYq//aMbg4oqanQHyojZbKOyJSKQhwSVXSIy+lUmykwSICobTNoGo/qhszEtQHVXdnnKIAWcVhH70MmsR6CfO+/fKc7OWSdB2KAtP0iL2Wy+KQ02GsTYRMHvW2TanYQPyPiceaBiDwPKnKx+L7AfAxhn9scOgXsAusaIVPArOcwHqG59v2xtPWzTh9/wgh+4J4BhaEn3QrPs8mS4cH4NpdV4PWOxPG8+b3p9Yd8F4Ex732nfO5In2eGqiy6tIjNiVCgftGvTTNT6LhN0ANQp89u/JSp639gatm5rEA7LslYbQR8UYPE/8XS5i5g4/NPoQ0eG/J3CGF+g2wBu2PDQ==--4Vd1bP9TPDA0NCEQ--Nfc4ZLRC7eOSXy7Rip4rPg== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index 2640cb5..bc9cefc 100644 --- a/config/database.yml +++ b/config/database.yml @@ -9,33 +9,29 @@ default: &default pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 -development: - <<: *default - database: storage/development.sqlite3 - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - <<: *default - database: storage/test.sqlite3 - - -# Store production database in the storage/ directory, which by default -# is mounted as a persistent Docker volume in config/deploy.yml. -production: +databases: &databases primary: <<: *default - database: storage/production.sqlite3 + database: storage/<%= Rails.env %>.sqlite3 cache: <<: *default - database: storage/production_cache.sqlite3 + database: storage/<%= Rails.env %>_cache.sqlite3 migrations_paths: db/cache_migrate queue: <<: *default - database: storage/production_queue.sqlite3 + database: storage/<%= Rails.env %>_queue.sqlite3 migrations_paths: db/queue_migrate cable: <<: *default - database: storage/production_cable.sqlite3 + database: storage/<%= Rails.env %>_cable.sqlite3 migrations_paths: db/cable_migrate + +development: + <<: *databases + +test: + <<: *default + database: storage/test.sqlite3 + +production: + <<: *databases diff --git a/config/deploy.yml b/config/deploy.yml index fab12f5..b1ec004 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -2,12 +2,12 @@ service: zdrofit_booker # Name of the container image. -image: your-user/zdrofit_booker +image: jan-kaczorowski/zdrofit_booker # Deploy to these servers. servers: web: - - 192.168.0.1 + - 52.28.8.239 # job: # hosts: # - 192.168.0.1 @@ -19,17 +19,21 @@ servers: # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. proxy: ssl: true - host: app.example.com + host: zdrofitbooker.redsphinx.pl + app_port: 80 + healthcheck: + path: /up + interval: 10 + timeout: 10 -# Credentials for your image host. registry: + server: 546620766310.dkr.ecr.eu-west-1.amazonaws.com # Specify the registry server, if you're not using Docker Hub # server: registry.digitalocean.com / ghcr.io / ... - username: your-user + username: AWS - # Always use an access token rather than real password when possible. - password: - - KAMAL_REGISTRY_PASSWORD + # Always use an access token rather than real password (pulled from .kamal/secrets). + password: <%= %x[AWS_PROFILE=privr aws ecr get-login-password --region eu-west-1].strip %> # Inject ENV variables into containers (secrets come from .kamal/secrets). env: @@ -39,6 +43,8 @@ env: # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. # When you start using multiple servers, you should split out job processing to a dedicated machine. SOLID_QUEUE_IN_PUMA: true + PORT: 80 + HOST: zdrofitbooker.redsphinx.pl # Set number of processes dedicated to Solid Queue (default: 1) # JOB_CONCURRENCY: 3 @@ -77,19 +83,10 @@ asset_path: /rails/public/assets builder: arch: amd64 - # # Build image via remote server (useful for faster amd64 builds on arm64 computers) - # remote: ssh://docker@docker-builder-server - # - # # Pass arguments and secrets to the Docker build process - # args: - # RUBY_VERSION: ruby-3.4.1 - # secrets: - # - GITHUB_TOKEN - # - RAILS_MASTER_KEY - # Use a different ssh user than root -# ssh: -# user: app +ssh: + user: ubuntu + keys: [ "~/.ssh/my-aws.pem" ] # Use accessory services (secrets come from .kamal/secrets). # accessories: diff --git a/config/environments/development.rb b/config/environments/development.rb index 4cc21c4..72208ee 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -69,4 +69,28 @@ # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT) +end + +HttpLog.configure do |config| + # Enable or disable all logging + config.enabled = true + + # You can assign a different logger or method to call on that logger + config.logger = Logger.new($stdout) + config.logger_method = :log + + # I really wouldn't change this... + config.severity = Logger::Severity::DEBUG + + # Tweak which parts of the HTTP cycle to log... + config.log_connect = true + config.log_request = true + config.log_headers = false + config.log_data = true + config.log_status = true + config.log_response = true + config.log_benchmark = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index bdcd01d..c1a9de5 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -58,7 +58,7 @@ # config.action_mailer.raise_delivery_errors = false # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "example.com" } + config.action_mailer.default_url_options = { host: ENV["HOST"] } # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. # config.action_mailer.smtp_settings = { diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb new file mode 100644 index 0000000..d9a1222 --- /dev/null +++ b/config/initializers/zeitwerk.rb @@ -0,0 +1 @@ +Rails.autoloaders.main.ignore(Rails.root.join("lib/zdrofit_client")) diff --git a/config/recurring.yml b/config/recurring.yml index d045b19..d5e3519 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -8,3 +8,21 @@ # command: "SoftDeletedRecord.due.delete_all" # priority: 2 # schedule: at 5am every day + +development: + booking_checker_job: + class: BookingCheckerJob + queue: default + schedule: "5 0 * * *" # At 00:05 every day + +test: + booking_checker_job: + class: BookingCheckerJob + queue: default + schedule: "5 0 * * *" + +production: + booking_checker_job: + class: BookingCheckerJob + queue: default + schedule: "5 0 * * *" diff --git a/config/routes.rb b/config/routes.rb index 48254e8..897c3db 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,5 +10,11 @@ # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker # Defines the root path route ("/") - # root "posts#index" + root "home#index" + + post "/login", to: "home#login" + get "/dashboard", to: "home#dashboard", as: :dashboard + get "/weekly_classes", to: "home#weekly_classes" + post "/book", to: "home#book" + post "/update_location", to: "home#update_location" end diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000..03b420f --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,12 @@ +module.exports = { + content: [ + './app/views/**/*.erb', + './app/helpers/**/*.rb', + './app/javascript/**/*.js', + './config/initializers/simple_form_tailwind.rb', + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/config/tailwind.rb b/config/tailwind.rb new file mode 100644 index 0000000..55c7a7b --- /dev/null +++ b/config/tailwind.rb @@ -0,0 +1,7 @@ +require "tailwindcss-ruby" + +Tailwindcss.configure do |config| + config.input = "app/assets/stylesheets/application.tailwind.css" + config.output = "app/assets/builds/tailwind.css" + config.prefix = "" +end diff --git a/db/cable_schema.rb b/db/cable_schema.rb index 2366660..d7fe776 100644 --- a/db/cable_schema.rb +++ b/db/cable_schema.rb @@ -1,4 +1,16 @@ -ActiveRecord::Schema[7.1].define(version: 1) do +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 1) do create_table "solid_cable_messages", force: :cascade do |t| t.binary "channel", limit: 1024, null: false t.binary "payload", limit: 536870912, null: false diff --git a/db/cache_schema.rb b/db/cache_schema.rb index 6005a29..fc99b30 100644 --- a/db/cache_schema.rb +++ b/db/cache_schema.rb @@ -1,6 +1,16 @@ -# frozen_string_literal: true +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 1) do +ActiveRecord::Schema[8.0].define(version: 1) do create_table "solid_cache_entries", force: :cascade do |t| t.binary "key", limit: 1024, null: false t.binary "value", limit: 536870912, null: false diff --git a/db/migrate/20250205212548_create_zdrofit_class_bookings.rb b/db/migrate/20250205212548_create_zdrofit_class_bookings.rb new file mode 100644 index 0000000..f83d543 --- /dev/null +++ b/db/migrate/20250205212548_create_zdrofit_class_bookings.rb @@ -0,0 +1,15 @@ +class CreateZdrofitClassBookings < ActiveRecord::Migration[8.0] + def change + create_table :zdrofit_class_bookings do |t| + t.integer :class_id + t.integer :club_id + t.datetime :next_occurence + t.references :zdrofit_user, null: false, foreign_key: true + t.string :status + t.string :mode + t.string :class_name + t.string :trainer_name + t.timestamps + end + end +end diff --git a/db/migrate/20250205224113_add_last_location_to_zdrofit_users.rb b/db/migrate/20250205224113_add_last_location_to_zdrofit_users.rb new file mode 100644 index 0000000..6c86456 --- /dev/null +++ b/db/migrate/20250205224113_add_last_location_to_zdrofit_users.rb @@ -0,0 +1,6 @@ +class AddLastLocationToZdrofitUsers < ActiveRecord::Migration[8.0] + def change + add_column :zdrofit_users, :last_city_id, :string + add_column :zdrofit_users, :last_club_id, :integer + end +end diff --git a/db/migrate/20250207221640_fix_typo_in_table_occurrence.rb b/db/migrate/20250207221640_fix_typo_in_table_occurrence.rb new file mode 100644 index 0000000..93acf92 --- /dev/null +++ b/db/migrate/20250207221640_fix_typo_in_table_occurrence.rb @@ -0,0 +1,5 @@ +class FixTypoInTableOccurrence < ActiveRecord::Migration[8.0] + def change + rename_column :zdrofit_class_bookings, :next_occurence, :next_occurrence + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb index 85194b6..4b2cdcd 100644 --- a/db/queue_schema.rb +++ b/db/queue_schema.rb @@ -1,4 +1,16 @@ -ActiveRecord::Schema[7.1].define(version: 1) do +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 1) do create_table "solid_queue_blocked_executions", force: :cascade do |t| t.bigint "job_id", null: false t.string "queue_name", null: false @@ -6,24 +18,24 @@ t.string "concurrency_key", null: false t.datetime "expires_at", null: false t.datetime "created_at", null: false - t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" - t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" - t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true end create_table "solid_queue_claimed_executions", force: :cascade do |t| t.bigint "job_id", null: false t.bigint "process_id" t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true - t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" end create_table "solid_queue_failed_executions", force: :cascade do |t| t.bigint "job_id", null: false t.text "error" t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true end create_table "solid_queue_jobs", force: :cascade do |t| @@ -37,17 +49,17 @@ t.string "concurrency_key" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" - t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" - t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" - t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" - t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" end create_table "solid_queue_pauses", force: :cascade do |t| t.string "queue_name", null: false t.datetime "created_at", null: false - t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true end create_table "solid_queue_processes", force: :cascade do |t| @@ -59,9 +71,9 @@ t.text "metadata" t.datetime "created_at", null: false t.string "name", null: false - t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" - t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true - t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" end create_table "solid_queue_ready_executions", force: :cascade do |t| @@ -69,9 +81,9 @@ t.string "queue_name", null: false t.integer "priority", default: 0, null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true - t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" - t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" end create_table "solid_queue_recurring_executions", force: :cascade do |t| @@ -79,8 +91,8 @@ t.string "task_key", null: false t.datetime "run_at", null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true - t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true end create_table "solid_queue_recurring_tasks", force: :cascade do |t| @@ -95,8 +107,8 @@ t.text "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true - t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" end create_table "solid_queue_scheduled_executions", force: :cascade do |t| @@ -105,8 +117,8 @@ t.integer "priority", default: 0, null: false t.datetime "scheduled_at", null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true - t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" end create_table "solid_queue_semaphores", force: :cascade do |t| @@ -115,9 +127,9 @@ t.datetime "expires_at", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" - t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" - t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true end add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade diff --git a/db/schema.rb b/db/schema.rb index 93f31f5..8ad8703 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,29 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_02_04_201349) do +ActiveRecord::Schema[8.0].define(version: 2025_02_07_221640) do + create_table "zdrofit_class_bookings", force: :cascade do |t| + t.integer "class_id" + t.integer "club_id" + t.datetime "next_occurrence" + t.integer "zdrofit_user_id", null: false + t.string "status" + t.string "mode" + t.string "class_name" + t.string "trainer_name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["zdrofit_user_id"], name: "index_zdrofit_class_bookings_on_zdrofit_user_id" + end + create_table "zdrofit_users", force: :cascade do |t| t.string "email" t.string "pass" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "last_city_id" + t.integer "last_club_id" end + + add_foreign_key "zdrofit_class_bookings", "zdrofit_users" end diff --git a/lib/zdrofit_client/.gitignore b/lib/zdrofit_client/.gitignore new file mode 100644 index 0000000..1364d67 --- /dev/null +++ b/lib/zdrofit_client/.gitignore @@ -0,0 +1,2 @@ +*.gem +Gemfile.lock \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client.rb b/lib/zdrofit_client/lib/zdrofit_client.rb new file mode 100644 index 0000000..85e9541 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client.rb @@ -0,0 +1,21 @@ +require "httparty" +require "zdrofit_client/version" +require_relative "zdrofit_client/api_call" + +module ZdrofitClient + class Error < StandardError; end + + module ApiCalls + end + + def self.new(login, password) + Client.new(login, password) + end +end + +# Load all API calls +Dir[File.join(__dir__, "zdrofit_client/api_calls", "*.rb")].each do |file| + require_relative file +end + +require_relative "zdrofit_client/client" diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_call.rb b/lib/zdrofit_client/lib/zdrofit_client/api_call.rb new file mode 100644 index 0000000..8122a3c --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_call.rb @@ -0,0 +1,36 @@ +module ZdrofitClient + class ApiCall + def initialize(client) + @client = client + end + + def call(**params) + raise NotImplementedError + end + + private + + attr_reader :client + + def post(path, body: {}) + response = @client.class.post( + path, + headers: @client.authenticated_headers, + body: body.to_json + ) + + raise "API call failed: #{response.body}" unless response.success? + JSON.parse(response.body) + end + + def get(path, query: nil) + options = { headers: @client.authenticated_headers } + options[:query] = query if query + + response = @client.class.get(path, options) + + raise "API call failed: #{response.body}" unless response.success? + JSON.parse(response.body) + end + end +end diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/book_class.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/book_class.rb new file mode 100644 index 0000000..1e80437 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/book_class.rb @@ -0,0 +1,15 @@ +module ZdrofitClient + module ApiCalls + class BookClass < ApiCall + def call(class_id:, club_id:) + post( + "/Classes/ClassCalendar/BookClass", + body: { + classId: class_id, + clubId: club_id.to_s + } + ) + end + end + end +end diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/cancel_booking.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/cancel_booking.rb new file mode 100644 index 0000000..1e50397 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/cancel_booking.rb @@ -0,0 +1,12 @@ +module ZdrofitClient + module ApiCalls + class CancelBooking < ApiCall + def call(class_id:) + post( + "/Classes/ClassCalendar/CancelBooking", + body: { classId: class_id } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_calendar_filters.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_calendar_filters.rb new file mode 100644 index 0000000..2a2667a --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_calendar_filters.rb @@ -0,0 +1,12 @@ +module ZdrofitClient + module ApiCalls + class GetCalendarFilters < ApiCall + def call(club_id:) + post( + "/Classes/ClassCalendar/GetCalendarFilters", + body: { clubId: club_id } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_class_tickets.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_class_tickets.rb new file mode 100644 index 0000000..c57d323 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_class_tickets.rb @@ -0,0 +1,15 @@ +module ZdrofitClient + module ApiCalls + class GetClassTickets < ApiCall + def call(class_id:, user_id:) + get( + "/Classes/ClassCalendar/GetClassTickets", + query: { + classId: class_id, + userId: user_id + } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_identity.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_identity.rb new file mode 100644 index 0000000..657047d --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_identity.rb @@ -0,0 +1,9 @@ +module ZdrofitClient + module ApiCalls + class GetIdentity < ApiCall + def call + post("/Auth/Login/Identity") + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_my_calendar.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_my_calendar.rb new file mode 100644 index 0000000..6bbe612 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_my_calendar.rb @@ -0,0 +1,9 @@ +module ZdrofitClient + module ApiCalls + class GetMyCalendar < ApiCall + def call + get("/MyCalendar/MyCalendar/GetCalendar") + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_personal_id_info.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_personal_id_info.rb new file mode 100644 index 0000000..877f7da --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_personal_id_info.rb @@ -0,0 +1,16 @@ +module ZdrofitClient + module ApiCalls + class GetPersonalIdInfo < ApiCall + def call(country: "PL", personal_id:, user_type: "ClubMember") + post( + "/PersonalData/GetPersonalIdInfo", + body: { + country: country, + personalId: personal_id, + userType: user_type + } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_phone_info.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_phone_info.rb new file mode 100644 index 0000000..2687fac --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_phone_info.rb @@ -0,0 +1,15 @@ +module ZdrofitClient + module ApiCalls + class GetPhoneInfo < ApiCall + def call(phone_number:, country_symbol: "PL") + post( + "/PersonalData/GetPhoneInfo", + body: { + phoneNumber: phone_number, + phoneNumberCountrySymbol: country_symbol + } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_profile_for_edit.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_profile_for_edit.rb new file mode 100644 index 0000000..27d4a38 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/get_profile_for_edit.rb @@ -0,0 +1,12 @@ +module ZdrofitClient + module ApiCalls + class GetProfileForEdit < ApiCall + def call(user_id:) + post( + "/Profile/Profile/GetProfileForEdit", + body: { userId: user_id } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/list_available_clubs.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/list_available_clubs.rb new file mode 100644 index 0000000..b19da40 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/list_available_clubs.rb @@ -0,0 +1,9 @@ +module ZdrofitClient + module ApiCalls + class ListAvailableClubs < ApiCall + def call + get("/Clubs/GetAvailableClassesClubs") + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/list_weekly_classes.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/list_weekly_classes.rb new file mode 100644 index 0000000..48827a2 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/list_weekly_classes.rb @@ -0,0 +1,50 @@ +module ZdrofitClient + module ApiCalls + class ListWeeklyClasses < ApiCall + def call(club_id:, days_in_week: 1, date: nil, category_id: nil, time_table_id: nil, trainer_id: nil) + response = post( + "/Classes/ClassCalendar/WeeklyClasses", + body: { + clubId: club_id, + date: date, + categoryId: category_id, + timeTableId: time_table_id, + trainerId: trainer_id, + daysInWeek: days_in_week + } + ) + + # Add filtering logic to the response + filter_classes(response) + end + + private + + def filter_classes(response) + return response unless response["CalendarData"] + + response["CalendarData"].each do |zone| + next unless zone["ClassesPerHour"] + + zone["ClassesPerHour"].each do |cph| + next unless cph["ClassesPerDay"] + + cph["ClassesPerDay"].each do |classes_per_day| + next unless classes_per_day.is_a?(Array) + + # Filter out classes that don't meet criteria + classes_per_day.select! do |el| + next false unless el.is_a?(Hash) + + el["Status"] == "Bookable" || + (el["Status"] == "Unavailable" && Time.parse(el["StartTime"]) > Time.current) + end + end + end + end + + response + end + end + end +end diff --git a/lib/zdrofit_client/lib/zdrofit_client/api_calls/summarize_booking_cancellation.rb b/lib/zdrofit_client/lib/zdrofit_client/api_calls/summarize_booking_cancellation.rb new file mode 100644 index 0000000..f3a5bdd --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/api_calls/summarize_booking_cancellation.rb @@ -0,0 +1,12 @@ +module ZdrofitClient + module ApiCalls + class SummarizeBookingCancellation < ApiCall + def call(timetable_event_id:) + post( + "/Classes/ClassCalendar/SummarizeBookingCancellation", + body: { timeTableEventId: timetable_event_id } + ) + end + end + end +end \ No newline at end of file diff --git a/lib/zdrofit_client/lib/zdrofit_client/client.rb b/lib/zdrofit_client/lib/zdrofit_client/client.rb new file mode 100644 index 0000000..54bd514 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/client.rb @@ -0,0 +1,81 @@ +module ZdrofitClient + class Client + include HTTParty + base_uri "https://zdrofit.perfectgym.pl/ClientPortal2" + + attr_reader :auth_token + attr_reader :authenticated_headers + + API_CALLS = { + list_weekly_classes: ZdrofitClient::ApiCalls::ListWeeklyClasses, + book_class: ZdrofitClient::ApiCalls::BookClass, + list_available_clubs: ZdrofitClient::ApiCalls::ListAvailableClubs, + get_calendar_filters: ZdrofitClient::ApiCalls::GetCalendarFilters, + get_identity: ZdrofitClient::ApiCalls::GetIdentity, + get_class_tickets: ZdrofitClient::ApiCalls::GetClassTickets, + summarize_booking_cancellation: ZdrofitClient::ApiCalls::SummarizeBookingCancellation, + cancel_booking: ZdrofitClient::ApiCalls::CancelBooking, + get_my_calendar: ZdrofitClient::ApiCalls::GetMyCalendar, + get_personal_id_info: ZdrofitClient::ApiCalls::GetPersonalIdInfo, + get_phone_info: ZdrofitClient::ApiCalls::GetPhoneInfo, + get_profile_for_edit: ZdrofitClient::ApiCalls::GetProfileForEdit + }.freeze + + def initialize(login, password) + @login = login + @password = password + @auth_token = nil + @default_headers = { + "Accept" => "application/json, text/plain, */*", + "Accept-Language" => "pl", + "CP-LANG" => "pl", + "CP-MODE" => "desktop", + "Content-Type" => "application/json", + "Origin" => "https://zdrofit.perfectgym.pl", + "Referer" => "https://zdrofit.perfectgym.pl/ClientPortal2/", + "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" + } + @authenticated_headers = @default_headers + end + + def login + response = self.class.post( + "/Auth/Login", + headers: @default_headers, + body: { + RememberMe: false, + Login: @login, + Password: @password + }.to_json + ) + + raise "Login failed: #{response.body}" unless response.success? + + @auth_token = response.headers["jwt-token"] + @authenticated_headers = @default_headers.merge({ + "Authorization" => "Bearer #{@auth_token}" + }) + + self + end + + def method_missing(method_name, *args, **kwargs) + if API_CALLS.key?(method_name) + ensure_authenticated + API_CALLS[method_name].new(self).call(**kwargs) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + API_CALLS.key?(method_name) || super + end + + private + + def ensure_authenticated + login if @auth_token.nil? + end + end +end diff --git a/lib/zdrofit_client/lib/zdrofit_client/version.rb b/lib/zdrofit_client/lib/zdrofit_client/version.rb new file mode 100644 index 0000000..97308b0 --- /dev/null +++ b/lib/zdrofit_client/lib/zdrofit_client/version.rb @@ -0,0 +1,3 @@ +module ZdrofitClient + VERSION = "0.1.0" +end \ No newline at end of file diff --git a/lib/zdrofit_client/zdrofit_client.gemspec b/lib/zdrofit_client/zdrofit_client.gemspec new file mode 100644 index 0000000..b63062c --- /dev/null +++ b/lib/zdrofit_client/zdrofit_client.gemspec @@ -0,0 +1,13 @@ +require_relative "lib/zdrofit_client/version" + +Gem::Specification.new do |spec| + spec.name = "zdrofit_client" + spec.version = ZdrofitClient::VERSION + spec.authors = ["Your Name"] + spec.summary = "Zdrofit API client" + + spec.files = Dir["lib/**/*", "Gemfile", "zdrofit_client.gemspec"] + spec.require_paths = ["lib"] + + spec.add_dependency "httparty" +end \ No newline at end of file diff --git a/spec/factories/zdrofit_class_weekly_bookings.rb b/spec/factories/zdrofit_class_weekly_bookings.rb new file mode 100644 index 0000000..f450968 --- /dev/null +++ b/spec/factories/zdrofit_class_weekly_bookings.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :zdrofit_class_weekly_booking do + class_id { 1 } + club_id { 1 } + next_occurrence { "2025-02-05 22:25:48" } + end +end diff --git a/spec/factories/zdrofit_users.rb b/spec/factories/zdrofit_users.rb new file mode 100644 index 0000000..96be532 --- /dev/null +++ b/spec/factories/zdrofit_users.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :zdrofit_user do + email { "test@example.com" } + pass { "password123" } + end +end \ No newline at end of file diff --git a/spec/lib/zdrofit_client_spec.rb b/spec/lib/zdrofit_client_spec.rb new file mode 100644 index 0000000..e2bc7bd --- /dev/null +++ b/spec/lib/zdrofit_client_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' +require 'zdrofit_client' + +RSpec.describe ZdrofitClient do + let(:email) { "jan.kaczorowski@gmail.com" } + let(:password) { "@MyBelovedCats1" } + let(:client) { described_class.new(email, password) } + + describe '#login' do + it 'authenticates successfully' do + VCR.use_cassette('zdrofit_client/login_success') do + expect { client.login }.not_to raise_error + expect(client.instance_variable_get(:@auth_token)).not_to be_nil + end + end + end + + describe '#list_available_clubs' do + it 'returns list of clubs' do + VCR.use_cassette('zdrofit_client/list_clubs') do + clubs = client.list_available_clubs + expect(clubs).to be_an(Array) + end + end + end + + describe '#get_calendar_filters' do + it 'returns calendar filters for a club' do + VCR.use_cassette('zdrofit_client/calendar_filters') do + filters = client.get_calendar_filters(club_id: 77) + expect(filters).to be_a(Hash) + end + end + end + + describe '#book_class' do + it 'books a class successfully' do + VCR.use_cassette('zdrofit_client/book_class') do + result = client.book_class(class_id: 801539, club_id: 77) + expect(result).to be_a(Hash) + end + end + end + + # Add more tests for other methods... +end diff --git a/spec/models/zdrofit_class_weekly_booking_spec.rb b/spec/models/zdrofit_class_weekly_booking_spec.rb new file mode 100644 index 0000000..f33d0c5 --- /dev/null +++ b/spec/models/zdrofit_class_weekly_booking_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ZdrofitClassWeeklyBooking, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..ea84cc7 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,9 @@ +require 'factory_bot' +require 'support/vcr' +require 'support/factorybot' + +RSpec.configure do |config| + # ... existing config ... + + # Factory Bot configuration +end diff --git a/spec/support/factorybot.rb b/spec/support/factorybot.rb new file mode 100644 index 0000000..c7890e4 --- /dev/null +++ b/spec/support/factorybot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb new file mode 100644 index 0000000..4bbefdf --- /dev/null +++ b/spec/support/vcr.rb @@ -0,0 +1,25 @@ +require 'vcr' + +VCR.configure do |config| + config.cassette_library_dir = "spec/vcr_cassettes" + config.hook_into :webmock + config.configure_rspec_metadata! + + # Don't record sensitive data + config.filter_sensitive_data('') do |interaction| + auth_header = interaction.request.headers['Authorization']&.first + auth_header&.gsub('Bearer ', '') if auth_header + end + + config.filter_sensitive_data('') do |interaction| + if interaction.request.body && interaction.request.body.include?('Login') + JSON.parse(interaction.request.body)['Login'] rescue nil + end + end + + config.filter_sensitive_data('') do |interaction| + if interaction.request.body && interaction.request.body.include?('Password') + JSON.parse(interaction.request.body)['Password'] rescue nil + end + end +end diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..9d9fdc8 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +module.exports = { + content: [ + './app/views/**/*.html.erb', + './app/helpers/**/*.rb', + './app/javascript/**/*.js' + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/test/fixtures/zdrofit_users.yml b/test/fixtures/zdrofit_users.yml deleted file mode 100644 index 1e17371..0000000 --- a/test/fixtures/zdrofit_users.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -me: - email: MyString - pass: MyString - -