diff --git a/app/controllers/overlays_controller.rb b/app/controllers/overlays_controller.rb index f5ffb91..d31b5fc 100644 --- a/app/controllers/overlays_controller.rb +++ b/app/controllers/overlays_controller.rb @@ -1,46 +1,77 @@ class OverlaysController < ApplicationController before_action :set_overlay, only: [ :destroy ] - before_action :set_server_template, only: [ :create ] + before_action :set_server_template, only: [ :create, :destroy ] def index @system_overlays = Overlay.system_overlays @custom_overlays = current_user.overlays.custom_overlays.order(:name) end + def new + @overlay = current_user.overlays.build + end + def create - @server_template = current_user.server_templates.find(params[:server_template_id]) - @overlay = Overlay.find(params[:overlay_id]) + if @server_template.present? + # Attach existing overlay to a template + @overlay = Overlay.find(params[:overlay_id]) - position = @server_template.template_overlays.maximum(:position).to_i + 1 - @template_overlay = @server_template.template_overlays.build(overlay_id: @overlay.id, position: position) + position = @server_template.template_overlays.maximum(:position).to_i + 1 + @template_overlay = @server_template.template_overlays.build(overlay_id: @overlay.id, position: position) - if @template_overlay.save - Activity.log(current_user, "added_overlay", "ServerTemplate", @server_template.id, { overlay: @overlay.name }) - redirect_to @server_template, notice: "Overlay added successfully!" + if @template_overlay.save + Activity.log(current_user, "added_overlay", "ServerTemplate", @server_template.id, { overlay: @overlay.name }) + redirect_to @server_template, notice: "Overlay added successfully!" + else + redirect_to @server_template, alert: "Failed to add overlay" + end else - redirect_to @server_template, alert: "Failed to add overlay" + # Create a new custom overlay for the current user + @overlay = current_user.overlays.build(overlay_params.merge(overlay_type: "custom")) + + if @overlay.save + Activity.log(current_user, "created_overlay", "Overlay", @overlay.id, { name: @overlay.name, slug: @overlay.slug }) + redirect_to overlays_path, notice: "Overlay created" + else + render :new, status: :unprocessable_entity + end end end def destroy - authorize_user! - server_template = @overlay.server_templates.find(params[:server_template_id]) - @overlay.template_overlays.where(server_template_id: server_template.id).destroy_all - Activity.log(current_user, "removed_overlay", "ServerTemplate", server_template.id, { overlay: @overlay.name }) - redirect_to server_template, notice: "Overlay removed successfully!" + if @server_template.present? + authorize_user_for_template_overlay! + @overlay.template_overlays.where(server_template_id: @server_template.id).destroy_all + Activity.log(current_user, "removed_overlay", "ServerTemplate", @server_template.id, { overlay: @overlay.name }) + redirect_to @server_template, notice: "Overlay removed successfully!" + else + authorize_owner! + name = @overlay.name + @overlay.destroy + redirect_to overlays_path, notice: "Overlay '#{name}' deleted" + end end private def set_overlay - @overlay = Overlay.find(params[:id]) + @overlay = Overlay.find(params[:id]) if params[:id].present? end def set_server_template + return unless params[:server_template_id].present? @server_template = current_user.server_templates.find(params[:server_template_id]) end - def authorize_user! + def overlay_params + params.require(:overlay).permit(:name, :slug) + end + + def authorize_user_for_template_overlay! redirect_to dashboard_path, alert: "Not authorized" unless @overlay.user_id.nil? || @overlay.user_id == current_user.id end + + def authorize_owner! + redirect_to overlays_path, alert: "Not authorized" unless @overlay.user_id == current_user.id + end end diff --git a/app/models/config_option.rb b/app/models/config_option.rb index bebda14..16ccfbd 100644 --- a/app/models/config_option.rb +++ b/app/models/config_option.rb @@ -1,6 +1,6 @@ class ConfigOption < ApplicationRecord belongs_to :server_template - validates :config_key, :config_value, presence: true + validates :config_key, presence: true validates :config_key, uniqueness: { scope: :server_template_id } end diff --git a/app/models/overlay.rb b/app/models/overlay.rb index de6542e..bb299d4 100644 --- a/app/models/overlay.rb +++ b/app/models/overlay.rb @@ -3,11 +3,37 @@ class Overlay < ApplicationRecord has_many :template_overlays, dependent: :destroy has_many :server_templates, through: :template_overlays - validates :name, :overlay_type, :path, presence: true + validates :name, :overlay_type, :slug, presence: true validates :overlay_type, inclusion: { in: %w[system custom] } validates :name, uniqueness: { scope: :user_id, message: "must be unique per user" } + validate :slug_is_single_directory + + before_validation :normalize_slug scope :system_overlays, -> { where(overlay_type: "system").where(user_id: nil) } scope :custom_overlays, -> { where(overlay_type: "custom") } scope :for_user, ->(user) { where("user_id IS NULL OR user_id = ?", user.id) } + + private + + def normalize_slug + return if slug.blank? && name.blank? + + # Use provided slug or derive from name + source = slug.presence || name + self.slug = source.to_s.strip + .downcase + .gsub(%r{[^\w\-.]}, "_") # Replace non-word chars (except - and .) with underscore + .gsub(%r{_+}, "_") # Collapse multiple underscores + .sub(%r{^_+}, "") # Strip leading underscores + .sub(%r{_+$}, "") # Strip trailing underscores + end + + def slug_is_single_directory + return if slug.blank? + + if slug.match?(%r{[\\/]}) + errors.add(:slug, "must be a single directory name (no slashes)") + end + end end diff --git a/app/models/startup_param.rb b/app/models/startup_param.rb index a30e065..bf223bd 100644 --- a/app/models/startup_param.rb +++ b/app/models/startup_param.rb @@ -1,6 +1,6 @@ class StartupParam < ApplicationRecord belongs_to :server_template - validates :param_key, :param_value, presence: true + validates :param_key, presence: true validates :param_key, uniqueness: { scope: :server_template_id } end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..4bbdc0f --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,44 @@ + + + + <%= content_for(:title) || "L4d Tools" %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + + + <% if flash.any? %> +
+ <% flash.each do |type, message| %> +
<%= message %>
+ <% end %> +
+ <% end %> + <%= yield %> + + diff --git a/app/views/overlays/index.html.slim b/app/views/overlays/index.html.slim new file mode 100644 index 0000000..bcbc94b --- /dev/null +++ b/app/views/overlays/index.html.slim @@ -0,0 +1,24 @@ +.overlays + h2 Overlays + = link_to "New Overlay", new_overlay_path, class: "btn btn--primary" + + - all_overlays = (@system_overlays + @custom_overlays).sort_by(&:name) + - if all_overlays.any? + table + thead + tr + th Name + th Dir (slug) + th Type + th Actions + tbody + - all_overlays.each do |overlay| + tr + td = overlay.name + td = overlay.slug + td = overlay.overlay_type.humanize + td + - if overlay.overlay_type == "custom" && overlay.user_id == current_user.id + = link_to "Delete", overlay_path(overlay), method: :delete, data: { confirm: "Delete this overlay?" }, class: "btn btn--small btn--danger" + - else + p No overlays available. diff --git a/app/views/overlays/new.html.slim b/app/views/overlays/new.html.slim new file mode 100644 index 0000000..c62c4e6 --- /dev/null +++ b/app/views/overlays/new.html.slim @@ -0,0 +1,22 @@ +.overlay_form + h2 New Overlay + + = form_with model: @overlay, url: overlays_path, local: true do |f| + - if @overlay.errors.any? + .alert.alert--error + h4 = pluralize(@overlay.errors.count, "error") + ul + - @overlay.errors.full_messages.each do |msg| + li = msg + + .form-group + = f.label :name + = f.text_field :name, placeholder: "e.g., custom_addons" + + .form-group + = f.label :slug, "Directory slug (optional, auto-derived from name)" + = f.text_field :slug, placeholder: "Leave blank to auto-generate" + + .form-actions + = f.submit "Create Overlay", class: "btn btn--primary" + = link_to "Cancel", overlays_path, class: "btn" diff --git a/app/views/server_templates/show.html.slim b/app/views/server_templates/show.html.slim index e90d247..bde03e3 100644 --- a/app/views/server_templates/show.html.slim +++ b/app/views/server_templates/show.html.slim @@ -18,6 +18,8 @@ - @server_template.template_overlays.ordered.each do |to| li = to.overlay.name + span.small + | (dir: /opt/l4d2/overlays/#{to.overlay.slug}) = link_to "Remove", server_template_overlay_path(@server_template, to.overlay), method: :delete, data: { confirm: "Sure?" }, class: "btn btn--small btn--danger" - else p No overlays selected. diff --git a/config/routes.rb b/config/routes.rb index db7e0a2..e3d41ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,7 +17,7 @@ Rails.application.routes.draw do resources :startup_params, only: [ :create, :destroy ] end - resources :overlays, only: [ :index ] + resources :overlays, only: [ :index, :new, :create, :destroy ] resources :servers do member do diff --git a/db/migrate/20260118180000_rename_overlay_path_to_slug.rb b/db/migrate/20260118180000_rename_overlay_path_to_slug.rb new file mode 100644 index 0000000..661ffc2 --- /dev/null +++ b/db/migrate/20260118180000_rename_overlay_path_to_slug.rb @@ -0,0 +1,5 @@ +class RenameOverlayPathToSlug < ActiveRecord::Migration[8.1] + def change + rename_column :overlays, :path, :slug + end +end diff --git a/db/schema.rb b/db/schema.rb index 8724abb..de913cf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_01_18_162630) do +ActiveRecord::Schema[8.1].define(version: 2026_01_18_180000) do create_table "activities", force: :cascade do |t| t.string "action", null: false t.datetime "created_at", null: false @@ -38,7 +38,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_18_162630) do t.text "description" t.string "name", null: false t.string "overlay_type", default: "system", null: false - t.string "path", null: false + t.string "slug", null: false t.datetime "updated_at", null: false t.integer "user_id" t.index ["user_id", "name"], name: "index_overlays_on_user_id_and_name", unique: true diff --git a/lib/l4d_server/launcher.rb b/lib/l4d_server/launcher.rb index f4cd9d6..d961187 100644 --- a/lib/l4d_server/launcher.rb +++ b/lib/l4d_server/launcher.rb @@ -46,7 +46,7 @@ module L4dServer def mount_overlayfs server_dir = "/opt/l4d2/servers/#{@server.id}" - overlays = @template.template_overlays.ordered.map { |to| "/opt/l4d2/overlays/#{to.overlay.name}" }.join(":") + overlays = @template.template_overlays.ordered.map { |to| "/opt/l4d2/overlays/#{to.overlay.slug}" }.join(":") lower_dirs = "#{overlays}:/opt/l4d2/installation" mount_cmd = "fuse-overlayfs -o lowerdir=#{lower_dirs},upperdir=#{server_dir}/upper,workdir=#{server_dir}/work #{server_dir}/merged"