diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0aa860d..4b378c5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,10 @@ "permissions": { "allow": [ "Bash(go build *)", - "Bash(go vet *)" + "Bash(go vet *)", + "Bash(chmod +x *)", + "Bash(xargs ruby *)", + "Bash(docker compose exec *)" ] } } diff --git a/.gitignore b/.gitignore index 7c88fd3..a5cd653 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ teletype_softwares pma-config.user.inc.php data apps/frontend/src/events.yaml +NOTES_* + diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..8dc1f42 --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,5 @@ +.bundle/ +log/ +tmp/ +*.sqlite3 +.env diff --git a/apps/api/Dockerfile.development b/apps/api/Dockerfile.development new file mode 100644 index 0000000..f3c2e5e --- /dev/null +++ b/apps/api/Dockerfile.development @@ -0,0 +1,24 @@ +FROM --platform=linux/amd64 ruby:3.3-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + default-libmysqlclient-dev \ + git \ + tzdata \ + unzip \ + nodejs \ + libffi-dev \ + libyaml-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY Gemfile ./ +RUN bundle install + +COPY . . + +EXPOSE 3000 + +CMD ["/bin/sh", "entrypoint.sh"] diff --git a/apps/api/Gemfile b/apps/api/Gemfile new file mode 100644 index 0000000..ea3dcde --- /dev/null +++ b/apps/api/Gemfile @@ -0,0 +1,16 @@ +source "https://rubygems.org" + +gem "rails", "~> 8.0" +gem "mysql2", "~> 0.5" +gem "puma", ">= 5.0" +gem "rubyzip", "~> 2.3" +gem "bootsnap", require: false + +gem "activeadmin" +gem "devise" +gem "sprockets-rails" +gem "sassc-rails" + +group :development, :test do + gem "debug", platforms: %i[mri windows] +end diff --git a/apps/api/Gemfile.lock b/apps/api/Gemfile.lock new file mode 100644 index 0000000..d865378 --- /dev/null +++ b/apps/api/Gemfile.lock @@ -0,0 +1,298 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.18) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activeadmin (3.5.1) + arbre (~> 1.2, >= 1.2.1) + csv + formtastic (>= 3.1) + formtastic_i18n (>= 0.4) + inherited_resources (~> 1.7) + jquery-rails (>= 4.2) + kaminari (>= 1.2.1) + railties (>= 6.1) + ransack (>= 4.0) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + arbre (1.7.0) + activesupport (>= 3.0.0) + ruby2_keywords (>= 0.0.2) + base64 (0.3.0) + bcrypt (3.1.22) + bigdecimal (4.1.2) + bootsnap (1.24.3) + msgpack (~> 1.2) + builder (3.3.0) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + csv (3.3.5) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + devise (5.0.3) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 7.0) + responders + warden (~> 1.2.3) + drb (2.2.3) + erb (6.0.4) + erubi (1.13.1) + ffi (1.17.4-x86_64-linux-gnu) + formtastic (6.0.0) + actionpack (>= 7.2.0) + formtastic_i18n (0.7.0) + globalid (1.3.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + inherited_resources (1.14.0) + actionpack (>= 6.0) + has_scope (>= 0.6) + railties (>= 6.0) + responders (>= 2) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jquery-rails (4.6.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (2.19.5) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + mysql2 (0.5.7) + bigdecimal + net-imap (0.6.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio + puma (8.0.1) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.4.2) + ransack (4.4.1) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + reline (0.6.3) + io-console (~> 0.5) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + securerandom (0.4.1) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + stringio (3.2.0) + thor (1.5.0) + tilt (2.7.0) + timeout (0.6.1) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.5) + +PLATFORMS + x86_64-linux-gnu + +DEPENDENCIES + activeadmin + bootsnap + debug + devise + mysql2 (~> 0.5) + puma (>= 5.0) + rails (~> 8.0) + rubyzip (~> 2.3) + sassc-rails + sprockets-rails + +BUNDLED WITH + 2.5.22 diff --git a/apps/api/Rakefile b/apps/api/Rakefile new file mode 100644 index 0000000..7820749 --- /dev/null +++ b/apps/api/Rakefile @@ -0,0 +1,2 @@ +require_relative "config/application" +Rails.application.load_tasks diff --git a/apps/api/app/admin/admin_users.rb b/apps/api/app/admin/admin_users.rb new file mode 100644 index 0000000..9b9ce0f --- /dev/null +++ b/apps/api/app/admin/admin_users.rb @@ -0,0 +1,32 @@ +ActiveAdmin.register AdminUser do + permit_params :email, :password, :password_confirmation + + index do + selectable_column + id_column + column :email + column :created_at + actions + end + + filter :email + filter :created_at + + form do |f| + f.inputs do + f.input :email + f.input :password + f.input :password_confirmation + end + f.actions + end + + show do + attributes_table do + row :id + row :email + row :created_at + row :updated_at + end + end +end diff --git a/apps/api/app/admin/dashboard.rb b/apps/api/app/admin/dashboard.rb new file mode 100644 index 0000000..c6a0295 --- /dev/null +++ b/apps/api/app/admin/dashboard.rb @@ -0,0 +1,27 @@ +ActiveAdmin.register_page "Dashboard" do + menu priority: 1, label: proc { I18n.t("active_admin.dashboard") } + + content title: proc { I18n.t("active_admin.dashboard") } do + columns do + column do + panel "Softwares" do + ul do + li "Total: #{Software.count}" + li "Released: #{Software.where(status: 'released').count}" + li "In development: #{Software.where(status: 'development').count}" + end + end + end + + column do + panel "Recent Releases" do + table_for Release.order(created_at: :desc).limit(10) do + column(:software) { |r| link_to r.software.title, admin_software_path(r.software) rescue r.software_id } + column :version + column :created_at + end + end + end + end + end +end diff --git a/apps/api/app/admin/external_links.rb b/apps/api/app/admin/external_links.rb new file mode 100644 index 0000000..20623fd --- /dev/null +++ b/apps/api/app/admin/external_links.rb @@ -0,0 +1,34 @@ +ActiveAdmin.register ExternalLink do + permit_params :software_id, :label, :url + + menu false + + belongs_to :software + + index do + selectable_column + id_column + column :label + column(:url) { |el| link_to el.url, el.url, target: "_blank" } + column :created_at + actions + end + + form do |f| + f.inputs do + f.input :label + f.input :url + end + f.actions + end + + show do + attributes_table do + row :id + row :label + row(:url) { |el| link_to el.url, el.url, target: "_blank" } + row :created_at + row :updated_at + end + end +end diff --git a/apps/api/app/admin/releases.rb b/apps/api/app/admin/releases.rb new file mode 100644 index 0000000..6e54e77 --- /dev/null +++ b/apps/api/app/admin/releases.rb @@ -0,0 +1,33 @@ +ActiveAdmin.register Release do + actions :index, :show + + menu false + + belongs_to :software, optional: true + + index do + selectable_column + id_column + column(:software) { |r| link_to r.software.title, admin_software_path(r.software) } + column :version + column :html_folder_path + column :created_at + actions only: [:show] + end + + filter :version + + show do + attributes_table do + row :id + row(:software) { |r| link_to r.software.title, admin_software_path(r.software) } + row :version + row :cartridge_path + row :source_path + row :html_folder_path + row :docs_folder_path + row :created_at + row :updated_at + end + end +end diff --git a/apps/api/app/admin/softwares.rb b/apps/api/app/admin/softwares.rb new file mode 100644 index 0000000..032dacf --- /dev/null +++ b/apps/api/app/admin/softwares.rb @@ -0,0 +1,97 @@ +ActiveAdmin.register Software do + permit_params :name, :title, :author, :desc, :story, + :license, :platform, :status, :highlighted, + external_links_attributes: [:id, :label, :url, :_destroy], + releases_attributes: [:id, :_destroy] + + menu priority: 2 + + index do + selectable_column + id_column + column :name + column :title + column :platform + column :status do |sw| + status_tag sw.status + end + column :highlighted + column :created_at + actions + end + + filter :name + filter :title + filter :author + filter :platform, as: :select, collection: %w[tic80 ebitengine love] + filter :status, as: :select, collection: %w[development demo released archived] + filter :highlighted + + show do + attributes_table do + row :id + row :name + row :title + row :author + row :platform + row :status + row :highlighted + row :license + row(:desc) { |sw| simple_format sw.desc } + row(:story) { |sw| simple_format sw.story } + row :created_at + row :updated_at + end + + panel "External Links" do + table_for resource.external_links do + column :label + column(:url) { |el| link_to el.url, el.url, target: "_blank" } + column :created_at + column do |el| + link_to "Edit", edit_admin_software_external_link_path(resource, el) + concat " " + link_to "Delete", admin_software_external_link_path(resource, el), + method: :delete, data: { confirm: "Are you sure?" } + end + end + br + link_to "Add External Link", + new_admin_software_external_link_path(resource) + end + + panel "Releases" do + table_for resource.releases.order(created_at: :desc) do + column :version + column :html_folder_path + column :cartridge_path + column :source_path + column :docs_folder_path + column :created_at + end + end + end + + form do |f| + f.inputs "Details" do + f.input :name + f.input :title + f.input :author + f.input :platform, as: :select, collection: %w[tic80 ebitengine love] + f.input :status, as: :select, collection: %w[development demo released archived] + f.input :highlighted + f.input :license + f.input :desc, as: :text, input_html: { rows: 4 } + f.input :story, as: :text, input_html: { rows: 8 } + end + + f.inputs "External Links" do + f.has_many :external_links, allow_destroy: true, new_record: true do |el| + el.input :label + el.input :url + end + end + + f.actions + end +end diff --git a/apps/api/app/assets/config/manifest.js b/apps/api/app/assets/config/manifest.js new file mode 100644 index 0000000..694c926 --- /dev/null +++ b/apps/api/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link active_admin.css +//= link active_admin.js diff --git a/apps/api/app/assets/javascripts/active_admin.js b/apps/api/app/assets/javascripts/active_admin.js new file mode 100644 index 0000000..d2b66c5 --- /dev/null +++ b/apps/api/app/assets/javascripts/active_admin.js @@ -0,0 +1 @@ +//= require active_admin/base diff --git a/apps/api/app/assets/stylesheets/active_admin.scss b/apps/api/app/assets/stylesheets/active_admin.scss new file mode 100644 index 0000000..65d1ea3 --- /dev/null +++ b/apps/api/app/assets/stylesheets/active_admin.scss @@ -0,0 +1,2 @@ +@import "active_admin/mixins"; +@import "active_admin/base"; diff --git a/apps/api/app/controllers/api/software_controller.rb b/apps/api/app/controllers/api/software_controller.rb new file mode 100644 index 0000000..e477c67 --- /dev/null +++ b/apps/api/app/controllers/api/software_controller.rb @@ -0,0 +1,8 @@ +module Api + class SoftwareController < ApiController + def index + result = SoftwareListService.new.call + render json: result + end + end +end diff --git a/apps/api/app/controllers/api/software_highlighted_controller.rb b/apps/api/app/controllers/api/software_highlighted_controller.rb new file mode 100644 index 0000000..e15be53 --- /dev/null +++ b/apps/api/app/controllers/api/software_highlighted_controller.rb @@ -0,0 +1,12 @@ +module Api + class SoftwareHighlightedController < ApiController + def index + result = SoftwareHighlightedService.new.call + if result.nil? + render json: { error: "no highlighted software found" }, status: :not_found + else + render json: result + end + end + end +end diff --git a/apps/api/app/controllers/api_controller.rb b/apps/api/app/controllers/api_controller.rb new file mode 100644 index 0000000..af65e22 --- /dev/null +++ b/apps/api/app/controllers/api_controller.rb @@ -0,0 +1,2 @@ +class ApiController < ActionController::API +end diff --git a/apps/api/app/controllers/application_controller.rb b/apps/api/app/controllers/application_controller.rb new file mode 100644 index 0000000..0491b0a --- /dev/null +++ b/apps/api/app/controllers/application_controller.rb @@ -0,0 +1,3 @@ +class ApplicationController < ActionController::Base + protect_from_forgery with: :null_session +end diff --git a/apps/api/app/controllers/files_controller.rb b/apps/api/app/controllers/files_controller.rb new file mode 100644 index 0000000..f5b8177 --- /dev/null +++ b/apps/api/app/controllers/files_controller.rb @@ -0,0 +1,13 @@ +class FilesController < ApplicationController + def show + base = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath + full = base.join(params[:path]) + + return render plain: "Not Found", status: :not_found unless full.to_s.start_with?(base.to_s) + return render plain: "Not Found", status: :not_found unless full.exist? && full.file? + + send_file full.to_s, disposition: "inline" + rescue Errno::ENOENT + render plain: "Not Found", status: :not_found + end +end diff --git a/apps/api/app/controllers/update_controller.rb b/apps/api/app/controllers/update_controller.rb new file mode 100644 index 0000000..b7c3e18 --- /dev/null +++ b/apps/api/app/controllers/update_controller.rb @@ -0,0 +1,32 @@ +class UpdateController < ApiController + PLATFORM_SERVICES = { + "tic80" => "SoftwareUpdater::Tic80Service", + "ebitengine" => "SoftwareUpdater::EbitengineService", + "love" => "SoftwareUpdater::LoveService" + }.freeze + + def update + return render plain: "Unauthorized", status: :unauthorized unless authorized? + + platform = params[:platform] + name = params[:name] + version = params[:version] + + return render plain: "Version not provided", status: :bad_request if version.blank? + + service_class_name = PLATFORM_SERVICES[platform] + return render plain: "Unsupported platform: #{platform}", status: :bad_request unless service_class_name + + service_class_name.constantize.new.update(name, version) + render plain: "Updated" + rescue => e + Rails.logger.error("[UpdateController] #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}") + render plain: e.message, status: :internal_server_error + end + + private + + def authorized? + params[:secret] == ENV.fetch("UPDATE_SECRET", "") + end +end diff --git a/apps/api/app/models/admin_user.rb b/apps/api/app/models/admin_user.rb new file mode 100644 index 0000000..043bad2 --- /dev/null +++ b/apps/api/app/models/admin_user.rb @@ -0,0 +1,8 @@ +class AdminUser < ApplicationRecord + devise :database_authenticatable, + :recoverable, :rememberable, :validatable + + def self.ransackable_attributes(auth_object = nil) + %w[created_at email id updated_at] + end +end diff --git a/apps/api/app/models/application_record.rb b/apps/api/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/apps/api/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/apps/api/app/models/external_link.rb b/apps/api/app/models/external_link.rb new file mode 100644 index 0000000..d8818b2 --- /dev/null +++ b/apps/api/app/models/external_link.rb @@ -0,0 +1,15 @@ +class ExternalLink < ApplicationRecord + self.table_name = "external_links" + + belongs_to :software + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id label software_id updated_at url] + end + + def self.ransackable_associations(auth_object = nil) + %w[software] + end +end diff --git a/apps/api/app/models/release.rb b/apps/api/app/models/release.rb new file mode 100644 index 0000000..5a02f90 --- /dev/null +++ b/apps/api/app/models/release.rb @@ -0,0 +1,15 @@ +class Release < ApplicationRecord + self.table_name = "releases" + + belongs_to :software + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[cartridge_path created_at deleted_at docs_folder_path html_folder_path id software_id source_path updated_at version web_playable] + end + + def self.ransackable_associations(auth_object = nil) + %w[software] + end +end diff --git a/apps/api/app/models/software.rb b/apps/api/app/models/software.rb new file mode 100644 index 0000000..bfe9a82 --- /dev/null +++ b/apps/api/app/models/software.rb @@ -0,0 +1,18 @@ +class Software < ApplicationRecord + self.table_name = "softwares" + + has_many :releases, foreign_key: :software_id + has_many :external_links, foreign_key: :software_id + + accepts_nested_attributes_for :external_links, allow_destroy: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[author created_at desc highlighted id license name platform site status story title updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[releases external_links] + end +end diff --git a/apps/api/app/services/software_highlighted_service.rb b/apps/api/app/services/software_highlighted_service.rb new file mode 100644 index 0000000..2165aac --- /dev/null +++ b/apps/api/app/services/software_highlighted_service.rb @@ -0,0 +1,12 @@ +class SoftwareHighlightedService + def call + software = Software.includes(:external_links) + .where(highlighted: true) + .order(id: :desc) + .first + return nil unless software + + releases = Release.where(software_id: software.id).to_a + SoftwareSerializer.show_data(software, releases) + end +end diff --git a/apps/api/app/services/software_list_service.rb b/apps/api/app/services/software_list_service.rb new file mode 100644 index 0000000..6eb499d --- /dev/null +++ b/apps/api/app/services/software_list_service.rb @@ -0,0 +1,9 @@ +class SoftwareListService + def call + softwares = Software.includes(:releases, :external_links).all + items = softwares.map do |sw| + SoftwareSerializer.show_data(sw, sw.releases.to_a) + end + { softwares: items } + end +end diff --git a/apps/api/app/services/software_serializer.rb b/apps/api/app/services/software_serializer.rb new file mode 100644 index 0000000..8121df3 --- /dev/null +++ b/apps/api/app/services/software_serializer.rb @@ -0,0 +1,82 @@ +class SoftwareSerializer + GO_ZERO_TIME = "0001-01-01T00:00:00Z" + FILE_PATH_FROM = "/softwares/" + FILE_PATH_TO = "/file/" + + def self.show_data(software, releases) + sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse + + latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first + web_playable = sorted.find { |r| r.html_folder_path.present? } + + { + software: serialize_software(software), + releases: sorted.map { |r| serialize_release(r) }, + latestRelease: latest ? serialize_release(latest) : nil, + webPlayableRelease: web_playable ? serialize_release(web_playable) : nil + } + end + + def self.serialize_software(sw) + { + "ID" => sw.id, + "CreatedAt" => fmt(sw.created_at), + "UpdatedAt" => fmt(sw.updated_at), + "DeletedAt" => fmt_nullable(sw.deleted_at), + "name" => sw.name, + "title" => sw.title, + "author" => sw.author, + "desc" => sw.desc.to_s, + "story" => sw.story.to_s, + "license" => sw.license.to_s, + "platform" => sw.platform, + "status" => sw.status, + "highlighted" => sw.highlighted ? true : false, + "externalLinks" => sw.external_links.map { |el| serialize_external_link(el) } + } + end + + def self.serialize_release(r) + { + "ID" => r.id, + "CreatedAt" => fmt(r.created_at), + "UpdatedAt" => fmt(r.updated_at), + "DeletedAt" => fmt_nullable(r.deleted_at), + "softwareId" => r.software_id, + "version" => r.version, + "cartridgePath" => rewrite(r.cartridge_path), + "sourcePath" => rewrite(r.source_path), + "htmlFolderPath" => rewrite(r.html_folder_path), + "docsFolderPath" => rewrite(r.docs_folder_path) + } + end + + def self.serialize_external_link(el) + { + "ID" => el.id, + "CreatedAt" => fmt(el.created_at), + "UpdatedAt" => fmt(el.updated_at), + "DeletedAt" => fmt_nullable(el.deleted_at), + "softwareId" => el.software_id, + "label" => el.label, + "url" => el.url + } + end + + # null → Go zero-time string (matches GORM behavior) + def self.fmt(t) + return GO_ZERO_TIME if t.nil? + t.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ") + end + + # null → JSON null (matches gorm.DeletedAt behavior) + def self.fmt_nullable(t) + return nil if t.nil? + t.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ") + end + + def self.rewrite(path) + return "" if path.blank? + path.gsub(FILE_PATH_FROM, FILE_PATH_TO) + end +end diff --git a/apps/api/app/services/software_show_service.rb b/apps/api/app/services/software_show_service.rb new file mode 100644 index 0000000..4ec9f0e --- /dev/null +++ b/apps/api/app/services/software_show_service.rb @@ -0,0 +1,11 @@ +class SoftwareShowService + def initialize(name) + @name = name + end + + def call + software = Software.includes(:external_links).find_by!(name: @name) + releases = Release.where(software_id: software.id).to_a + SoftwareSerializer.show_data(software, releases) + end +end diff --git a/apps/api/app/services/software_updater/base_service.rb b/apps/api/app/services/software_updater/base_service.rb new file mode 100644 index 0000000..30ad544 --- /dev/null +++ b/apps/api/app/services/software_updater/base_service.rb @@ -0,0 +1,61 @@ +require "zip" +require "fileutils" + +module SoftwareUpdater + class BaseService + def initialize + @base_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares") + end + + private + + def full_path(filename) + File.join(@base_path, filename) + end + + def dir_exists?(dirname) + File.directory?(full_path(dirname)) + end + + def delete_dir(dirname) + FileUtils.rm_rf(full_path(dirname)) + end + + def create_dir(dirname) + FileUtils.mkdir_p(full_path(dirname)) + end + + def unzip_file(zip_filename, dest_dirname) + dest = full_path(dest_dirname) + Zip::File.open(full_path(zip_filename)) do |zip| + zip.each do |entry| + entry_dest = File.join(dest, entry.name) + FileUtils.mkdir_p(File.dirname(entry_dest)) + entry.extract(entry_dest) { true } + end + end + end + + def update_or_create_software(attrs) + software = Software.unscoped.find_or_initialize_by(name: attrs[:name]) + software.assign_attributes(attrs.except(:name)) + software.deleted_at = nil + software.save! + software + end + + def upsert_external_link(software_id, label, url) + link = ExternalLink.unscoped.find_or_initialize_by(software_id: software_id, label: label) + link.url = url + link.deleted_at = nil + link.save! + end + + def create_release_if_not_exists(attrs) + existing = Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version]) + return existing if existing + + Release.create!(attrs) + end + end +end diff --git a/apps/api/app/services/software_updater/ebitengine_service.rb b/apps/api/app/services/software_updater/ebitengine_service.rb new file mode 100644 index 0000000..555d091 --- /dev/null +++ b/apps/api/app/services/software_updater/ebitengine_service.rb @@ -0,0 +1,34 @@ +require "json" + +module SoftwareUpdater + class EbitengineService < BaseService + def update(name, version) + versioned = "#{name}-#{version}" + zip_file = "#{versioned}.html.zip" + html_dir = versioned + + delete_dir(html_dir) if dir_exists?(html_dir) + create_dir(html_dir) + unzip_file(zip_file, html_dir) + + metadata = parse_metadata(full_path("#{versioned}.metadata.json")) + site_url = metadata.delete(:site) + + software = update_or_create_software(metadata.merge(platform: "ebitengine")) + upsert_external_link(software.id, "Source Code", site_url) if site_url.present? + + create_release_if_not_exists( + software_id: software.id, + version: version, + html_folder_path: full_path(html_dir) + ) + end + + private + + def parse_metadata(path) + raw = JSON.parse(File.read(path), symbolize_names: true) + raw.slice(:name, :title, :author, :desc, :site, :license) + end + end +end diff --git a/apps/api/app/services/software_updater/love_service.rb b/apps/api/app/services/software_updater/love_service.rb new file mode 100644 index 0000000..baa0e46 --- /dev/null +++ b/apps/api/app/services/software_updater/love_service.rb @@ -0,0 +1,34 @@ +require "json" + +module SoftwareUpdater + class LoveService < BaseService + def update(name, version) + versioned = "#{name}-#{version}" + zip_file = "#{versioned}.html.zip" + html_dir = versioned + + delete_dir(html_dir) if dir_exists?(html_dir) + create_dir(html_dir) + unzip_file(zip_file, html_dir) + + metadata = parse_metadata(full_path("#{versioned}.metadata.json")) + site_url = metadata.delete(:site) + + software = update_or_create_software(metadata.merge(platform: "love")) + upsert_external_link(software.id, "Source Code", site_url) if site_url.present? + + create_release_if_not_exists( + software_id: software.id, + version: version, + html_folder_path: full_path(html_dir) + ) + end + + private + + def parse_metadata(path) + raw = JSON.parse(File.read(path), symbolize_names: true) + raw.slice(:name, :title, :author, :desc, :site, :license) + end + end +end diff --git a/apps/api/app/services/software_updater/tic80_service.rb b/apps/api/app/services/software_updater/tic80_service.rb new file mode 100644 index 0000000..01ed427 --- /dev/null +++ b/apps/api/app/services/software_updater/tic80_service.rb @@ -0,0 +1,50 @@ +module SoftwareUpdater + class Tic80Service < BaseService + def update(name, version) + versioned = "#{name}-#{version}" + zip_file = "#{versioned}.html.zip" + docs_zip_file = "#{versioned}-docs.zip" + html_dir = versioned + docs_dir = "#{versioned}-docs" + + delete_dir(html_dir) if dir_exists?(html_dir) + delete_dir(docs_dir) if dir_exists?(docs_dir) + + create_dir(html_dir) + unzip_file(zip_file, html_dir) + + create_dir(docs_dir) + unzip_file(docs_zip_file, docs_dir) + + metadata = parse_lua_metadata(full_path("#{versioned}.lua")) + site_url = metadata.delete(:site) + + software = update_or_create_software(metadata.merge(platform: "tic80")) + upsert_external_link(software.id, "Source Code", site_url) if site_url.present? + + create_release_if_not_exists( + software_id: software.id, + version: version, + cartridge_path: full_path("#{versioned}.tic"), + source_path: full_path("#{versioned}.lua"), + html_folder_path: full_path(html_dir), + docs_folder_path: full_path(docs_dir) + ) + end + + private + + def parse_lua_metadata(source_path) + metadata = {} + File.foreach(source_path) do |line| + break unless line.start_with?("--") + parts = line[2..].split(":", 2) + next if parts.length != 2 + key = parts[0].strip.downcase.to_sym + value = parts[1].strip + metadata[key] = value + end + metadata + end + end +end diff --git a/apps/api/bin/bundle b/apps/api/bin/bundle new file mode 100755 index 0000000..7b83a3d --- /dev/null +++ b/apps/api/bin/bundle @@ -0,0 +1,81 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application's executable is in bin/bundle. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? + return unless "--version" == ARGV.first + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + bundler_version = a if update_index && update_index.succ == i + next unless a =~ /\A--bundler\z/ + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = "#{gemfile}.lock" + return lockfile if File.exist?(lockfile) + nil + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def lockfile_version + return unless lockfile + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s*(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def load_bundler! + $LOAD_PATH.unshift(bundler_spec.lib_dirs_glob) if bundler_spec + require "bundler" + unless bundler_spec + Gem::GemRunner.new.run(["install", "bundler", "--version", bundler_version]) if bundler_version + require "bundler" + end + end + + def bundler_spec + @bundler_spec ||= Gem::Specification.find_by_name("bundler", bundler_version) + rescue Gem::MissingSpecVersionError, Gem::MissingSpecError + nil + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/apps/api/bin/rails b/apps/api/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/apps/api/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/apps/api/config.ru b/apps/api/config.ru new file mode 100644 index 0000000..ce7479a --- /dev/null +++ b/apps/api/config.ru @@ -0,0 +1,3 @@ +require_relative "config/environment" +run Rails.application +Rails.application.load_server diff --git a/apps/api/config/application.rb b/apps/api/config/application.rb new file mode 100644 index 0000000..fed85df --- /dev/null +++ b/apps/api/config/application.rb @@ -0,0 +1,23 @@ +require_relative "boot" +require "rails" +require "active_model/railtie" +require "active_record/railtie" +require "action_controller/railtie" +require "action_view/railtie" +require "action_dispatch/railtie" +require "sprockets/railtie" + +Bundler.require(*Rails.groups) + +module Api + class Application < Rails::Application + config.load_defaults 8.0 + + config.time_zone = "UTC" + config.active_record.default_timezone = :utc + + config.assets.prefix = "/admin/assets" + + config.autoload_lib(ignore: %w[assets tasks]) + end +end diff --git a/apps/api/config/boot.rb b/apps/api/config/boot.rb new file mode 100644 index 0000000..8d433ae --- /dev/null +++ b/apps/api/config/boot.rb @@ -0,0 +1,4 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' +require 'bootsnap/setup' diff --git a/apps/api/config/database.yml b/apps/api/config/database.yml new file mode 100644 index 0000000..ec50605 --- /dev/null +++ b/apps/api/config/database.yml @@ -0,0 +1,16 @@ +default: &default + adapter: mysql2 + encoding: utf8mb4 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + username: <%= ENV.fetch("DB_USER", "root") %> + password: <%= ENV.fetch("DB_PASSWORD", "") %> + host: <%= ENV.fetch("DB_HOST", "mysql") %> + port: <%= ENV.fetch("DB_PORT", "3306") %> + +development: + <<: *default + database: <%= ENV.fetch("DB_NAME", "softwares") %> + +production: + <<: *default + database: <%= ENV.fetch("DB_NAME", "softwares") %> diff --git a/apps/api/config/environment.rb b/apps/api/config/environment.rb new file mode 100644 index 0000000..73a3979 --- /dev/null +++ b/apps/api/config/environment.rb @@ -0,0 +1,3 @@ +require_relative "application" + +Rails.application.initialize! diff --git a/apps/api/config/environments/development.rb b/apps/api/config/environments/development.rb new file mode 100644 index 0000000..829e557 --- /dev/null +++ b/apps/api/config/environments/development.rb @@ -0,0 +1,14 @@ +Rails.application.configure do + config.eager_load = false + config.consider_all_requests_local = true + config.server_timing = true + + config.cache_classes = false + config.action_controller.perform_caching = false + + config.active_record.migration_error = :page_load + config.active_record.verbose_query_logs = true + + config.log_level = :debug + config.log_tags = [:request_id] +end diff --git a/apps/api/config/environments/production.rb b/apps/api/config/environments/production.rb new file mode 100644 index 0000000..cee11da --- /dev/null +++ b/apps/api/config/environments/production.rb @@ -0,0 +1,7 @@ +Rails.application.configure do + config.eager_load = true + config.consider_all_requests_local = false + config.log_level = :info + config.log_tags = [:request_id] + config.active_record.verbose_query_logs = false +end diff --git a/apps/api/config/initializers/active_admin.rb b/apps/api/config/initializers/active_admin.rb new file mode 100644 index 0000000..5fa71dd --- /dev/null +++ b/apps/api/config/initializers/active_admin.rb @@ -0,0 +1,14 @@ +ActiveAdmin.setup do |config| + config.site_title = "Teletype Games Admin" + + config.authentication_method = :authenticate_admin_user! + config.current_user_method = :current_admin_user + config.logout_link_path = :destroy_admin_user_session_path + config.logout_link_method = :delete + + config.comments = false + config.batch_actions = true + config.filter_attributes = [:password, :password_confirmation, :current_password, :encrypted_password] + + config.localize_format = :long +end diff --git a/apps/api/config/initializers/devise.rb b/apps/api/config/initializers/devise.rb new file mode 100644 index 0000000..03699c2 --- /dev/null +++ b/apps/api/config/initializers/devise.rb @@ -0,0 +1,16 @@ +Devise.setup do |config| + config.mailer_sender = "noreply@teletype.hu" + + require "devise/orm/active_record" + + config.case_insensitive_keys = [:email] + config.strip_whitespace_keys = [:email] + config.skip_session_storage = [:http_auth] + config.stretches = Rails.env.test? ? 1 : 12 + config.reconfirmable = false + config.expire_all_remember_me_on_sign_out = true + config.password_length = 6..128 + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + config.reset_password_within = 6.hours + config.sign_out_via = :delete +end diff --git a/apps/api/config/initializers/security_headers.rb b/apps/api/config/initializers/security_headers.rb new file mode 100644 index 0000000..091c22e --- /dev/null +++ b/apps/api/config/initializers/security_headers.rb @@ -0,0 +1,5 @@ +Rails.application.config.action_dispatch.default_headers = { + "Cross-Origin-Opener-Policy" => "same-origin", + "Cross-Origin-Embedder-Policy" => "require-corp", + "Cross-Origin-Resource-Policy" => "same-origin" +} diff --git a/apps/api/config/puma.rb b/apps/api/config/puma.rb new file mode 100644 index 0000000..59ce65d --- /dev/null +++ b/apps/api/config/puma.rb @@ -0,0 +1,8 @@ +max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +port ENV.fetch("PORT", 3000) +environment ENV.fetch("RAILS_ENV", "development") +pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") +plugin :tmp_restart diff --git a/apps/api/config/routes.rb b/apps/api/config/routes.rb new file mode 100644 index 0000000..3496e5f --- /dev/null +++ b/apps/api/config/routes.rb @@ -0,0 +1,12 @@ +Rails.application.routes.draw do + devise_for :admin_users, ActiveAdmin::Devise.config + ActiveAdmin.routes(self) + + namespace :api do + get "software", to: "software#index" + get "software/highlighted", to: "software_highlighted#index" + end + + get "update", to: "update#update" + get "file/*path", to: "files#show", format: false +end diff --git a/apps/api/db/migrate/20260505000001_create_softwares.rb b/apps/api/db/migrate/20260505000001_create_softwares.rb new file mode 100644 index 0000000..d9a4cb5 --- /dev/null +++ b/apps/api/db/migrate/20260505000001_create_softwares.rb @@ -0,0 +1,24 @@ +class CreateSoftwares < ActiveRecord::Migration[8.1] + def change + create_table :softwares, id: { type: :bigint, unsigned: true }, + charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", + if_not_exists: true do |t| + t.string :author + t.datetime :created_at, precision: 3 + t.datetime :deleted_at, precision: 3 + t.text :desc + t.boolean :highlighted, default: false + t.string :license, limit: 128 + t.string :name, limit: 128 + t.string :platform, limit: 128 + t.string :site + t.string :status, limit: 20, default: "development" + t.text :story + t.string :title + t.datetime :updated_at, precision: 3 + + t.index :deleted_at, name: "idx_softwares_deleted_at" + t.index :name, name: "idx_softwares_name", unique: true + end + end +end diff --git a/apps/api/db/migrate/20260505000002_create_releases.rb b/apps/api/db/migrate/20260505000002_create_releases.rb new file mode 100644 index 0000000..37e4df0 --- /dev/null +++ b/apps/api/db/migrate/20260505000002_create_releases.rb @@ -0,0 +1,23 @@ +class CreateReleases < ActiveRecord::Migration[8.1] + def change + create_table :releases, id: { type: :bigint, unsigned: true }, + charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", + if_not_exists: true do |t| + t.string :cartridge_path + t.datetime :created_at, precision: 3 + t.datetime :deleted_at, precision: 3 + t.string :docs_folder_path + t.string :html_folder_path + t.bigint :software_id, unsigned: true + t.string :source_path + t.datetime :updated_at, precision: 3 + t.string :version, limit: 64 + t.boolean :web_playable, default: false + + t.index :deleted_at, name: "idx_releases_deleted_at" + t.index :software_id, name: "idx_releases_software_id" + end + + add_foreign_key :releases, :softwares, name: "fk_softwares_releases" unless foreign_key_exists?(:releases, :softwares) + end +end diff --git a/apps/api/db/migrate/20260505000003_create_external_links.rb b/apps/api/db/migrate/20260505000003_create_external_links.rb new file mode 100644 index 0000000..8a32568 --- /dev/null +++ b/apps/api/db/migrate/20260505000003_create_external_links.rb @@ -0,0 +1,19 @@ +class CreateExternalLinks < ActiveRecord::Migration[8.1] + def change + create_table :external_links, id: { type: :bigint, unsigned: true }, + charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", + if_not_exists: true do |t| + t.datetime :created_at, precision: 3 + t.datetime :deleted_at, precision: 3 + t.string :label, limit: 128 + t.bigint :software_id, unsigned: true + t.datetime :updated_at, precision: 3 + t.string :url + + t.index :deleted_at, name: "idx_external_links_deleted_at" + t.index :software_id, name: "idx_external_links_software_id" + end + + add_foreign_key :external_links, :softwares, name: "fk_softwares_external_links" unless foreign_key_exists?(:external_links, :softwares) + end +end diff --git a/apps/api/db/migrate/20260505000004_create_admin_users.rb b/apps/api/db/migrate/20260505000004_create_admin_users.rb new file mode 100644 index 0000000..6d1a0f5 --- /dev/null +++ b/apps/api/db/migrate/20260505000004_create_admin_users.rb @@ -0,0 +1,17 @@ +class CreateAdminUsers < ActiveRecord::Migration[8.1] + def change + create_table :admin_users, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", + if_not_exists: true do |t| + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + t.string :reset_password_token + t.datetime :reset_password_sent_at + t.datetime :remember_created_at + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + + t.index :email, name: "index_admin_users_on_email", unique: true + t.index :reset_password_token, name: "index_admin_users_on_reset_password_token", unique: true + end + end +end diff --git a/apps/api/db/schema.rb b/apps/api/db/schema.rb new file mode 100644 index 0000000..91d355c --- /dev/null +++ b/apps/api/db/schema.rb @@ -0,0 +1,173 @@ +# 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.1].define(version: 2026_05_05_000004) do + create_table "admin_users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.datetime "remember_created_at" + t.datetime "reset_password_sent_at" + t.string "reset_password_token" + t.datetime "updated_at", null: false + t.index ["email"], name: "index_admin_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true + end + + create_table "articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.text "content" + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.string "slug" + t.string "title" + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + end + + create_table "external_links", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.string "label", limit: 128 + t.bigint "software_id", unsigned: true + t.datetime "updated_at", precision: 3 + t.string "url" + t.index ["deleted_at"], name: "idx_external_links_deleted_at" + t.index ["software_id"], name: "idx_external_links_software_id" + end + + create_table "goadmin_menu", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.string "header", limit: 150 + t.string "icon", limit: 50, null: false + t.integer "order", default: 0, null: false + t.integer "parent_id", default: 0, null: false + t.string "plugin_name", limit: 150, default: "", null: false + t.string "title", limit: 50, null: false + t.integer "type", default: 0, null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.string "uri", null: false + t.string "uuid", limit: 150 + end + + create_table "goadmin_operation_log", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.text "input", null: false + t.string "ip", limit: 15, null: false + t.string "method", limit: 10, null: false + t.string "path", null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.integer "user_id", null: false + end + + create_table "goadmin_permissions", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.string "http_method" + t.text "http_path" + t.string "name", limit: 50, null: false + t.string "slug", limit: 50, null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.index ["name"], name: "permissions_name_unique", unique: true + t.index ["slug"], name: "permissions_slug_unique", unique: true + end + + create_table "goadmin_role_menu", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.integer "menu_id", null: false + t.integer "role_id", null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + end + + create_table "goadmin_role_permissions", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.integer "permission_id", null: false + t.integer "role_id", null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + end + + create_table "goadmin_role_users", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.integer "role_id", null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.integer "user_id", null: false + end + + create_table "goadmin_roles", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.string "name", limit: 100, null: false + t.string "slug", limit: 100, null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.index ["name"], name: "roles_name_unique", unique: true + t.index ["slug"], name: "roles_slug_unique", unique: true + end + + create_table "goadmin_site", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.string "description", limit: 3000, null: false + t.string "key_", limit: 100, null: false + t.integer "state", limit: 1, default: 0, null: false, unsigned: true + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.text "value", null: false + t.index ["key_"], name: "site_key_unique", unique: true + end + + create_table "goadmin_user_permissions", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.integer "permission_id", null: false + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.integer "user_id", null: false + end + + create_table "goadmin_users", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "avatar" + t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" } + t.string "name", limit: 100, null: false + t.string "password", limit: 100, null: false + t.string "remember_token", limit: 100 + t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" } + t.string "username", limit: 100, null: false + t.index ["username"], name: "users_username_unique", unique: true + end + + create_table "releases", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "cartridge_path" + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.string "docs_folder_path" + t.string "html_folder_path" + t.bigint "software_id", unsigned: true + t.string "source_path" + t.datetime "updated_at", precision: 3 + t.string "version", limit: 64 + t.boolean "web_playable", default: false + t.index ["deleted_at"], name: "idx_releases_deleted_at" + t.index ["software_id"], name: "idx_releases_software_id" + end + + create_table "softwares", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "author" + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.text "desc" + t.boolean "highlighted", default: false + t.string "license", limit: 128 + t.string "name", limit: 128 + t.string "platform", limit: 128 + t.string "site" + t.string "status", limit: 20, default: "development" + t.text "story" + t.string "title" + t.datetime "updated_at", precision: 3 + t.index ["deleted_at"], name: "idx_softwares_deleted_at" + t.index ["name"], name: "idx_softwares_name", unique: true + end + + add_foreign_key "external_links", "softwares", name: "fk_softwares_external_links" + add_foreign_key "releases", "softwares", name: "fk_softwares_releases" +end diff --git a/apps/api/db/seeds.rb b/apps/api/db/seeds.rb new file mode 100644 index 0000000..780ddb4 --- /dev/null +++ b/apps/api/db/seeds.rb @@ -0,0 +1,4 @@ +AdminUser.find_or_create_by!(email: ENV.fetch("ADMIN_EMAIL", "admin@teletype.hu")) do |user| + user.password = ENV.fetch("ADMIN_PASSWORD", "password123") + user.password_confirmation = ENV.fetch("ADMIN_PASSWORD", "password123") +end diff --git a/apps/api/entrypoint.sh b/apps/api/entrypoint.sh new file mode 100644 index 0000000..3b044c6 --- /dev/null +++ b/apps/api/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +mkdir -p tmp/pids tmp/cache tmp/sockets log +rm -f tmp/pids/server.pid + +bundle check || bundle install + +bundle exec rails db:migrate +bundle exec rails db:seed + +bundle exec rails server -b 0.0.0.0 -p 3000 diff --git a/apps/catalog/.air.toml b/apps/catalog/.air.toml deleted file mode 100755 index f33887d..0000000 --- a/apps/catalog/.air.toml +++ /dev/null @@ -1,39 +0,0 @@ -root = "." -testdata_dir = "testdata" -tmp_dir = "tmp" - -[build] - args_bin = [] - bin = "./tmp/teletype_softwares" - cmd = "go build -o ./tmp/teletype_softwares ./main.go" - delay = 1000 - exclude_dir = [ - "tmp", - ] - exclude_file = [] - exclude_regex = ["_test.go", "data"] - exclude_unchanged = false - follow_symlink = false - full_bin = "" - include_dir = [] - include_ext = ["go", "tpl", "tmpl", "html"] - kill_delay = "0s" - log = "build-errors.log" - send_interrupt = false - stop_on_error = true - -[color] - app = "" - build = "yellow" - main = "magenta" - runner = "green" - watcher = "cyan" - -[log] - time = false - -[misc] - clean_on_exit = false - -[screen] - clear_on_rebuild = false diff --git a/apps/catalog/Dockerfile b/apps/catalog/Dockerfile deleted file mode 100644 index 8d8c0a0..0000000 --- a/apps/catalog/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -# stage 1: building application binary file -FROM --platform=linux/amd64 golang:1.19-alpine as build - -WORKDIR /app - -COPY . . - -RUN go mod tidy -RUN go build -o main ./main.go - -# stage 2: copy only the application binary file and necessary files to the alpine container -FROM --platform=linux/amd64 alpine:latest -RUN apk --update add ca-certificates - -WORKDIR /app - -COPY --from=build /app/main . -COPY --from=build /app/http/openapi.yml . - -# run the service on container startup. -CMD ["/app/main"] \ No newline at end of file diff --git a/apps/catalog/Dockerfile.development b/apps/catalog/Dockerfile.development deleted file mode 100755 index eb121dd..0000000 --- a/apps/catalog/Dockerfile.development +++ /dev/null @@ -1,14 +0,0 @@ -FROM --platform=linux/amd64 golang:1.23-alpine - -RUN apk add curl git unzip - -WORKDIR /app - -COPY . . - -RUN chmod +x ./entrypoint.sh - -RUN curl -fLo install.sh https://raw.githubusercontent.com/cosmtrek/air/master/install.sh \ - && chmod +x install.sh && sh install.sh && cp ./bin/air /bin/air - -CMD ["./entrypoint.sh"] diff --git a/apps/catalog/domain/domain.go b/apps/catalog/domain/domain.go deleted file mode 100755 index 38f6374..0000000 --- a/apps/catalog/domain/domain.go +++ /dev/null @@ -1,66 +0,0 @@ -package domain - -import ( - "teletype_softwares/lib/mysql_utils" -) - -type Domain struct { - SoftwareRepository SoftwareRepositoryInterface - ReleaseRepository ReleaseRepositoryInterface - FileRepository FileRepositoryInterface - ExternalLinkRepository ExternalLinkRepositoryInterface - DownloadService DownloadServiceInterface - SoftwareUpdaterService SoftwareUpdaterServiceInterface - SoftwareService SoftwareServiceInterface - FileService FileServiceInterface -} - -func NewDomain() Domain { - DB := mysql_utils.Init() - MigrateGoDatabase(DB) - - software_repository := &SoftwareRepository{db: DB} - release_repository := &ReleaseRepository{db: DB} - file_repository := NewFileRepository() - external_link_repository := &ExternalLinkRepository{db: DB} - - software_service := NewSoftwareService(software_repository, release_repository) - - tic80_updater := NewSoftwareUpdaterTIC80Service( - software_repository, - release_repository, - file_repository, - external_link_repository, - ) - - ebitengine_updater := NewSoftwareUpdaterEbitengineService( - software_repository, - release_repository, - file_repository, - external_link_repository, - ) - - love_updater := NewSoftwareUpdaterLoveService( - software_repository, - release_repository, - file_repository, - external_link_repository, - ) - - software_updater_service := NewSoftwareUpdaterService(tic80_updater, ebitengine_updater, love_updater) - - download_service := NewDownloadService(software_repository, release_repository) - - file_service := NewFileService(file_repository) - - return Domain{ - SoftwareRepository: software_repository, - ReleaseRepository: release_repository, - FileRepository: file_repository, - ExternalLinkRepository: external_link_repository, - DownloadService: download_service, - SoftwareUpdaterService: software_updater_service, - SoftwareService: software_service, - FileService: file_service, - } -} diff --git a/apps/catalog/domain/model.external_link.go b/apps/catalog/domain/model.external_link.go deleted file mode 100644 index 6590876..0000000 --- a/apps/catalog/domain/model.external_link.go +++ /dev/null @@ -1,10 +0,0 @@ -package domain - -import "gorm.io/gorm" - -type ExternalLink struct { - gorm.Model - SoftwareID uint `gorm:"index" json:"softwareId"` - Label string `gorm:"size:128" json:"label"` - URL string `gorm:"size:255" json:"url"` -} diff --git a/apps/catalog/domain/model.migrate.go b/apps/catalog/domain/model.migrate.go deleted file mode 100644 index d82689a..0000000 --- a/apps/catalog/domain/model.migrate.go +++ /dev/null @@ -1,13 +0,0 @@ -package domain - -import ( - "gorm.io/gorm" -) - -func MigrateGoDatabase(db *gorm.DB) { - db.AutoMigrate( - Software{}, - Release{}, - ExternalLink{}, - ) -} diff --git a/apps/catalog/domain/model.release.go b/apps/catalog/domain/model.release.go deleted file mode 100644 index f13df63..0000000 --- a/apps/catalog/domain/model.release.go +++ /dev/null @@ -1,13 +0,0 @@ -package domain - -import "gorm.io/gorm" - -type Release struct { - gorm.Model - SoftwareID uint `gorm:"index" json:"softwareId"` - Version string `gorm:"size:64" json:"version"` - CartridgePath string `gorm:"size:255" json:"cartridgePath"` - SourcePath string `gorm:"size:255" json:"sourcePath"` - HTMLFolderPath string `gorm:"size:255" json:"htmlFolderPath"` - DocsFolderPath string `gorm:"size:255" json:"docsFolderPath"` -} diff --git a/apps/catalog/domain/model.software.go b/apps/catalog/domain/model.software.go deleted file mode 100644 index c0365b4..0000000 --- a/apps/catalog/domain/model.software.go +++ /dev/null @@ -1,18 +0,0 @@ -package domain - -import "gorm.io/gorm" - -type Software struct { - gorm.Model - Name string `gorm:"uniqueIndex;size:128" json:"name"` - Title string `gorm:"size:255" json:"title"` - Author string `gorm:"size:255" json:"author"` - Desc string `gorm:"type:text" json:"desc"` - Story string `gorm:"type:text" json:"story"` - License string `gorm:"size:128" json:"license"` - Platform string `gorm:"size:128" json:"platform"` - Status string `gorm:"size:20;default:development" json:"status"` - Highlighted bool `gorm:"default:false" json:"highlighted"` - Releases []Release `json:"-"` - ExternalLinks []ExternalLink `json:"externalLinks"` -} diff --git a/apps/catalog/domain/repository.external_link.go b/apps/catalog/domain/repository.external_link.go deleted file mode 100644 index 8990425..0000000 --- a/apps/catalog/domain/repository.external_link.go +++ /dev/null @@ -1,23 +0,0 @@ -package domain - -import "gorm.io/gorm" - -type ExternalLinkRepositoryInterface interface { - UpsertByLabel(softwareID uint, label, url string) error -} - -type ExternalLinkRepository struct { - db *gorm.DB -} - -func (r *ExternalLinkRepository) UpsertByLabel(softwareID uint, label, url string) error { - var link ExternalLink - err := r.db.Where("software_id = ? AND label = ?", softwareID, label).First(&link).Error - if err == gorm.ErrRecordNotFound { - return r.db.Create(&ExternalLink{SoftwareID: softwareID, Label: label, URL: url}).Error - } - if err != nil { - return err - } - return r.db.Model(&link).Update("url", url).Error -} diff --git a/apps/catalog/domain/repository.file.go b/apps/catalog/domain/repository.file.go deleted file mode 100644 index 61dea1f..0000000 --- a/apps/catalog/domain/repository.file.go +++ /dev/null @@ -1,85 +0,0 @@ -package domain - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" -) - -type FileRepositoryInterface interface { - GetPath(path string) string - FileExists(path string) bool - CreateDir(path string) error - DeleteDir(path string) error - DeleteFile(path string) error - MoveFile(src_path, dest_path string) error - UnzipFile(path, dest_path string) error -} - -type FileRepository struct { - fileContainerPath string -} - -func NewFileRepository() *FileRepository { - file_container_path, _ := os.LookupEnv("FILE_CONTAINER_PATH") - return &FileRepository{ - fileContainerPath: file_container_path, - } -} - -func (fr *FileRepository) GetPath(path string) string { - return filepath.Join(fr.fileContainerPath, path) -} - -func (fr *FileRepository) FileExists(path string) bool { - full_path := fr.GetPath(path) - _, err := os.Stat(full_path) - return err == nil -} - -func (fr *FileRepository) CreateDir(path string) error { - full_path := fr.GetPath(path) - return os.MkdirAll(full_path, 0755) -} - -func (fr *FileRepository) DeleteDir(path string) error { - full_path := fr.GetPath(path) - return os.RemoveAll(full_path) -} - -func (fr *FileRepository) DeleteFile(path string) error { - full_path := fr.GetPath(path) - return os.RemoveAll(full_path) -} - -func (fr *FileRepository) MoveFile(src_path, dest_path string) error { - full_src_path := fr.GetPath(src_path) - full_dest_path := fr.GetPath(dest_path) - - dest_dir := filepath.Dir(full_dest_path) - if err := os.MkdirAll(dest_dir, 0755); err != nil { - return fmt.Errorf("failed to create destination directory: %w", err) - } - - return os.Rename(full_src_path, full_dest_path) -} - -func (fr *FileRepository) UnzipFile(zipfile, desc string) error { - zip_path := fr.GetPath(zipfile) - desc_path := fr.GetPath(desc) - - if err := os.MkdirAll(desc_path, 0755); err != nil { - return err - } - - cmd := exec.Command("unzip", "-q", zip_path, "-d", desc_path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - return err - } - - return os.Remove(zip_path) -} diff --git a/apps/catalog/domain/repository.release.go b/apps/catalog/domain/repository.release.go deleted file mode 100644 index 3a227ea..0000000 --- a/apps/catalog/domain/repository.release.go +++ /dev/null @@ -1,49 +0,0 @@ -package domain - -import "gorm.io/gorm" - -type ReleaseRepositoryInterface interface { - Create(release *Release) error - CreateIfNotExist(release *Release) error - FindLatestBySoftwareID(software_id uint) (*Release, error) - FindBySoftwareIDAndVersion(software_id uint, version string) (*Release, error) - ListBySoftwareID(software_id uint) []Release -} - -type ReleaseRepository struct { - db *gorm.DB -} - -func (r *ReleaseRepository) Create(release *Release) error { - return r.db.Create(release).Error -} - -func (r *ReleaseRepository) CreateIfNotExist(release *Release) error { - var existing Release - err := r.db.Where("software_id = ? AND version = ?", release.SoftwareID, release.Version).First(&existing).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return r.db.Create(release).Error - } - return err - } - return nil -} - -func (r *ReleaseRepository) FindLatestBySoftwareID(software_id uint) (*Release, error) { - var release Release - err := r.db.Where("software_id = ?", software_id).Order("created_at desc").First(&release).Error - return &release, err -} - -func (r *ReleaseRepository) FindBySoftwareIDAndVersion(software_id uint, version string) (*Release, error) { - var release Release - err := r.db.Where("software_id = ? AND version = ?", software_id, version).First(&release).Error - return &release, err -} - -func (r *ReleaseRepository) ListBySoftwareID(software_id uint) []Release { - var releases []Release - r.db.Where("software_id = ?", software_id).Find(&releases) - return releases -} diff --git a/apps/catalog/domain/repository.software.go b/apps/catalog/domain/repository.software.go deleted file mode 100644 index e37e848..0000000 --- a/apps/catalog/domain/repository.software.go +++ /dev/null @@ -1,52 +0,0 @@ -package domain - -import "gorm.io/gorm" - -type SoftwareRepositoryInterface interface { - List() ([]Software, error) - GetByName(name string) (*Software, error) - UpdateOrCreate(software *Software) error - GetHighlighted() (*Software, error) -} - -type SoftwareRepository struct { - db *gorm.DB -} - -func (r *SoftwareRepository) GetHighlighted() (*Software, error) { - var software Software - if err := r.db.Preload("Releases").Preload("ExternalLinks").Where("highlighted = 1").Order("id DESC").First(&software).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &software, nil -} - - -func (r *SoftwareRepository) GetByName(name string) (*Software, error) { - var software Software - if err := r.db.Preload("Releases").Preload("ExternalLinks").Where("name = ?", name).First(&software).Error; err != nil { - return nil, err - } - return &software, nil -} - -func (r *SoftwareRepository) List() ([]Software, error) { - var softwares []Software - if err := r.db.Preload("Releases").Preload("ExternalLinks").Find(&softwares).Error; err != nil { - return nil, err - } - return softwares, nil -} - -func (r *SoftwareRepository) UpdateOrCreate(software *Software) error { - var existing Software - if err := r.db.Where("name = ?", software.Name).First(&existing).Error; err == nil { - software.ID = existing.ID - return r.db.Model(&existing).Updates(software).Error - } else { - return r.db.Create(software).Error - } -} diff --git a/apps/catalog/domain/service.download.go b/apps/catalog/domain/service.download.go deleted file mode 100644 index f0ca7c9..0000000 --- a/apps/catalog/domain/service.download.go +++ /dev/null @@ -1,56 +0,0 @@ -package domain - -import ( - "errors" -) - -var ( - ErrSoftwareNotFound = errors.New("software not found") - ErrNoReleasesFound = errors.New("no releases found for software") - ErrReleaseNotFound = errors.New("release not found for software and version") -) - -type DownloadServiceInterface interface { - GetLatestRelease(software_name string) (*Release, error) - GetSpecificRelease(software_name string, version string) (*Release, error) -} - -type DownloadService struct { - softwareRepository SoftwareRepositoryInterface - releaseRepository ReleaseRepositoryInterface -} - -func NewDownloadService(software_repository SoftwareRepositoryInterface, release_repository ReleaseRepositoryInterface) *DownloadService { - return &DownloadService{ - softwareRepository: software_repository, - releaseRepository: release_repository, - } -} - -func (s *DownloadService) GetLatestRelease(software_name string) (*Release, error) { - software, err := s.softwareRepository.GetByName(software_name) - if err != nil { - return nil, ErrSoftwareNotFound - } - - release, err := s.releaseRepository.FindLatestBySoftwareID(software.ID) - if err != nil { - return nil, ErrNoReleasesFound - } - - return release, nil -} - -func (s *DownloadService) GetSpecificRelease(software_name string, version string) (*Release, error) { - software, err := s.softwareRepository.GetByName(software_name) - if err != nil { - return nil, ErrSoftwareNotFound - } - - release, err := s.releaseRepository.FindBySoftwareIDAndVersion(software.ID, version) - if err != nil { - return nil, ErrReleaseNotFound - } - - return release, nil -} diff --git a/apps/catalog/domain/service.file.go b/apps/catalog/domain/service.file.go deleted file mode 100644 index 24c986b..0000000 --- a/apps/catalog/domain/service.file.go +++ /dev/null @@ -1,19 +0,0 @@ -package domain - -type FileServiceInterface interface { - GetPath(path string) string -} - -type FileService struct { - FileRepository FileRepositoryInterface -} - -func NewFileService(file_repository FileRepositoryInterface) *FileService { - return &FileService{ - FileRepository: file_repository, - } -} - -func (s *FileService) GetPath(path string) string { - return s.FileRepository.GetPath(path) -} diff --git a/apps/catalog/domain/service.software.go b/apps/catalog/domain/service.software.go deleted file mode 100644 index 810c652..0000000 --- a/apps/catalog/domain/service.software.go +++ /dev/null @@ -1,153 +0,0 @@ -package domain - -import ( - "fmt" - "sort" -) - -type SoftwareListDTO struct { - Software - LatestRelease *Release -} - -type SoftwareDetaildListDTO struct { - Softwares []SoftwareShowData `json:"softwares"` -} - -type SoftwareShowData struct { - Software *Software `json:"software"` - Releases []Release `json:"releases"` - LatestRelease *Release `json:"latestRelease"` - WebPlayableRelease *Release `json:"webPlayableRelease"` -} - -type SoftwareServiceInterface interface { - List() ([]SoftwareListDTO, error) - DetailedList() (*SoftwareDetaildListDTO, error) - GetByName(name string) (*Software, error) - GetLatestRelease(software_id string) (*Release, error) - GetForShowByName(name string) (*SoftwareShowData, error) - GetHighlighted() (*SoftwareShowData, error) -} - -type SoftwareService struct { - softeare_repository SoftwareRepositoryInterface - release_repository ReleaseRepositoryInterface -} - -func (s *SoftwareService) GetHighlighted() (*SoftwareShowData, error) { - software, err := s.softeare_repository.GetHighlighted() - if err != nil { - return nil, err - } - if software == nil { - return nil, nil - } - - return s.GetForShowByName(software.Name) -} - -func NewSoftwareService( - softeare_repository SoftwareRepositoryInterface, - release_repository ReleaseRepositoryInterface, -) *SoftwareService { - return &SoftwareService{ - softeare_repository: softeare_repository, - release_repository: release_repository, - } -} - -func (s *SoftwareService) List() ([]SoftwareListDTO, error) { - softwares, err := s.softeare_repository.List() - if err != nil { - return nil, err - } - - dtos := make([]SoftwareListDTO, 0, len(softwares)) - for _, sw := range softwares { - dto := SoftwareListDTO{Software: sw} - if len(sw.Releases) > 0 { - sort.Slice(sw.Releases, func(i, j int) bool { - return sw.Releases[i].CreatedAt.After(sw.Releases[j].CreatedAt) - }) - dto.LatestRelease = &sw.Releases[0] - } - dtos = append(dtos, dto) - } - - return dtos, nil -} - -func (s *SoftwareService) GetByName(name string) (*Software, error) { - return s.softeare_repository.GetByName(name) -} - -func (s *SoftwareService) GetForShowByName(name string) (*SoftwareShowData, error) { - software, err := s.softeare_repository.GetByName(name) - if err != nil { - return nil, err - } - if software == nil { - return nil, fmt.Errorf("software not found") - } - - releases := s.release_repository.ListBySoftwareID(software.ID) - - var latest_release *Release - if len(releases) > 0 { - sort.Slice(releases, func(i, j int) bool { - return releases[i].CreatedAt.After(releases[j].CreatedAt) - }) - latest_release = &releases[0] - } - - var web_playable_release *Release - for i := range releases { - if releases[i].HTMLFolderPath != "" { - web_playable_release = &releases[i] // fix: slice elem címe, nem a loop változóé - break - } - } - - return &SoftwareShowData{ - Software: software, - Releases: releases, - LatestRelease: latest_release, - WebPlayableRelease: web_playable_release, - }, nil -} - -func (s *SoftwareService) GetLatestRelease(software_id string) (*Release, error) { - software, err := s.softeare_repository.GetByName(software_id) - if err != nil { - return nil, err - } - if software == nil { - return nil, nil - } - if len(software.Releases) == 0 { - return nil, nil - } - sort.Slice(software.Releases, func(i, j int) bool { - return software.Releases[i].CreatedAt.After(software.Releases[j].CreatedAt) - }) - return &software.Releases[0], nil -} - -func (s *SoftwareService) DetailedList() (*SoftwareDetaildListDTO, error) { - softwares, err := s.softeare_repository.List() - if err != nil { - return nil, err - } - - dtos := make([]SoftwareShowData, 0, len(softwares)) - for _, software := range softwares { - showData, err := s.GetForShowByName(software.Name) - if err != nil { - return nil, err - } - dtos = append(dtos, *showData) - } - - return &SoftwareDetaildListDTO{Softwares: dtos}, nil -} diff --git a/apps/catalog/domain/service.software_updater.go b/apps/catalog/domain/service.software_updater.go deleted file mode 100644 index 2d15c10..0000000 --- a/apps/catalog/domain/service.software_updater.go +++ /dev/null @@ -1,36 +0,0 @@ -package domain - -import ( - "fmt" -) - -type SoftwareUpdaterServiceInterface interface { - Update(platform, name, version string) error -} - -type SoftwareUpdaterService struct { - tic80Updater SoftwareUpdaterTIC80ServiceInterface - ebitengineUpdater SoftwareUpdaterEbitengineServiceInterface - loveUpdater SoftwareUpdaterLoveServiceInterface -} - -func NewSoftwareUpdaterService(tic80_updater SoftwareUpdaterTIC80ServiceInterface, ebitengine_updater SoftwareUpdaterEbitengineServiceInterface, love_updater SoftwareUpdaterLoveServiceInterface) *SoftwareUpdaterService { - return &SoftwareUpdaterService{ - tic80Updater: tic80_updater, - ebitengineUpdater: ebitengine_updater, - loveUpdater: love_updater, - } -} - -func (s *SoftwareUpdaterService) Update(platform, name, version string) error { - if platform == "tic80" { - return s.tic80Updater.Update(name, version) - } - if platform == "ebitengine" { - return s.ebitengineUpdater.Update(name, version) - } - if platform == "love" { - return s.loveUpdater.Update(name, version) - } - return fmt.Errorf("unsupported platform: %s", platform) -} diff --git a/apps/catalog/domain/service.software_updater_ebitengine.go b/apps/catalog/domain/service.software_updater_ebitengine.go deleted file mode 100644 index e4b7f51..0000000 --- a/apps/catalog/domain/service.software_updater_ebitengine.go +++ /dev/null @@ -1,134 +0,0 @@ -package domain - -import ( - "encoding/json" - "fmt" - "os" -) - -type SoftwareUpdaterEbitengineMetadata struct { - Name string `json:"name"` - Title string `json:"title"` - Author string `json:"author"` - Desc string `json:"desc"` - Site string `json:"site"` - License string `json:"license"` -} - -type SoftwareUpdaterEbitengineServiceInterface interface { - Update(name, version string) error -} - -type SoftwareUpdaterEbitengineService struct { - softwareRepository SoftwareRepositoryInterface - releaseRepository ReleaseRepositoryInterface - fileRepository FileRepositoryInterface - externalLinkRepository ExternalLinkRepositoryInterface -} - -func NewSoftwareUpdaterEbitengineService( - software_repository SoftwareRepositoryInterface, - release_repository ReleaseRepositoryInterface, - file_repository FileRepositoryInterface, - external_link_repository ExternalLinkRepositoryInterface, -) *SoftwareUpdaterEbitengineService { - return &SoftwareUpdaterEbitengineService{ - softwareRepository: software_repository, - releaseRepository: release_repository, - fileRepository: file_repository, - externalLinkRepository: external_link_repository, - } -} - -func (s *SoftwareUpdaterEbitengineService) Update(name, version string) error { - fmt.Printf("Ebitengine Updater: Starting update for name: %s, version: %s\n", name, version) - - versioned_name := name + "-" + version - - zip_filename := versioned_name + ".html.zip" - metadata_filename := versioned_name + ".metadata.json" - html_dirname := versioned_name - - fmt.Printf("Ebitengine Updater: Processing zip file: %s\n", zip_filename) - fmt.Printf("Ebitengine Updater: Processing metadata file: %s\n", metadata_filename) - fmt.Printf("Ebitengine Updater: Processing HTML directory: %s\n", html_dirname) - - if s.fileRepository.FileExists(html_dirname) { - fmt.Printf("Ebitengine Updater: Removing existing HTML directory: %s\n", html_dirname) - s.fileRepository.DeleteDir(html_dirname) - } - - fmt.Printf("Ebitengine Updater: Creating HTML directory: %s\n", html_dirname) - s.fileRepository.CreateDir(html_dirname) - fmt.Printf("Ebitengine Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname) - s.fileRepository.UnzipFile(zip_filename, html_dirname) - - html_dir_path := s.fileRepository.GetPath(html_dirname) - metadata_path := s.fileRepository.GetPath(metadata_filename) - - var err error - - fmt.Printf("Ebitengine Updater: Extracting metadata from metadata file: %s\n", metadata_path) - meta_data, err := s.GetMetadata(metadata_path) - if err != nil { - fmt.Printf("Ebitengine Updater: Error extracting metadata: %s\n", err.Error()) - return err - } - fmt.Printf("Ebitengine Updater: Extracted metadata: %+v\n", meta_data) - siteURL := meta_data.Site - software := s.BuildSoftware(meta_data) - - err = s.softwareRepository.UpdateOrCreate(software) - if err != nil { - fmt.Printf("Ebitengine Updater: Error updating or creating software: %s\n", err.Error()) - return err - } - - if siteURL != "" { - if linkErr := s.externalLinkRepository.UpsertByLabel(software.ID, "Source Code", siteURL); linkErr != nil { - fmt.Printf("Ebitengine Updater: Error upserting external link: %s\n", linkErr.Error()) - } - } - - release := Release{ - SoftwareID: software.ID, - Version: version, - CartridgePath: "", - SourcePath: "", - HTMLFolderPath: html_dir_path, - } - - fmt.Printf("Ebitengine Updater: Creating release for software ID: %d, version: %s\n", software.ID, version) - s.releaseRepository.CreateIfNotExist(&release) - - fmt.Printf("Ebitengine Updater: Successfully processed Lua cartridge: %s\n", name) - return nil -} - -func (s *SoftwareUpdaterEbitengineService) BuildSoftware(metadata SoftwareUpdaterEbitengineMetadata) *Software { - software := &Software{ - Name: metadata.Name, - Title: metadata.Title, - Author: metadata.Author, - Desc: metadata.Desc, - License: metadata.License, - Platform: "ebitengine", - } - - return software -} - -func (s *SoftwareUpdaterEbitengineService) GetMetadata(metadata_path string) (SoftwareUpdaterEbitengineMetadata, error) { - var metadata SoftwareUpdaterEbitengineMetadata - - data, err := os.ReadFile(metadata_path) - if err != nil { - return metadata, err - } - - if err := json.Unmarshal(data, &metadata); err != nil { - return metadata, err - } - - return metadata, nil -} diff --git a/apps/catalog/domain/service.software_updater_love.go b/apps/catalog/domain/service.software_updater_love.go deleted file mode 100644 index 2bad0f9..0000000 --- a/apps/catalog/domain/service.software_updater_love.go +++ /dev/null @@ -1,134 +0,0 @@ -package domain - -import ( - "encoding/json" - "fmt" - "os" -) - -type SoftwareUpdaterLoveMetadata struct { - Name string `json:"name"` - Title string `json:"title"` - Author string `json:"author"` - Desc string `json:"desc"` - Site string `json:"site"` - License string `json:"license"` -} - -type SoftwareUpdaterLoveServiceInterface interface { - Update(name, version string) error -} - -type SoftwareUpdaterLoveService struct { - softwareRepository SoftwareRepositoryInterface - releaseRepository ReleaseRepositoryInterface - fileRepository FileRepositoryInterface - externalLinkRepository ExternalLinkRepositoryInterface -} - -func NewSoftwareUpdaterLoveService( - software_repository SoftwareRepositoryInterface, - release_repository ReleaseRepositoryInterface, - file_repository FileRepositoryInterface, - external_link_repository ExternalLinkRepositoryInterface, -) *SoftwareUpdaterLoveService { - return &SoftwareUpdaterLoveService{ - softwareRepository: software_repository, - releaseRepository: release_repository, - fileRepository: file_repository, - externalLinkRepository: external_link_repository, - } -} - -func (s *SoftwareUpdaterLoveService) Update(name, version string) error { - fmt.Printf("Love Updater: Starting update for name: %s, version: %s\n", name, version) - - versioned_name := name + "-" + version - - zip_filename := versioned_name + ".html.zip" - metadata_filename := versioned_name + ".metadata.json" - html_dirname := versioned_name - - fmt.Printf("Love Updater: Processing zip file: %s\n", zip_filename) - fmt.Printf("Love Updater: Processing metadata file: %s\n", metadata_filename) - fmt.Printf("Love Updater: Processing HTML directory: %s\n", html_dirname) - - if s.fileRepository.FileExists(html_dirname) { - fmt.Printf("Love Updater: Removing existing HTML directory: %s\n", html_dirname) - s.fileRepository.DeleteDir(html_dirname) - } - - fmt.Printf("Love Updater: Creating HTML directory: %s\n", html_dirname) - s.fileRepository.CreateDir(html_dirname) - fmt.Printf("Love Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname) - s.fileRepository.UnzipFile(zip_filename, html_dirname) - - html_dir_path := s.fileRepository.GetPath(html_dirname) - metadata_path := s.fileRepository.GetPath(metadata_filename) - - var err error - - fmt.Printf("Love Updater: Extracting metadata from metadata file: %s\n", metadata_path) - meta_data, err := s.GetMetadata(metadata_path) - if err != nil { - fmt.Printf("Love Updater: Error extracting metadata: %s\n", err.Error()) - return err - } - fmt.Printf("Love Updater: Extracted metadata: %+v\n", meta_data) - siteURL := meta_data.Site - software := s.BuildSoftware(meta_data) - - err = s.softwareRepository.UpdateOrCreate(software) - if err != nil { - fmt.Printf("Love Updater: Error updating or creating software: %s\n", err.Error()) - return err - } - - if siteURL != "" { - if linkErr := s.externalLinkRepository.UpsertByLabel(software.ID, "Source Code", siteURL); linkErr != nil { - fmt.Printf("Love Updater: Error upserting external link: %s\n", linkErr.Error()) - } - } - - release := Release{ - SoftwareID: software.ID, - Version: version, - CartridgePath: "", - SourcePath: "", - HTMLFolderPath: html_dir_path, - } - - fmt.Printf("Love Updater: Creating release for software ID: %d, version: %s\n", software.ID, version) - s.releaseRepository.CreateIfNotExist(&release) - - fmt.Printf("Love Updater: Successfully processed Lua cartridge: %s\n", name) - return nil -} - -func (s *SoftwareUpdaterLoveService) BuildSoftware(metadata SoftwareUpdaterLoveMetadata) *Software { - software := &Software{ - Name: metadata.Name, - Title: metadata.Title, - Author: metadata.Author, - Desc: metadata.Desc, - License: metadata.License, - Platform: "love", - } - - return software -} - -func (s *SoftwareUpdaterLoveService) GetMetadata(metadata_path string) (SoftwareUpdaterLoveMetadata, error) { - var metadata SoftwareUpdaterLoveMetadata - - data, err := os.ReadFile(metadata_path) - if err != nil { - return metadata, err - } - - if err := json.Unmarshal(data, &metadata); err != nil { - return metadata, err - } - - return metadata, nil -} diff --git a/apps/catalog/domain/service.software_updater_tic80.go b/apps/catalog/domain/service.software_updater_tic80.go deleted file mode 100644 index 3e9d114..0000000 --- a/apps/catalog/domain/service.software_updater_tic80.go +++ /dev/null @@ -1,159 +0,0 @@ -package domain - -import ( - "bufio" - "fmt" - "os" - "strings" -) - -type SoftwareUpdaterTIC80ServiceInterface interface { - Update(name, version string) error -} - -type SoftwareUpdaterTIC80Service struct { - softwareRepository SoftwareRepositoryInterface - releaseRepository ReleaseRepositoryInterface - fileRepository FileRepositoryInterface - externalLinkRepository ExternalLinkRepositoryInterface -} - -func NewSoftwareUpdaterTIC80Service( - software_repository SoftwareRepositoryInterface, - release_repository ReleaseRepositoryInterface, - file_repository FileRepositoryInterface, - external_link_repository ExternalLinkRepositoryInterface, -) *SoftwareUpdaterTIC80Service { - return &SoftwareUpdaterTIC80Service{ - softwareRepository: software_repository, - releaseRepository: release_repository, - fileRepository: file_repository, - externalLinkRepository: external_link_repository, - } -} - -func (s *SoftwareUpdaterTIC80Service) Update(name, version string) error { - fmt.Printf("TIC80 Updater: Starting update for name: %s, version: %s\n", name, version) - - versioned_name := name + "-" + version - - zip_filename := versioned_name + ".html.zip" - docs_zip_filename := versioned_name + "-docs.zip" - cartridge_filename := versioned_name + ".tic" - source_filename := versioned_name + ".lua" - html_dirname := versioned_name - docs_dirname := versioned_name + "-docs" - - fmt.Printf("TIC80 Updater: Processing zip file: %s\n", zip_filename) - fmt.Printf("TIC80 Updater: Processing docs zip file: %s\n", docs_zip_filename) - fmt.Printf("TIC80 Updater: Processing cartridge file: %s\n", cartridge_filename) - fmt.Printf("TIC80 Updater: Processing source file: %s\n", source_filename) - fmt.Printf("TIC80 Updater: Processing HTML directory: %s\n", html_dirname) - fmt.Printf("TIC80 Updater: Processing docs directory: %s\n", docs_dirname) - - if s.fileRepository.FileExists(html_dirname) { - fmt.Printf("TIC80 Updater: Removing existing HTML directory: %s\n", html_dirname) - s.fileRepository.DeleteDir(html_dirname) - } - - if s.fileRepository.FileExists(docs_dirname) { - fmt.Printf("TIC80 Updater: Removing existing docs directory: %s\n", docs_dirname) - s.fileRepository.DeleteDir(docs_dirname) - } - - s.fileRepository.CreateDir(html_dirname) - fmt.Printf("TIC80 Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname) - s.fileRepository.UnzipFile(zip_filename, html_dirname) - - s.fileRepository.CreateDir(docs_dirname) - fmt.Printf("TIC80 Updater: Unzipping docs file: %s to directory: %s\n", docs_zip_filename, docs_dirname) - s.fileRepository.UnzipFile(docs_zip_filename, docs_dirname) - - cartridge_path := s.fileRepository.GetPath((cartridge_filename)) - source_path := s.fileRepository.GetPath(source_filename) - html_dir_path := s.fileRepository.GetPath(html_dirname) - docs_dir_path := s.fileRepository.GetPath(docs_dirname) - - var err error - fmt.Printf("TIC80 Updater: Extracting metadata from source file: %s\n", source_path) - meta_data, err := s.GetMetadata(source_path) - if err != nil { - fmt.Printf("TIC80 Updater: Error extracting metadata: %s\n", err.Error()) - return err - } - - siteURL := meta_data["site"] - software := s.BuildSoftware(meta_data) - err = s.softwareRepository.UpdateOrCreate(software) - if err != nil { - fmt.Printf("TIC80 Updater: Error updating or creating software: %s\n", err.Error()) - return err - } - - if siteURL != "" { - if linkErr := s.externalLinkRepository.UpsertByLabel(software.ID, "Source Code", siteURL); linkErr != nil { - fmt.Printf("TIC80 Updater: Error upserting external link: %s\n", linkErr.Error()) - } - } - - fmt.Printf("TIC80 Updater: Creating release for software ID: %d, version: %s\n", software.ID, version) - release := Release{ - SoftwareID: software.ID, - Version: version, - CartridgePath: cartridge_path, - SourcePath: source_path, - HTMLFolderPath: html_dir_path, - DocsFolderPath: docs_dir_path, - } - - s.releaseRepository.CreateIfNotExist(&release) - - fmt.Printf("TIC80 Updater: Successfully processed Lua cartridge: %s\n", name) - return nil -} - -func (s *SoftwareUpdaterTIC80Service) BuildSoftware(metadata map[string]string) *Software { - software := &Software{ - Name: metadata["name"], - Title: metadata["title"], - Author: metadata["author"], - Desc: metadata["desc"], - License: metadata["license"], - Platform: "tic80", - } - - return software -} - -func (s *SoftwareUpdaterTIC80Service) GetMetadata(source_path string) (map[string]string, error) { - - file, err := os.Open(source_path) - if err != nil { - return nil, err - } - defer file.Close() - - meta_data := make(map[string]string) - scanner := bufio.NewScanner(file) - - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "--") { - parts := strings.SplitN(strings.TrimPrefix(line, "--"), ":", 2) - if len(parts) != 2 { - continue - } - key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - meta_data[strings.ToLower(key)] = val - } else { - break - } - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return meta_data, nil -} diff --git a/apps/catalog/entrypoint.sh b/apps/catalog/entrypoint.sh deleted file mode 100755 index 3447204..0000000 --- a/apps/catalog/entrypoint.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -go mod download && air -c .air.toml diff --git a/apps/catalog/go.mod b/apps/catalog/go.mod deleted file mode 100644 index 28e2c4e..0000000 --- a/apps/catalog/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module teletype_softwares - -go 1.21.5 - -require ( - github.com/DATA-DOG/go-sqlmock v1.5.1 - github.com/go-chi/chi/v5 v5.0.10 - github.com/google/uuid v1.4.0 - gorm.io/driver/mysql v1.5.2 - gorm.io/gorm v1.25.5 -) - -require ( - github.com/go-sql-driver/mysql v1.7.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect -) diff --git a/apps/catalog/go.sum b/apps/catalog/go.sum deleted file mode 100644 index 479e353..0000000 --- a/apps/catalog/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -github.com/DATA-DOG/go-sqlmock v1.5.1 h1:FK6RCIUSfmbnI/imIICmboyQBkOckutaa6R5YYlLZyo= -github.com/DATA-DOG/go-sqlmock v1.5.1/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= -gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= -gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/apps/catalog/http/controller.api.software.go b/apps/catalog/http/controller.api.software.go deleted file mode 100644 index 943a0e2..0000000 --- a/apps/catalog/http/controller.api.software.go +++ /dev/null @@ -1,48 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - "strings" - - "teletype_softwares/domain" -) - -type APISoftwareController struct { - softwareService domain.SoftwareServiceInterface -} - -func NewAPISoftwareController(software_service domain.SoftwareServiceInterface) *APISoftwareController { - return &APISoftwareController{softwareService: software_service} -} - -func replaceReleasePaths(release *domain.Release) { - release.CartridgePath = strings.ReplaceAll(release.CartridgePath, "/softwares/", "/file/") - release.SourcePath = strings.ReplaceAll(release.SourcePath, "/softwares/", "/file/") - release.HTMLFolderPath = strings.ReplaceAll(release.HTMLFolderPath, "/softwares/", "/file/") - release.DocsFolderPath = strings.ReplaceAll(release.DocsFolderPath, "/softwares/", "/file/") -} - -func (c *APISoftwareController) Index(w http.ResponseWriter, r *http.Request) { - - softwares, _ := c.softwareService.DetailedList() - - for idx := range softwares.Softwares { - softwareShowData := &softwares.Softwares[idx] - - for i := range softwareShowData.Releases { - replaceReleasePaths(&softwareShowData.Releases[i]) - } - - // LatestRelease és WebPlayableRelease a Releases slice elemeire mutat, - // ezért ezeket külön is frissíteni kell, mivel értékmásolat kerülhet bele - if softwareShowData.LatestRelease != nil { - replaceReleasePaths(softwareShowData.LatestRelease) - } - if softwareShowData.WebPlayableRelease != nil { - replaceReleasePaths(softwareShowData.WebPlayableRelease) - } - } - - json.NewEncoder(w).Encode(softwares) -} diff --git a/apps/catalog/http/controller.api.software_highlighted.go b/apps/catalog/http/controller.api.software_highlighted.go deleted file mode 100644 index bc6184b..0000000 --- a/apps/catalog/http/controller.api.software_highlighted.go +++ /dev/null @@ -1,45 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "teletype_softwares/domain" -) - -type APISoftwareHighlightedController struct { - softwareService domain.SoftwareServiceInterface -} - -func NewAPISoftwareHighlightedController(software_service domain.SoftwareServiceInterface) *APISoftwareHighlightedController { - return &APISoftwareHighlightedController{softwareService: software_service} -} - -func (c *APISoftwareHighlightedController) Index(w http.ResponseWriter, r *http.Request) { - softwareShowData, err := c.softwareService.GetHighlighted() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if softwareShowData == nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(map[string]string{"error": "no highlighted software found"}) - return - } - - for i := range softwareShowData.Releases { - replaceReleasePaths(&softwareShowData.Releases[i]) - } - - if softwareShowData.LatestRelease != nil { - replaceReleasePaths(softwareShowData.LatestRelease) - } - if softwareShowData.WebPlayableRelease != nil { - replaceReleasePaths(softwareShowData.WebPlayableRelease) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(softwareShowData) -} diff --git a/apps/catalog/http/controller.software_updater.go b/apps/catalog/http/controller.software_updater.go deleted file mode 100644 index 35aff1a..0000000 --- a/apps/catalog/http/controller.software_updater.go +++ /dev/null @@ -1,46 +0,0 @@ -package http - -import ( - "net/http" - "os" - - "teletype_softwares/domain" -) - -type SoftwareUpdaterController struct { - softwareUpdaterService domain.SoftwareUpdaterServiceInterface - softwareService domain.SoftwareServiceInterface -} - -func NewSoftwareUpdaterController( - softwareUpdaterService domain.SoftwareUpdaterServiceInterface, - softwareService domain.SoftwareServiceInterface, -) *SoftwareUpdaterController { - return &SoftwareUpdaterController{ - softwareUpdaterService: softwareUpdaterService, - softwareService: softwareService, - } -} - -func (c *SoftwareUpdaterController) Update(w http.ResponseWriter, r *http.Request) { - secret := r.URL.Query().Get("secret") - if secret != os.Getenv("UPDATE_SECRET") { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - platform := r.URL.Query().Get("platform") - name := r.URL.Query().Get("name") - version := r.URL.Query().Get("version") - - if version == "" { - http.Error(w, "Version not provided", http.StatusBadRequest) - return - } - - if err := c.softwareUpdaterService.Update(platform, name, version); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write([]byte("Updated")) -} diff --git a/apps/catalog/http/http.go b/apps/catalog/http/http.go deleted file mode 100755 index c643f50..0000000 --- a/apps/catalog/http/http.go +++ /dev/null @@ -1,18 +0,0 @@ -package http - -import ( - "teletype_softwares/domain" - "teletype_softwares/lib/http_utils" -) - -func StartHttpServer(domain_instance domain.Domain) { - router := NewRouter( - NewAPISoftwareController(domain_instance.SoftwareService), - NewAPISoftwareHighlightedController(domain_instance.SoftwareService), - NewSoftwareUpdaterController(domain_instance.SoftwareUpdaterService, domain_instance.SoftwareService), - ).Init() - - http_utils.StartGenericHTTPServer(http_utils.StartGenericHTTPServerContext{ - Router: router, - }) -} diff --git a/apps/catalog/http/middleware.cors.go b/apps/catalog/http/middleware.cors.go deleted file mode 100644 index ceef313..0000000 --- a/apps/catalog/http/middleware.cors.go +++ /dev/null @@ -1,12 +0,0 @@ -package http - -import "net/http" - -func CORSMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") - w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") - w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") - next.ServeHTTP(w, r) - }) -} diff --git a/apps/catalog/http/router.go b/apps/catalog/http/router.go deleted file mode 100755 index fbf4e78..0000000 --- a/apps/catalog/http/router.go +++ /dev/null @@ -1,39 +0,0 @@ -package http - -import ( - "net/http" - - "github.com/go-chi/chi/v5" -) - -type Router struct { - apiSoftwareController *APISoftwareController - apiSoftwareHighlightedController *APISoftwareHighlightedController - softwareUpdaterController *SoftwareUpdaterController -} - -func NewRouter( - api_software_controller *APISoftwareController, - api_software_highlighted_controller *APISoftwareHighlightedController, - software_updater_controller *SoftwareUpdaterController, -) *Router { - return &Router{ - apiSoftwareController: api_software_controller, - apiSoftwareHighlightedController: api_software_highlighted_controller, - softwareUpdaterController: software_updater_controller, - } -} - -func (r *Router) Init() *chi.Mux { - router := chi.NewRouter() - router.Use(CORSMiddleware) - - router.Get("/api/software", r.apiSoftwareController.Index) - router.Get("/api/software/highlighted", r.apiSoftwareHighlightedController.Index) - router.Get("/update", r.softwareUpdaterController.Update) - - fs_file := http.FileServer(http.Dir("/softwares")) - router.Handle("/file/*", http.StripPrefix("/file/", fs_file)) - - return router -} diff --git a/apps/catalog/lib/http_utils/http_utils.go b/apps/catalog/lib/http_utils/http_utils.go deleted file mode 100755 index cdac680..0000000 --- a/apps/catalog/lib/http_utils/http_utils.go +++ /dev/null @@ -1,38 +0,0 @@ -package http_utils - -import ( - "encoding/json" - "log" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" -) - -func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) { - response, _ := json.Marshal(payload) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - w.Write(response) -} - -func GetParam(r *http.Request, name string) string { - return chi.URLParam(r, name) -} - -func GetParamUUID(r *http.Request, name string) (uuid.UUID, error) { - return uuid.Parse(GetParam(r, name)) -} - -type StartGenericHTTPServerContext struct { - Router *chi.Mux -} - -func StartGenericHTTPServer(ctx StartGenericHTTPServerContext) { - r := chi.NewRouter() - r.Use(LoggingMiddleware) - - r.Mount("/", ctx.Router) - http.Handle("/", r) - log.Fatal(http.ListenAndServe(":80", r)) -} diff --git a/apps/catalog/lib/http_utils/middleware.logger.go b/apps/catalog/lib/http_utils/middleware.logger.go deleted file mode 100644 index 5aa7b61..0000000 --- a/apps/catalog/lib/http_utils/middleware.logger.go +++ /dev/null @@ -1,13 +0,0 @@ -package http_utils - -import ( - "fmt" - "net/http" -) - -func LoggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%s %s\n", r.Method, r.RequestURI) - next.ServeHTTP(w, r) - }) -} diff --git a/apps/catalog/lib/mysql_utils/db_mock.go b/apps/catalog/lib/mysql_utils/db_mock.go deleted file mode 100755 index b83b78f..0000000 --- a/apps/catalog/lib/mysql_utils/db_mock.go +++ /dev/null @@ -1,46 +0,0 @@ -package mysql_utils - -import ( - "database/sql" - - "github.com/DATA-DOG/go-sqlmock" - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -type DBMockSuite struct { - DB *gorm.DB - Mock sqlmock.Sqlmock -} - -func NewDBMockSuite() *DBMockSuite { - var ( - db *sql.DB - gormdb *gorm.DB - mock sqlmock.Sqlmock - err error - ) - - db, mock, err = sqlmock.New() - - if err != nil { - panic("SQL Mock error") - } - - mock.ExpectQuery("SELECT VERSION()").WithArgs().WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) - - gormdb, err = gorm.Open(mysql.New(mysql.Config{ - DSN: "sqlmock_db_0", - DriverName: "mysql", - Conn: db, - }), &gorm.Config{}) - - if err != nil { - panic("GORM open error") - } - - return &DBMockSuite{ - DB: gormdb, - Mock: mock, - } -} diff --git a/apps/catalog/lib/mysql_utils/mysql.go b/apps/catalog/lib/mysql_utils/mysql.go deleted file mode 100755 index bbacab9..0000000 --- a/apps/catalog/lib/mysql_utils/mysql.go +++ /dev/null @@ -1,38 +0,0 @@ -package mysql_utils - -import ( - "fmt" - "os" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -type DbConnectData struct { - User string - Password string - Host string - Port string - Database string -} - -func getDbConnectData() DbConnectData { - return DbConnectData{ - User: os.Getenv("DB_USER"), - Password: os.Getenv("DB_PASSWORD"), - Host: os.Getenv("DB_HOST"), - Port: os.Getenv("DB_PORT"), - Database: os.Getenv("DB_NAME"), - } - -} - -func Init() *gorm.DB { - c := getDbConnectData() - dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.User, c.Password, c.Host, c.Port, c.Database) - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - panic(err) - } - return db -} diff --git a/apps/catalog/lib/template_utils/cache.go b/apps/catalog/lib/template_utils/cache.go deleted file mode 100644 index d06d890..0000000 --- a/apps/catalog/lib/template_utils/cache.go +++ /dev/null @@ -1,45 +0,0 @@ -package template_utils - -import ( - "html/template" - "sync" -) - -type TemplateCache struct { - cache map[string]*template.Template - mu sync.RWMutex -} - -var globalCache *TemplateCache - -func init() { - globalCache = &TemplateCache{ - cache: make(map[string]*template.Template), - } -} - -func GetTemplate(name string, files ...string) (*template.Template, error) { - globalCache.mu.RLock() - if tmpl, exists := globalCache.cache[name]; exists { - globalCache.mu.RUnlock() - return tmpl, nil - } - globalCache.mu.RUnlock() - - tmpl, err := template.ParseFiles(files...) - if err != nil { - return nil, err - } - - globalCache.mu.Lock() - globalCache.cache[name] = tmpl - globalCache.mu.Unlock() - - return tmpl, nil -} - -func ClearCache() { - globalCache.mu.Lock() - globalCache.cache = make(map[string]*template.Template) - globalCache.mu.Unlock() -} diff --git a/apps/catalog/main.go b/apps/catalog/main.go deleted file mode 100755 index 58874ef..0000000 --- a/apps/catalog/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "teletype_softwares/domain" - "teletype_softwares/http" -) - -func main() { - domain := domain.NewDomain() - http.StartHttpServer(domain) -} diff --git a/docker-compose.yaml b/docker-compose.yaml index 556080c..2c667c8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -138,12 +138,13 @@ services: - proxy - interstack - catalog: + api: build: - context: ./apps/catalog + context: ./apps/api dockerfile: Dockerfile.development - container_name: catalog + container_name: api environment: + - RAILS_ENV=development - DB_USER=root - DB_PASSWORD=${DB_PASSWORD} - DB_HOST=mysql @@ -151,18 +152,22 @@ services: - DB_NAME=softwares - UPDATE_SECRET=${UPDATE_SECRET} - FILE_CONTAINER_PATH=/softwares + - SECRET_KEY_BASE=${SECRET_KEY_BASE:-changeme_in_production_use_long_random_string} + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@teletype.hu} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:-password123} depends_on: mysql: condition: service_healthy volumes: - ./data/softwares:/softwares - - ./apps/catalog:/app + - ./apps/api:/app + - api_bundle:/usr/local/bundle labels: - "traefik.enable=true" - - "traefik.http.routers.catalog.rule=Host(`${WEBAPP_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/file`) || PathPrefix(`/update`))" - - "traefik.http.routers.catalog.entrypoints=web" - - "traefik.http.routers.catalog.priority=10" - - "traefik.http.services.catalog.loadbalancer.server.port=80" + - "traefik.http.routers.api.rule=Host(`${WEBAPP_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/file`) || PathPrefix(`/update`) || PathPrefix(`/admin`))" + - "traefik.http.routers.api.entrypoints=web" + - "traefik.http.routers.api.priority=10" + - "traefik.http.services.api.loadbalancer.server.port=3000" - "traefik.docker.network=proxy" networks: - proxy @@ -192,6 +197,7 @@ volumes: woodpecker: mysql-data: softwares: + api_bundle: networks: proxy: diff --git a/env-example b/env-example index 047aa32..d7167c3 100644 --- a/env-example +++ b/env-example @@ -25,4 +25,9 @@ DB_PASSWORD= UPDATE_SECRET= DROP_PASSWORD= WEBAPP_WIKIJS_TOKEN= -DISCORD_INVITE_LINK= \ No newline at end of file +DISCORD_INVITE_LINK= + +# Rails API (apps/api) +ADMIN_EMAIL= +ADMIN_PASSWORD= +SECRET_KEY_BASE= \ No newline at end of file