Skip to content

Commit

Permalink
pressign uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Ceolin committed Jul 21, 2024
1 parent 729a498 commit a14abe2
Show file tree
Hide file tree
Showing 27 changed files with 184 additions and 181 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ You need to use an S3-compatible storage service to store your files. At Zoonk,
- `AWS_BUCKET`: Your AWS bucket name.
- `AWS_ENDPOINT_URL_S3`: Your AWS endpoint URL.
- `AWS_CDN_URL`: Your AWS CDN URL (optional. If missing, we'll use the S3 endpoint URL).
- `CSP_CONNECT_SRC`: Your S3 domain (i.e. `https://fly.storage.tigris.dev`).

## Sponsors

Expand Down
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ config :zoonk, ZoonkWeb.Endpoint,
# Configure translation
config :zoonk, ZoonkWeb.Gettext, default_locale: "en", locales: ~w(en pt)

# Content security policy
config :zoonk, :csp, connect_src: System.get_env("CSP_CONNECT_SRC")

# Storage config
config :zoonk, :storage,
bucket: System.get_env("AWS_BUCKET"),
Expand Down
34 changes: 22 additions & 12 deletions lib/components/upload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ defmodule ZoonkWeb.Components.Upload do
<div :if={entry}>
<p :if={not entry.done?} class="text-gray-500"><%= gettext("Uploading: %{progress}%", progress: entry.progress) %></p>
<p :if={entry.done? and @uploading?} class="text-gray-500"><%= gettext("Processing file...") %></p>
<p :for={err <- upload_errors(@uploads.file, entry)} class="text-pink-600"><%= error_to_string(err) %></p>
</div>
</div>
Expand All @@ -59,14 +58,15 @@ defmodule ZoonkWeb.Components.Upload do
def mount(socket) do
socket =
socket
|> assign(:uploading?, false)
|> allow_upload(
:file,
accept: ~w(.jpg .jpeg .png .avif .gif .webp),
max_entries: 1,
auto_upload: true,
external: &presign_upload/2,
progress: &handle_progress/3
)
|> assign(:uploaded_file_key, nil)

{:ok, socket}
end
Expand All @@ -92,25 +92,35 @@ defmodule ZoonkWeb.Components.Upload do
:ok
end

# Only upload a file to the cloud after the progress is done.
defp presign_upload(entry, socket) do
config = ExAws.Config.new(:s3)
bucket = StorageAPI.get_bucket()
timestamp = DateTime.to_unix(DateTime.utc_now())
key = "#{timestamp}_#{entry.client_name}"

{:ok, url} =
ExAws.S3.presigned_url(config, :put, bucket, key,
expires_in: 3600,
query_params: [{"Content-Type", entry.client_type}]
)

{:ok, %{uploader: "S3", key: key, url: url}, assign(socket, :uploaded_file_key, key)}
end

defp handle_progress(_key, %{done?: true}, socket) do
[file | _] =
consume_uploaded_entries(socket, :file, fn %{path: path}, entry ->
StorageAPI.upload(path, entry.client_type)
{:ok, Path.basename(path)}
end)
%{uploaded_file_key: key} = socket.assigns

# Optimize the image in the background.
%{key: file} |> ImageOptimizer.new() |> Oban.insert!()
%{key: key} |> ImageOptimizer.new() |> Oban.insert!()

# Notify the parent component that the upload is done.
notify_parent(socket, file)
notify_parent(socket, key)

{:noreply, assign(socket, uploading?: false)}
{:noreply, socket}
end

defp handle_progress(_key, _entry, socket) do
{:noreply, assign(socket, uploading?: true)}
{:noreply, socket}
end

# Since we only allow uploading one file, we only care about the first entry.
Expand Down
5 changes: 3 additions & 2 deletions lib/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule ZoonkWeb.Router do
import ZoonkWeb.Plugs.UserAuth

alias Zoonk.Storage.StorageAPI
alias ZoonkWeb.Plugs.ContentSecurityPolicy
alias ZoonkWeb.Plugs.Course
alias ZoonkWeb.Plugs.School
alias ZoonkWeb.Plugs.Translate
Expand All @@ -23,7 +24,7 @@ defmodule ZoonkWeb.Router do

plug :put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src-elem 'self' https://plausible.io; connect-src 'self' https://plausible.io; img-src 'self' #{StorageAPI.get_domain()} data: blob:; frame-src 'self' www.youtube-nocookie.com;"
"default-src 'self'; script-src-elem 'self' https://plausible.io; connect-src 'self' https://plausible.io #{ContentSecurityPolicy.get_connect_src()}; img-src 'self' #{StorageAPI.get_domain()} data: blob:; frame-src 'self' www.youtube-nocookie.com;"
}

plug :fetch_current_user
Expand All @@ -37,7 +38,7 @@ defmodule ZoonkWeb.Router do
plug :accepts, ["html"]
plug :fetch_session
plug :protect_from_forgery
plug ZoonkWeb.Plugs.CspNonce, nonce: @nonce
plug ContentSecurityPolicy, nonce: @nonce
plug :put_secure_browser_headers, %{"content-security-policy" => "style-src 'self' 'nonce-#{@nonce}'"}
end

Expand Down
25 changes: 25 additions & 0 deletions lib/shared/content_security_policy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule ZoonkWeb.Plugs.ContentSecurityPolicy do
@moduledoc """
Set a CSP nonce for the current request.
"""

@spec init(Keyword.t()) :: Keyword.t()
def init(options), do: options

@spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
def call(conn, opts) do
nonce = Keyword.get(opts, :nonce)
Plug.Conn.assign(conn, :csp_nonce, nonce)
end

@doc """
Get allowed connect-src domains.
## Examples
iex> ZoonkWeb.Plugs.CspNonce.get_connect_src()
"https://fly.storage.tigris.dev"
"""
@spec get_connect_src() :: String.t()
def get_connect_src, do: Application.get_env(:zoonk, :csp)[:connect_src]
end
14 changes: 0 additions & 14 deletions lib/shared/csp_nonce_plug.ex

This file was deleted.

23 changes: 0 additions & 23 deletions lib/storage/storage_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,8 @@ defmodule Zoonk.Storage.StorageAPI do
"""
alias ExAws.S3

@callback upload(String.t(), String.t()) :: {:ok, term()} | {:error, term()}
@callback delete(String.t()) :: {:ok, term()} | {:error, term()}

@doc """
Uploads a file to the storage service.
## Examples
iex> StorageAPI.upload("path/to/file", "image/webp")
{:ok, %{}}
iex> StorageAPI.upload("path/to/file", "image/webp")
{:error, %{}}
"""
@spec upload(String.t(), String.t()) :: {:ok, term()} | {:error, term()}
def upload(file_path, content_type), do: impl().upload(file_path, content_type)

@doc """
Deletes a file from the storage service.
Expand Down Expand Up @@ -88,14 +73,6 @@ defmodule Zoonk.ExternalStorageAPI do
alias ExAws.S3
alias Zoonk.Storage.StorageAPI

@spec upload(String.t(), String.t()) :: {:ok, term()} | {:error, term()}
def upload(file_path, content_type) do
file_path
|> S3.Upload.stream_file()
|> S3.upload(StorageAPI.get_bucket(), Path.basename(file_path), content_type: content_type)
|> ExAws.request()
end

@spec delete(String.t()) :: {:ok, term()} | {:error, term()}
def delete(key) do
StorageAPI.get_bucket()
Expand Down
16 changes: 8 additions & 8 deletions priv/gettext/courses.pot
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
msgid ""
msgstr ""

#: lib/content/course_live/course_view.ex:59
#: lib/content/course_live/course_view.ex:60
#, elixir-autogen, elixir-format
msgid "A request to enroll has been sent to the course teacher."
msgstr ""
Expand All @@ -36,7 +36,7 @@ msgstr ""
msgid "Confirming..."
msgstr ""

#: lib/content/course_live/course_view.ex:58
#: lib/content/course_live/course_view.ex:59
#, elixir-autogen, elixir-format
msgid "Enrolled successfully!"
msgstr ""
Expand All @@ -51,7 +51,7 @@ msgstr ""
msgid "Expert"
msgstr ""

#: lib/content/course_live/course_view.ex:43
#: lib/content/course_live/course_view.ex:44
#, elixir-autogen, elixir-format
msgid "Failed to enroll"
msgstr ""
Expand Down Expand Up @@ -91,7 +91,7 @@ msgstr ""
msgid "Request to join"
msgstr ""

#: lib/content/course_live/components/lesson_step.ex:37
#: lib/content/course_live/components/lesson_step.ex:38
#, elixir-autogen, elixir-format
msgid "That's incorrect."
msgstr ""
Expand All @@ -101,12 +101,12 @@ msgstr ""
msgid "There's room for improvement"
msgstr ""

#: lib/content/course_live/lesson_play.ex:90
#: lib/content/course_live/lesson_play.ex:91
#, elixir-autogen, elixir-format
msgid "Unable to complete lesson"
msgstr ""

#: lib/content/course_live/lesson_play.ex:53
#: lib/content/course_live/lesson_play.ex:54
#, elixir-autogen, elixir-format
msgid "Unable to select option"
msgstr ""
Expand All @@ -116,7 +116,7 @@ msgstr ""
msgid "Very good!"
msgstr ""

#: lib/content/course_live/components/lesson_step.ex:37
#: lib/content/course_live/components/lesson_step.ex:38
#, elixir-autogen, elixir-format
msgid "Well done!"
msgstr ""
Expand Down Expand Up @@ -151,7 +151,7 @@ msgstr ""
msgid "No courses"
msgstr ""

#: lib/content/course_live/lesson_play.ex:76
#: lib/content/course_live/lesson_play.ex:77
#, elixir-autogen, elixir-format
msgid "Unable to send answer"
msgstr ""
Expand Down
16 changes: 8 additions & 8 deletions priv/gettext/de/LC_MESSAGES/courses.po
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: lib/content/course_live/course_view.ex:59
#: lib/content/course_live/course_view.ex:60
#, elixir-autogen, elixir-format
msgid "A request to enroll has been sent to the course teacher."
msgstr "Ein Antrag auf Einschreibung wurde an den Kursleiter geschickt."
Expand All @@ -36,7 +36,7 @@ msgstr "Anfänger"
msgid "Confirming..."
msgstr "Bestätigen..."

#: lib/content/course_live/course_view.ex:58
#: lib/content/course_live/course_view.ex:59
#, elixir-autogen, elixir-format
msgid "Enrolled successfully!"
msgstr "Erfolgreich eingeschrieben!"
Expand All @@ -51,7 +51,7 @@ msgstr "Ausgezeichnet!"
msgid "Expert"
msgstr "Experte"

#: lib/content/course_live/course_view.ex:43
#: lib/content/course_live/course_view.ex:44
#, elixir-autogen, elixir-format
msgid "Failed to enroll"
msgstr "Einschreibung fehlgeschlagen"
Expand Down Expand Up @@ -91,7 +91,7 @@ msgstr "Perfekt!"
msgid "Request to join"
msgstr "Antrag auf Beitritt"

#: lib/content/course_live/components/lesson_step.ex:37
#: lib/content/course_live/components/lesson_step.ex:38
#, elixir-autogen, elixir-format
msgid "That's incorrect."
msgstr "Das ist nicht richtig."
Expand All @@ -101,12 +101,12 @@ msgstr "Das ist nicht richtig."
msgid "There's room for improvement"
msgstr "Es gibt Raum für Verbesserungen"

#: lib/content/course_live/lesson_play.ex:90
#: lib/content/course_live/lesson_play.ex:91
#, elixir-autogen, elixir-format
msgid "Unable to complete lesson"
msgstr "Lektion kann nicht abgeschlossen werden"

#: lib/content/course_live/lesson_play.ex:53
#: lib/content/course_live/lesson_play.ex:54
#, elixir-autogen, elixir-format
msgid "Unable to select option"
msgstr "Option kann nicht ausgewählt werden"
Expand All @@ -116,7 +116,7 @@ msgstr "Option kann nicht ausgewählt werden"
msgid "Very good!"
msgstr "Sehr gut!"

#: lib/content/course_live/components/lesson_step.ex:37
#: lib/content/course_live/components/lesson_step.ex:38
#, elixir-autogen, elixir-format
msgid "Well done!"
msgstr "Gut gemacht!"
Expand Down Expand Up @@ -151,7 +151,7 @@ msgstr "Beginnen Sie mit der Teilnahme an einem Kurs."
msgid "No courses"
msgstr "Keine Kurse"

#: lib/content/course_live/lesson_play.ex:76
#: lib/content/course_live/lesson_play.ex:77
#, elixir-autogen, elixir-format
msgid "Unable to send answer"
msgstr "Senden der Antwort fehlgeschlagen"
Expand Down
Loading

0 comments on commit a14abe2

Please sign in to comment.