diff --git a/config/dev.exs b/config/dev.exs index 0dd59db..bb32dbd 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -10,6 +10,12 @@ config :accumulator, AccumulatorWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. http: [ip: {0, 0, 0, 0}, port: 4000], + https: [ + port: 4001, + cipher_suite: :strong, + certfile: "priv/cert/selfsigned.pem", + keyfile: "priv/cert/selfsigned_key.pem" + ], check_origin: false, code_reloader: true, debug_errors: true, diff --git a/config/prod.exs b/config/prod.exs index f66c59d..184cb81 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -18,8 +18,7 @@ config :accumulator, AccumulatorWeb.Endpoint, check_origin: [ "https://aayushsahu.com", "https://phoenix-aayushsahu-com.fly.dev", - "https://phoenix.aayushsahu.com/", - "moz-extension://*" + "https://phoenix.aayushsahu.com/" ] # Do not print debug messages in production diff --git a/lib/accumulator/application.ex b/lib/accumulator/application.ex index e9f1795..2fb902b 100644 --- a/lib/accumulator/application.ex +++ b/lib/accumulator/application.ex @@ -33,7 +33,8 @@ defmodule Accumulator.Application do Accumulator.Repo, {Accumulator.Scheduler.Spotify, interval: 60000}, {Accumulator.Scheduler.Pastes, interval: 3_600_000}, - {Task.Supervisor, name: Accumulator.TaskRunner} + {Task.Supervisor, name: Accumulator.TaskRunner}, + Accumulator.Extension ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/accumulator/extension.ex b/lib/accumulator/extension.ex new file mode 100644 index 0000000..82b59cb --- /dev/null +++ b/lib/accumulator/extension.ex @@ -0,0 +1,30 @@ +defmodule Accumulator.Extension do + @moduledoc """ + Stuff for browser extension + """ + alias Accumulator.{Repo, Extension.Bookmarks} + + # Agent stuff + + use Agent + + def start_link(_params) do + Agent.start_link(fn -> %{} end, name: __MODULE__) + end + + def add_tabs(id, tabs) do + Agent.update(__MODULE__, fn state -> + Map.update(state, id, [], fn _ -> tabs end) + end) + end + + def get_tabs(id) do + Agent.get(__MODULE__, & &1) |> Map.delete(id) + end + + def add_bookmark(params) do + %Bookmarks{} + |> Bookmarks.changeset(params) + |> Repo.insert() + end +end diff --git a/lib/accumulator/extension/bookmarks.ex b/lib/accumulator/extension/bookmarks.ex new file mode 100644 index 0000000..0a4f6f0 --- /dev/null +++ b/lib/accumulator/extension/bookmarks.ex @@ -0,0 +1,19 @@ +defmodule Accumulator.Extension.Bookmarks do + use Ecto.Schema + import Ecto.Changeset + + schema "extension_bookmarks" do + field(:url, :string) + field(:title, :string) + field(:browser_id, :string) + timestamps(updated_at: false) + end + + def changeset(bookmark, params \\ %{}) do + fields = [:url, :title, :browser_id] + + bookmark + |> cast(params, fields) + |> validate_required(fields) + end +end diff --git a/lib/accumulator_web/channels/extension_channel.ex b/lib/accumulator_web/channels/extension_channel.ex new file mode 100644 index 0000000..62203cc --- /dev/null +++ b/lib/accumulator_web/channels/extension_channel.ex @@ -0,0 +1,59 @@ +defmodule AccumulatorWeb.ExtensionChannel do + use AccumulatorWeb, :channel + + alias Accumulator.Extension + alias AccumulatorWeb.Presence + + @impl true + def join("extension", %{"browser" => browser, "id" => id} = _payload, socket) do + send(self(), :after_join) + {:ok, socket |> assign(:browser, browser) |> assign(:id, id)} + end + + @impl true + def handle_info(:after_join, socket) do + {:ok, _} = + Presence.track(socket, "extensions", %{ + browser: socket.assigns.browser, + id: socket.assigns.id + }) + + push(socket, "presence_state", Presence.list(socket)) + {:noreply, socket} + end + + @impl true + def handle_in("tabs", %{"tabs" => tabs} = payload, socket) do + Extension.add_tabs(socket.assigns.id, tabs) + # broadcast to listeners + {:reply, {:ok, payload}, socket} + end + + def handle_in("bookmark-tab", payload, socket) do + params = Map.put(payload, "browser_id", socket.assigns.id) + + reply_payload = + case Extension.add_bookmark(params) do + {:ok, _} -> + %{status: "ok"} + + {:error, _} -> + %{status: "fail"} + end + + {:reply, {:ok, reply_payload}, socket} + end + + def handle_in("get-tabs", _payload, socket) do + tabs = Extension.get_tabs(socket.assigns.id) + {:reply, {:ok, tabs}, socket} + end + + # It is also common to receive messages from the client and + # broadcast to everyone in the current topic (extension:lobby). + @impl true + def handle_in("shout", payload, socket) do + broadcast(socket, "shout", payload) + {:noreply, socket} + end +end diff --git a/lib/accumulator_web/channels/extension_socket.ex b/lib/accumulator_web/channels/extension_socket.ex new file mode 100644 index 0000000..148d316 --- /dev/null +++ b/lib/accumulator_web/channels/extension_socket.ex @@ -0,0 +1,54 @@ +defmodule AccumulatorWeb.ExtensionSocket do + use Phoenix.Socket + + # A Socket handler + # + # It's possible to control the websocket connection and + # assign values that can be accessed by your channel topics. + + ## Channels + # Uncomment the following line to define a "room:*" topic + # pointing to the `AccumulatorWeb.RoomChannel`: + # + # channel "room:*", AccumulatorWeb.RoomChannel + # + # To create a channel file, use the mix task: + # + # mix phx.gen.channel Room + # + # See the [`Channels guide`](https://hexdocs.pm/phoenix/channels.html) + # for further details. + channel "extension", AccumulatorWeb.ExtensionChannel + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error` or `{:error, term}`. To control the + # response the client receives in that case, [define an error handler in the + # websocket + # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration). + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + @impl true + def connect(_params, socket, _connect_info) do + {:ok, socket} + end + + # Socket IDs are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # Elixir.AccumulatorWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + @impl true + def id(_socket), do: nil +end diff --git a/lib/accumulator_web/endpoint.ex b/lib/accumulator_web/endpoint.ex index 85f6044..7c92e4a 100644 --- a/lib/accumulator_web/endpoint.ex +++ b/lib/accumulator_web/endpoint.ex @@ -57,4 +57,9 @@ defmodule AccumulatorWeb.Endpoint do socket "/socket", AccumulatorWeb.UserSocket, websocket: [connect_info: [session: @session_options]], longpoll: false + + socket "/extension", AccumulatorWeb.ExtensionSocket, + websocket: true, + longpoll: false, + check_origin: false end diff --git a/priv/cert/selfsigned.pem b/priv/cert/selfsigned.pem new file mode 100644 index 0000000..90d59d1 --- /dev/null +++ b/priv/cert/selfsigned.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIIRviyvqWz3P8wDQYJKoZIhvcNAQELBQAwQzEaMBgGA1UE +CgwRUGhvZW5peCBGcmFtZXdvcmsxJTAjBgNVBAMMHFNlbGYtc2lnbmVkIHRlc3Qg +Y2VydGlmaWNhdGUwHhcNMjQwNTI1MDAwMDAwWhcNMjUwNTI1MDAwMDAwWjBDMRow +GAYDVQQKDBFQaG9lbml4IEZyYW1ld29yazElMCMGA1UEAwwcU2VsZi1zaWduZWQg +dGVzdCBjZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AJ5Yh/7Yje1nLdf0UGvUuyFsA8DoC03a4AirVA65zTb6T+5sD+FfVjLFdj9i+jlr +OA2MDT8dA1gBru1ZQJN7knVxd2+XfSqbL0nfHQiGpRLcrxenKvHObgd3zIumbfHX +RsWzAbclK7jAX2kIMhJmDjzsD3ldgyH5Uvc1Qae88H8s0+2jVJMTe3c743u2flaW +ZbRf5niVYIw2F+VObdUYG0QbhiwcXjj8cN0sPdD8dksgY/dUvJV+mQP7TAVo0F8C +ENF6aTIqshrhDMuUqLvjFGszV96nNrj8R34V134TtQEDV4DVeYr0C1mcfmIA5Gsf +32e8ebVoukWHVvUvKF00KJECAwEAAaN0MHIwDAYDVR0TAQH/BAIwADAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW +BBRJz4SJ/IVx0yJBtUUwJGncxJ2cPjAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJ +KoZIhvcNAQELBQADggEBAAoWGlHCb8e+oa4iIPkzFE2dDjrG9V34vFQQSr0JOEnQ +6x6H8BjeQCV1yu2mbtrh1RMWE1uibde6thQaUhojAyYnnFBaQo+8tzsx1QGXT1rb +aW/nxGnvL3fckNph4JgsVBnSBiRIt9aQPET5askp0reIUAOYxgdludmsSPmdrWgX +hBHmMlJDIHlWCrtCwQKIEuHkgIhaDAefKSiaF7lVBgODYWPA2f2+8hfewAWGGAti ++RJbN7Pbs590hIKYoKuhAi7rUou0iKj1aCfSf8Ag2vrfUr0VKJYIgC5sYCxy3tEG +vcJx4y90shFhB3UZzluLpbamfD7Y33wDlTZ84d2NRLc= +-----END CERTIFICATE----- + diff --git a/priv/cert/selfsigned_key.pem b/priv/cert/selfsigned_key.pem new file mode 100644 index 0000000..29443d1 --- /dev/null +++ b/priv/cert/selfsigned_key.pem @@ -0,0 +1,28 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAnliH/tiN7Wct1/RQa9S7IWwDwOgLTdrgCKtUDrnNNvpP7mwP +4V9WMsV2P2L6OWs4DYwNPx0DWAGu7VlAk3uSdXF3b5d9KpsvSd8dCIalEtyvF6cq +8c5uB3fMi6Zt8ddGxbMBtyUruMBfaQgyEmYOPOwPeV2DIflS9zVBp7zwfyzT7aNU +kxN7dzvje7Z+VpZltF/meJVgjDYX5U5t1RgbRBuGLBxeOPxw3Sw90Px2SyBj91S8 +lX6ZA/tMBWjQXwIQ0XppMiqyGuEMy5Sou+MUazNX3qc2uPxHfhXXfhO1AQNXgNV5 +ivQLWZx+YgDkax/fZ7x5tWi6RYdW9S8oXTQokQIDAQABAoIBAAjOEvfemlvIaEHx +LbSlTmeQHlBQhupkIJ+SyViNR+ZF+oJfQ23Mk5o8pTPmoNPnDKWiM01eY2R2KYA8 +vYzF6xG5RsMviRAAs5s3uyFRfWXkXB8kVky+Zhtns7nTPhtb1W1iZBbHTBxYpCWO +xVwFDUcheEXPJ57QwqaI8VbuuG/V25oUwi/dwto9jc5YzBFXkMvGnSHROkDLFQ8U +wK6z4N7jGxDOc5GLT5Yli/wgd17rFck8GFywUuqZdifVKMigDGxb1jzrELlK/Qrv +jYeZV1UlYhNY/zx1LTz0sF+odIGCj2mpFhV81Dli3L5TRtBdy4N238A2KpTa0UJ6 +IfE43IECgYEA2NEJqRDotJKXGkMXGLT4ngCcPxR6pofVHEuGDwK3VgCKNQsKvTjW +bd/1OLrKMotAV07zca/K1Vv7WHCvHlobGenZAZiJqrEhcRySpFWNhAhRHI+J8y30 +Uyg1H1MXChTU9lB5hW68WY2wsNM+aUz4cEKADJMLyAdSvhf3XkKztGECgYEAuvZc +ODSrhodThhbGCglB4u8uvD3qxxTqicC/jamcz0FtP4DQsZjOGulTUgfjBoTsRpbu +jcVJk9us75veVsPY292wkkZzuhAUepWxx3v/RQLthqanxM1MXKTXXvJTo+r2+iw7 +cmSLK0HX4O7N6+hsq/GzITVD+F7gsiHx/I0x4jECgYEA0RXFGyUTRA+ZZ9oLC+h5 +WOV9x9cX8EBNY1vxi8gyxN0AauabFJ8bKhovgOWg190xzwB0A85i7B4n5MHGHp8G +Q5cfjkpreBAZD9teDtvx/MGIduJ1Re2rEAZWND8MmMw+EsrIZcTEHhhlrCAKr8Fq +U9fNZFLpqZxmTqsOAfiRFAECgYAIOXw3EMIW6e8Xr/rISD34wLLanxKr7VSf+LW0 +gqieSW+H4p/LoEA42NjMfAJVsBVAybT20Z35/ijuZXnzcSwiB++Tj7vZjImKFvm6 +H89L9uQCD2TD+JAKZ0n+KETbqiNxP+7himDA52WaxIaUgSX+rmRF6rTxwSK7U1j0 +1jVCkQKBgQCzvwiUC9yMxrgsUN3vcXC57MjbGSp34O2GIKamWa7X5PSigM+vDTzO +iGPwM1VXLzXDUr88ftOiK9CBwPjNlGy86rwjlwTW4kUDn9PC0zhtTXC/+DPmn9uH +s1EypwUQ0u01VUuv1kyA1sKWJd21tqqemzmVgTTraJvr7hs42eWl6A== +-----END RSA PRIVATE KEY----- + diff --git a/priv/repo/migrations/20240531211843_extension-bookmarks.exs b/priv/repo/migrations/20240531211843_extension-bookmarks.exs new file mode 100644 index 0000000..44181f5 --- /dev/null +++ b/priv/repo/migrations/20240531211843_extension-bookmarks.exs @@ -0,0 +1,12 @@ +defmodule :"Elixir.Accumulator.Repo.Migrations.Extension-bookmarks" do + use Ecto.Migration + + def change do + create table(:extension_bookmarks) do + add(:url, :text) + add(:title, :text) + add(:browser_id, :string) + timestamps(updated_at: false) + end + end +end