diff --git a/.gitignore b/.gitignore index 40645b5..dff4968 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ npm-debug.log # Ignore all user-generated content (uploads, QR codes, etc.) /priv/static/user_generated/ +/uploads/ # Ignore customized Docker Compose file. docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 71ad5ff..e5d95db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,6 +88,10 @@ ENV LC_ALL en_US.UTF-8 WORKDIR "/app" RUN chown nobody /app +# Create data directory for uploads +RUN mkdir -p /data/uploads/images && \ + chown -R nobody:root /data/uploads + # set runner ENV ENV MIX_ENV="prod" diff --git a/config/dev.exs b/config/dev.exs index edcc6ce..263019c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,6 +1,6 @@ import Config -# Configure your database +# Configure the database config :components_elixir, ComponentsElixir.Repo, username: "postgres", password: "fCnPB8VQdPkhJAD29hq6sZEY", @@ -10,8 +10,12 @@ config :components_elixir, ComponentsElixir.Repo, show_sensitive_data_on_connection_error: true, pool_size: 10 -# For development, we disable any cache and enable -# debugging and code reloading. +# For development work, log all queries +# config :components_elixir, ComponentsElixir.Repo, log: false + +# For development, use a local uploads directory +config :components_elixir, + uploads_dir: "./uploads" # # The watchers configuration can be used to run external # watchers to your application. For example, we can use it diff --git a/config/runtime.exs b/config/runtime.exs index 840405b..01d08b4 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,5 +1,9 @@ import Config +# Runtime configuration for uploads directory +config :components_elixir, + uploads_dir: System.get_env("UPLOADS_DIR", "./uploads") + # config/runtime.exs is executed for all environments, including # during releases. It is executed after compilation and before the # system starts, so it is typically used to load production configuration diff --git a/docker-compose.yml.example b/docker-compose.yml.example index e88888e..6fb44b0 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -25,9 +25,12 @@ services: PHX_HOST: "localhost" PHX_SERVER: "true" PORT: "4000" + UPLOADS_DIR: "/data/uploads" depends_on: db: condition: service_healthy + volumes: + - uploaded_files:/data/uploads command: [ "/bin/sh", @@ -37,3 +40,4 @@ services: volumes: postgres_data: + uploaded_files: diff --git a/lib/components_elixir_web/controllers/file_controller.ex b/lib/components_elixir_web/controllers/file_controller.ex new file mode 100644 index 0000000..450bfe5 --- /dev/null +++ b/lib/components_elixir_web/controllers/file_controller.ex @@ -0,0 +1,40 @@ +defmodule ComponentsElixirWeb.FileController do + use ComponentsElixirWeb, :controller + + def show(conn, %{"filename" => filename}) do + # Security: only allow alphanumeric, dashes, underscores, and dots + if String.match?(filename, ~r/^[a-zA-Z0-9_\-\.]+$/) do + uploads_dir = Application.get_env(:components_elixir, :uploads_dir) + file_path = Path.join([uploads_dir, "images", filename]) + + if File.exists?(file_path) do + # Get the file's MIME type + mime_type = get_mime_type(filename) + + conn + |> put_resp_content_type(mime_type) + |> put_resp_header("cache-control", "public, max-age=86400") # Cache for 1 day + |> send_file(200, file_path) + else + conn + |> put_status(:not_found) + |> text("File not found") + end + else + conn + |> put_status(:bad_request) + |> text("Invalid filename") + end + end + + defp get_mime_type(filename) do + case Path.extname(filename) |> String.downcase() do + ".jpg" -> "image/jpeg" + ".jpeg" -> "image/jpeg" + ".png" -> "image/png" + ".gif" -> "image/gif" + ".webp" -> "image/webp" + _ -> "application/octet-stream" + end + end +end diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index d5d4022..2ce8949 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -614,7 +614,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do <%= if @editing_component && @editing_component.image_filename do %>
Current image:
-