Skip to content

Commit

Permalink
ensure scopes plug
Browse files Browse the repository at this point in the history
  • Loading branch information
ericdude4 committed Aug 20, 2021
1 parent 320e2a0 commit 849a7dc
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 13 deletions.
25 changes: 19 additions & 6 deletions assets/js/app.js
Expand Up @@ -7,21 +7,34 @@ import enTranslations from '@shopify/polaris/locales/en.json'
import '@shopify/polaris/styles.css'

import ShowPlans from './components/show-plans'
import ExternalRedirect from './components/external-redirect'

function WrappedShowPlans(props) {
return (
<AppProvider i18n={enTranslations}>
<ShowPlans
plans={props.plans}
guard={props.guard}
shopUrl={props.shop_url}
redirectAfter={props.redirect_after}
<ShowPlans
plans={props.plans}
guard={props.guard}
shopUrl={props.shop_url}
redirectAfter={props.redirect_after}
shopifyApiKey={props.shopify_api_key}
sessionToken={props.session_token}
/>
</AppProvider>
)
}

function WrappedRedirect({
shop_url, shopify_api_key, redirect_location
}) {
return (
<AppProvider i18n={enTranslations}>
<ExternalRedirect shopUrl={shop_url} shopifyApiKey={shopify_api_key} redirectLocation={redirect_location} />
</AppProvider>
);
}

window.Components = {
WrappedShowPlans
WrappedShowPlans,
WrappedRedirect
}
43 changes: 43 additions & 0 deletions assets/js/components/external-redirect.js
@@ -0,0 +1,43 @@
import React, { useEffect } from 'react';
import createApp from '@shopify/app-bridge';
import { Redirect } from '@shopify/app-bridge/actions';
import { Frame, TextContainer, Layout, Page } from '@shopify/polaris'

export default function ExternalRedirect({ shopUrl, shopifyApiKey, redirectLocation }) {
const app = createApp({
apiKey: shopifyApiKey,
shopOrigin: shopUrl,
});

const redirect = Redirect.create(app);

const doRedirect = () => {
if (window.self == window.top) {
// do a normal redirect if not in an iFrame
window.location = redirectLocation
} else {
redirect.dispatch(Redirect.Action.REMOTE, redirectLocation)
}
}

useEffect(() => {
doRedirect()
}, [])

return (
<Page title="You are being redirected">
<Layout>
<Layout.Section>
<Frame>
<TextContainer style="padding-top: 3 rem;">
<p>
If you are not automatically redirected within 5 seconds, &nbsp;
<a href="#" onClick={doRedirect}>click here</a>
</p>
</TextContainer>
</Frame>
</Layout.Section>
</Layout>
</Page>
);
}
3 changes: 3 additions & 0 deletions lib/shopifex/errors/runtime_error.ex
@@ -0,0 +1,3 @@
defmodule Shopifex.RuntimeError do
defexception message: "An error occurred"
end
55 changes: 55 additions & 0 deletions lib/shopifex/plug/ensure_scopes.ex
@@ -0,0 +1,55 @@
defmodule Shopifex.Plug.EnsureScopes do
@moduledoc """
This plug ensures that the shop which is currently loaded in the session
has all of the scopes which are defined under `config :shopifex, scopes: "foo"`.
If the current shop does not have all the scopes, the conn is redirected to
the Shopify OAuth update flow.
Simply adding a new scope to your `:shopifex, scopes: "foo"` config will
trigger an OAuth update with your installations.
"""
import Plug.Conn
import Phoenix.Controller
require Logger

def init(options) do
# initialize options
options
end

def call(conn, _) do
case Shopifex.Plug.current_shop(conn) do
nil ->
raise(
Shopifex.RuntimeError,
"""
`Shopifex.Plug.EnsureScopes` must be placed in the pipeline after a plug which places a shop in the session; such as `Shopifex.Plug.ShopifySession` or `Shopifex.Plug.ShopifyWebhook`
"""
)

shop ->
required_scopes = String.split(Application.get_env(:shopifex, :scopes), ",")
shop_scopes = String.split(shop.scope, ",")

case required_scopes -- shop_scopes do
[] ->
conn

missing_scopes ->
Logger.info(
"Shop #{shop.url} is missing required scopes #{inspect(missing_scopes)}, initiating app update"
)

reinstall_url =
"https://#{shop.url}/admin/oauth/authorize?client_id=#{Application.fetch_env!(:shopifex, :api_key)}&scope=#{Application.fetch_env!(:shopifex, :scopes)}&redirect_uri=#{Application.fetch_env!(:shopifex, :reinstall_uri)}"

conn
|> put_view(ShopifexWeb.PageView)
|> put_layout({ShopifexWeb.LayoutView, "app.html"})
|> render("redirect.html", redirect_location: reinstall_url)
|> halt()
end
end
end
end
2 changes: 1 addition & 1 deletion lib/shopifex_web/controllers/auth_controller.ex
Expand Up @@ -94,7 +94,7 @@ defmodule ShopifexWeb.AuthController do
Logger.info("Initiating shop reinstallation for #{shop_url}")

reinstall_url =
"https://#{shop_url}/admin/oauth/request_grant?client_id=#{Application.fetch_env!(:shopifex, :api_key)}&scope=#{Application.fetch_env!(:shopifex, :scopes)}&redirect_uri=#{Application.fetch_env!(:shopifex, :reinstall_uri)}"
"https://#{shop_url}/admin/oauth/authorize?client_id=#{Application.fetch_env!(:shopifex, :api_key)}&scope=#{Application.fetch_env!(:shopifex, :scopes)}&redirect_uri=#{Application.fetch_env!(:shopifex, :reinstall_uri)}"

conn
|> redirect(external: reinstall_url)
Expand Down
2 changes: 2 additions & 0 deletions lib/shopifex_web/routes.ex
Expand Up @@ -11,6 +11,7 @@ defmodule ShopifexWeb.Routes do

pipeline :shopify_session do
plug(Shopifex.Plug.ShopifySession)
plug(Shopifex.Plug.EnsureScopes)
plug(Shopifex.Plug.LoadInIframe)
end

Expand All @@ -30,6 +31,7 @@ defmodule ShopifexWeb.Routes do
plug(Shopifex.Plug.FetchFlash)
plug(Shopifex.Plug.LoadInIframe)
plug(Shopifex.Plug.ShopifyWebhook)
plug(Shopifex.Plug.EnsureScopes)
end

pipeline :shopify_api do
Expand Down
10 changes: 10 additions & 0 deletions lib/shopifex_web/templates/page/redirect.html.eex
@@ -0,0 +1,10 @@
<%=
ReactPhoenix.ClientSide.react_component(
"Components.WrappedRedirect",
%{
shop_url: Shopifex.Plug.current_shop(@conn).url,
redirect_location: @redirect_location,
shopify_api_key: Application.get_env(:shopifex, :api_key)
}
)
%>
3 changes: 3 additions & 0 deletions lib/shopifex_web/views/page_view.ex
@@ -0,0 +1,3 @@
defmodule ShopifexWeb.PageView do
use ShopifexWeb, :view
end
2 changes: 1 addition & 1 deletion mix.exs
Expand Up @@ -4,7 +4,7 @@ defmodule Shopifex.MixProject do
def project do
[
app: :shopifex,
version: "2.0.0",
version: "2.0.1",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
Expand Down
2 changes: 1 addition & 1 deletion priv/static/js/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion priv/static/js/app.js.map

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions test/plug/ensure_scopes_test.exs
@@ -0,0 +1,45 @@
defmodule Shopifex.Plug.EnsureScopesTest do
use ShopifexWeb.ConnCase

setup do
conn = build_conn(:get, "/my-route?foo=bar&fizz=buzz")
{:ok, conn: conn}
end

setup [:shop_in_session]

test "shop scopes matching shopifex config passes plug", %{
conn: conn
} do
shop = Shopifex.Plug.current_shop(conn)
Application.put_env(:shopifex, :scopes, shop.scope)

conn = Shopifex.Plug.EnsureScopes.call(conn, [])

refute conn.halted
end

test "renders redirect page with location to Shopify OAuth update flow", %{
conn: conn
} do
Application.put_env(:shopifex, :scopes, "read_orders")

conn = Shopifex.Plug.EnsureScopes.call(conn, [])

assert conn.halted
assert html_response(conn, 200) =~ "Components.WrappedRedirect"

assert conn.assigns.redirect_location =~
"https://shopifex.myshopify.com/admin/oauth/authorize?client_id=thisisafakeapikey"
end

test "throws error when shop not in session", %{
conn: conn
} do
assert_raise Shopifex.RuntimeError, fn ->
conn
|> Map.put(:private, %{})
|> Shopifex.Plug.EnsureScopes.call([])
end
end
end
7 changes: 4 additions & 3 deletions test/plug/payment_guard_test.exs
Expand Up @@ -8,9 +8,10 @@ defmodule Shopifex.Plug.PaymentGuardTest do

setup [:shop_in_session]

test "payment guard blocks pay-walled function and redirects to payment page with session token", %{
conn: conn
} do
test "payment guard blocks pay-walled function and redirects to payment page with session token",
%{
conn: conn
} do
halted_conn = Shopifex.Plug.PaymentGuard.call(conn, "block")

assert html_response(halted_conn, 302) =~
Expand Down

0 comments on commit 849a7dc

Please sign in to comment.