Skip to content

Commit 81c4784

Browse files
committed
Add CORs headers to every response
Considering the use cases of the sync router macro and controller functions, it makes sense to just always whitelist the electric- headers required for the client. Fixes #26
1 parent 87888ea commit 81c4784

File tree

6 files changed

+161
-2
lines changed

6 files changed

+161
-2
lines changed

lib/phoenix/sync/controller.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ defmodule Phoenix.Sync.Controller do
4343
4444
"""
4545

46+
alias Phoenix.Sync.Plug.CORS
47+
4648
defmacro __using__(opts \\ []) do
4749
# validate that we're being used in the context of a Plug.Router impl
4850
Phoenix.Sync.Plug.Utils.env!(__CALLER__)
@@ -98,6 +100,6 @@ defmodule Phoenix.Sync.Controller do
98100

99101
{:ok, shape_api} = Phoenix.Sync.Adapter.PlugApi.predefined_shape(api, predefined_shape)
100102

101-
Phoenix.Sync.Adapter.PlugApi.call(shape_api, conn, params)
103+
Phoenix.Sync.Adapter.PlugApi.call(shape_api, CORS.call(conn), params)
102104
end
103105
end

lib/phoenix/sync/plug/cors.ex

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
defmodule Phoenix.Sync.Plug.CORS do
2+
@moduledoc """
3+
A `Plug` that adds the necessary CORS headers to responses from Electric sync
4+
endpoints.
5+
6+
`Phoenix.Sync.Controller.sync_render/4` and `Phoenix.Sync.Router.sync/2`
7+
already include these headers so there's no need to add this plug to your
8+
`Phoenix` or `Plug` router. This module is just exposed as a convenience.
9+
"""
10+
11+
@behaviour Plug
12+
13+
@electric_headers [
14+
"electric-cursor",
15+
"electric-handle",
16+
"electric-offset",
17+
"electric-schema",
18+
"electric-up-to-date"
19+
]
20+
21+
@expose_headers ["transfer-encoding" | @electric_headers]
22+
23+
def init(opts) do
24+
Map.new(opts)
25+
end
26+
27+
def call(conn) do
28+
conn
29+
|> Plug.Conn.put_resp_header("access-control-allow-origin", origin(conn))
30+
|> Plug.Conn.put_resp_header(
31+
"access-control-allow-methods",
32+
"GET, POST, PUT, DELETE, OPTIONS"
33+
)
34+
|> Plug.Conn.put_resp_header(
35+
"access-control-expose-headers",
36+
Enum.join(@expose_headers, ", ")
37+
)
38+
end
39+
40+
def call(conn, _opts) do
41+
call(conn)
42+
end
43+
44+
defp origin(conn) do
45+
case Plug.Conn.get_req_header(conn, "origin") do
46+
[] -> "*"
47+
[origin] -> origin
48+
end
49+
end
50+
51+
@doc false
52+
@spec electric_headers() :: [String.t()]
53+
def electric_headers, do: @electric_headers
54+
end

lib/phoenix/sync/router.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,11 @@ defmodule Phoenix.Sync.Router do
216216
defp serve_shape(conn, api, shape) do
217217
{:ok, shape_api} = Phoenix.Sync.Adapter.PlugApi.predefined_shape(api, shape)
218218

219-
conn = Plug.Conn.fetch_query_params(conn)
219+
conn =
220+
conn
221+
|> Plug.Conn.fetch_query_params()
222+
|> Phoenix.Sync.Plug.CORS.call()
223+
220224
Phoenix.Sync.Adapter.PlugApi.call(shape_api, conn, conn.params)
221225
end
222226
end

test/phoenix/sync/controller_test.exs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ defmodule Phoenix.Sync.ControllerTest do
9090
] = Jason.decode!(resp.resp_body)
9191
end
9292

93+
test "includes CORS headers", _ctx do
94+
resp =
95+
Phoenix.ConnTest.build_conn()
96+
|> Phoenix.ConnTest.get("/todos/all", %{offset: "-1"})
97+
98+
assert resp.status == 200
99+
assert [expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
100+
assert String.contains?(expose, "electric-offset")
101+
end
102+
93103
test "supports where clauses", _ctx do
94104
resp =
95105
Phoenix.ConnTest.build_conn()
@@ -185,5 +195,14 @@ defmodule Phoenix.Sync.ControllerTest do
185195
"application/json; charset=utf-8"
186196
]
187197
end
198+
199+
test "includes CORS headers", ctx do
200+
conn = conn(:get, "/shape/todos", %{"offset" => "-1"})
201+
202+
resp = PlugRouter.call(conn, PlugRouter.init(ctx.plug_opts))
203+
204+
assert [expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
205+
assert String.contains?(expose, "electric-offset")
206+
end
188207
end
189208
end

test/phoenix/sync/plug/cors_test.exs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
defmodule Phoenix.Sync.Plug.CorsHeadersTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Phoenix.Sync.Plug.CORS
5+
6+
import Plug.Test
7+
8+
test "electric headers are up-to-date with current electric" do
9+
# in test we always have electric as a dependency so we can test
10+
# that our vendored headers are up-to-date
11+
assert CORS.electric_headers() == Electric.Shapes.Api.Response.electric_headers()
12+
end
13+
14+
test "adds access-control-expose-headers header to response" do
15+
resp =
16+
conn(:get, "/sync/bananas")
17+
|> CORS.call(%{})
18+
19+
[expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
20+
21+
for header <- CORS.electric_headers() do
22+
assert String.contains?(expose, header)
23+
end
24+
25+
assert String.contains?(expose, "transfer-encoding")
26+
end
27+
28+
test "adds access-control-allow-origin header to response" do
29+
resp =
30+
conn(:get, "/v1/shape")
31+
|> CORS.call(%{})
32+
33+
["*"] = Plug.Conn.get_resp_header(resp, "access-control-allow-origin")
34+
35+
resp =
36+
conn(:get, "/large/noise")
37+
|> Plug.Conn.put_req_header("origin", "https://example.com")
38+
|> CORS.call(%{})
39+
40+
["https://example.com"] = Plug.Conn.get_resp_header(resp, "access-control-allow-origin")
41+
end
42+
43+
test "adds access-control-allow-methods header to response" do
44+
resp =
45+
conn(:get, "/my-shape")
46+
|> CORS.call(%{})
47+
48+
["GET, POST, PUT, DELETE, OPTIONS"] =
49+
Plug.Conn.get_resp_header(resp, "access-control-allow-methods")
50+
end
51+
end

test/phoenix/sync/router_test.exs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,25 @@ defmodule Phoenix.Sync.RouterTest do
188188
]
189189
end
190190

191+
@tag table: {
192+
"todos",
193+
[
194+
"id int8 not null primary key generated always as identity",
195+
"title text",
196+
"completed boolean default false"
197+
]
198+
}
199+
@tag data: {"todos", ["title"], [["one"], ["two"], ["three"]]}
200+
test "returns CORS headers", _ctx do
201+
resp =
202+
Phoenix.ConnTest.build_conn()
203+
|> Phoenix.ConnTest.get("/sync/things-to-do", %{offset: "-1"})
204+
205+
assert resp.status == 200
206+
assert [expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
207+
assert String.contains?(expose, "electric-offset")
208+
end
209+
191210
@tag table: {
192211
"ideas",
193212
[
@@ -465,5 +484,15 @@ defmodule Phoenix.Sync.RouterTest do
465484
] = Jason.decode!(resp.resp_body)
466485
end
467486
end
487+
488+
test "returns CORS headers", ctx do
489+
resp =
490+
conn(:get, "/shapes/todos", %{"offset" => "-1"})
491+
|> MyRouter.call(ctx.plug_opts)
492+
493+
assert resp.status == 200
494+
assert [expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
495+
assert String.contains?(expose, "electric-offset")
496+
end
468497
end
469498
end

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy