Browse Source

Merge branch 'feature/provide-indieauth-endpoints' into develop

* feature/provide-indieauth-endpoints:
  chore(project): Add IndieAuth.
  chore(project): Update UX.
jackyalcine 2 months ago
parent
commit
ed4236dfc9
Signed by: Jacky Alciné <yo@jacky.wtf> GPG Key ID: 537A4F904B15268D
44 changed files with 876 additions and 478 deletions
  1. 1
    1
      assets/app.ts
  2. 15
    7
      assets/site.scss
  3. 3
    2
      lib/application.ex
  4. 3
    0
      lib/auth/state.ex
  5. 0
    1
      lib/http.ex
  6. 0
    24
      lib/jobs/relme/verify.ex
  7. 0
    8
      lib/jobs/relme/verify_batch.ex
  8. 36
    44
      lib/provider.ex
  9. 3
    3
      lib/provider/federation.ex
  10. 3
    3
      lib/provider/silo.ex
  11. 0
    222
      lib/relme.ex
  12. 6
    3
      lib/repo/site.ex
  13. 88
    0
      lib/repo/site_provider.ex
  14. 25
    0
      lib/worker/provider.ex
  15. 2
    2
      mix.lock
  16. 1
    3
      priv/repo/migrations/20190302070000_create_sites.exs
  17. 17
    0
      priv/repo/migrations/20191120013944_create_site_providers.exs
  18. 9
    0
      priv/repo/migrations/20191120022406_add_column_status_to_site_providers.exs
  19. 259
    0
      web/controllers/indie/auth_controller.ex
  20. 133
    0
      web/controllers/indie/token_controller.ex
  21. 19
    1
      web/controllers/oauth2.ex
  22. 11
    1
      web/controllers/page.ex
  23. 25
    13
      web/controllers/resolver.ex
  24. 4
    2
      web/fortress.ex
  25. 66
    0
      web/helpers/auth.ex
  26. 0
    7
      web/helpers/auth_pipeline.ex
  27. 10
    0
      web/plug/auth_pipeline/indie.ex
  28. 10
    0
      web/plug/auth_pipeline/owner.ex
  29. 12
    0
      web/plug/error_handler/api.ex
  30. 16
    0
      web/plug/error_handler/browser.ex
  31. 18
    23
      web/router.ex
  32. 0
    48
      web/templates/indie/auth/authorize-code.html.eex
  33. 0
    0
      web/templates/indie/auth/authorize.html.eex
  34. 1
    25
      web/templates/indie/auth/error.html.eex
  35. 4
    9
      web/templates/layout/app.html.eex
  36. 15
    0
      web/templates/o_auth2/request-error.html.eex
  37. 7
    0
      web/templates/page/dashboard.html.eex
  38. 11
    7
      web/templates/page/homepage-login.html.leex
  39. 1
    1
      web/templates/page/index.html.eex
  40. 4
    2
      web/templates/resolver/present.html.leex
  41. 3
    0
      web/views/indie/auth.ex
  42. 26
    4
      web/views/live/homepage_auth_view.ex
  43. 3
    3
      web/views/live/provider-component.ex
  44. 6
    9
      web/views/live/resolver.ex

+ 1
- 1
assets/app.ts View File

@@ -51,7 +51,7 @@ window.addEventListener('load', () => {
51 51
     Feather.replace()
52 52
     WebFont.load({
53 53
         google: {
54
-            families: ['Oswald', 'Playfair Display', 'Open Sans', 'Droid Mono']
54
+            families: ['Oswald', 'Playfair Display', 'Open Sans', 'Droid Sans Mono']
55 55
         }
56 56
     })
57 57
     buildLiveSocket()

+ 15
- 7
assets/site.scss View File

@@ -4,7 +4,7 @@
4 4
   --font-flagship: "Oswald";
5 5
   --font-serif: "Playfair Display";
6 6
   --font-sans: "Open Sans";
7
-  --font-mono: "Droid Mono";
7
+  --font-mono: "Droid Sans Mono";
8 8
 }
9 9
 
10 10
 .font--flagship {
@@ -40,7 +40,7 @@ svg.feather.spin {
40 40
 }
41 41
 
42 42
 div.provider--status {
43
-  @extend .mb1, .br2, .bw2, .b--transparent, .pa2, .w-100, .flex-row, .flex, .items-center, .justify-start;
43
+  @extend .mb1, .ba, .br2, .bw1, .mv1, .pa2, .w-100, .flex-row, .flex, .items-center, .justify-start;
44 44
 
45 45
   > svg.feather, > i[data-feather] {
46 46
     @extend .w1, .h1, .ma1, .v-mid, .color-inherit;
@@ -53,7 +53,7 @@ div.provider--status {
53 53
   a { @extend .color-inherit, .link; }
54 54
 
55 55
   &[data-status="pending"] {
56
-    @extend .bg-dark-gray, .near-white;
56
+    @extend .dark-gray, .bg-moon-gray, .b--dark-gray;
57 57
 
58 58
     svg.feather-refresh-cw {
59 59
       @extend .spin;
@@ -61,11 +61,11 @@ div.provider--status {
61 61
   }
62 62
 
63 63
   &[data-status="broken"] {
64
-    @extend .bg-light-red, .near-black, .b--dark-red;
64
+    @extend .dark-red, .b--dark-red, .bg-washed-red;
65 65
   }
66 66
 
67 67
   &[data-status="found"] {
68
-    @extend .bg-light-green, .near-black, .b--dark-green;
68
+    @extend .dark-green, .b--dark-green, .bg-washed-green;
69 69
   }
70 70
 }
71 71
 
@@ -78,7 +78,7 @@ div.provider--info {
78 78
 }
79 79
 
80 80
 div.flash {
81
-  @extend .w-100, .pa2;
81
+  @extend .w-100, .pa2, .bb, .bw1;
82 82
 
83 83
   > p {
84 84
     @extend .mw7, .w-100, .center;
@@ -90,7 +90,11 @@ div.flash {
90 90
 }
91 91
 
92 92
 div.flash--success {
93
-  @extend .bg-light-green, .dark-green;
93
+  @extend .bg-light-green, .dark-green, .b--dark-green;
94
+}
95
+
96
+div.flash--error {
97
+  @extend .bg-washed-red, .dark-red, .b--dark-red;
94 98
 }
95 99
 
96 100
 body >  header {
@@ -112,3 +116,7 @@ body >  header {
112 116
     }
113 117
   }
114 118
 }
119
+
120
+.phx-disconnected {
121
+  button[type=submit], input[type=submit] { @extend .dn; }
122
+}

+ 3
- 2
lib/application.ex View File

@@ -7,10 +7,11 @@ defmodule Fortress.Application do
7 7
     :ok = load_runtime_config()
8 8
 
9 9
     children = [
10
-      supervisor(Fortress.Repo, []),
11 10
       worker(Guardian.DB.Token.SweeperServer, []),
12 11
       worker(Cachex, [:fortress, []]),
13
-      supervisor(Fortress.Web.Endpoint, [])
12
+      {Task.Supervisor, name: Fortress.Workers.Provider},
13
+      supervisor(Fortress.Repo, []),
14
+      supervisor(Fortress.Web.Endpoint, []),
14 15
     ]
15 16
 
16 17
     opts = [strategy: :one_for_one, name: Fortress.Supervisor]

+ 3
- 0
lib/auth/state.ex View File

@@ -28,4 +28,7 @@ defmodule Fortress.Auth.State do
28 28
     {:ok, status} = Cachex.exists?(:fortress, "state:#{token}")
29 29
     status
30 30
   end
31
+
32
+  def generate_token(),
33
+    do: 32 |> :crypto.strong_rand_bytes() |> Base.encode64(case: :lower, padding: false)
31 34
 end

+ 0
- 1
lib/http.ex View File

@@ -11,5 +11,4 @@ defmodule Fortress.Http do
11 11
   plug(Tesla.Middleware.MethodOverride)
12 12
   plug(Tesla.Middleware.Compression, format: "gzip")
13 13
   plug(Tesla.Middleware.Retry, delay: 1000, max_retries: 5)
14
-  plug(Tesla.Middleware.Cache, ttl: :timer.seconds(5))
15 14
 end

+ 0
- 24
lib/jobs/relme/verify.ex View File

@@ -1,24 +0,0 @@
1
-defmodule Fortress.Jobs.RelMe.Verify do
2
-  use Que.Worker, concurrency: 5
3
-  require Logger
4
-
5
-  def perform(site: site, relme: relme) do
6
-    {:ok, rel_me} = Fortress.RelMe.find(site, relme)
7
-
8
-    Fortress.Web.RelMe.update(rel_me)
9
-
10
-    result = case Fortress.RelMe.fetch!(site, relme) do
11
-      {:error, error} ->
12
-        Logger.warn("Failed to determine validity of rel=me for #{relme} to #{site}: #{inspect(error)}.")
13
-        :error
14
-
15
-      {:ok, status: status, site: site, relme: relme} ->
16
-        rel_me = Fortress.RelMe.persist(site, relme, status)
17
-        Fortress.Web.RelMe.update(rel_me)
18
-        :ok
19
-    end
20
-
21
-    Logger.info("Completed verification of #{relme} for #{site}.")
22
-    result
23
-  end
24
-end

+ 0
- 8
lib/jobs/relme/verify_batch.ex View File

@@ -1,8 +0,0 @@
1
-defmodule Fortress.Jobs.RelMe.VerifyBatch do
2
-  use Que.Worker, concurrency: 5
3
-
4
-  def perform(site_url) do
5
-    Fortress.RelMe.find(site_url)
6
-    |> Enum.each(&Que.add(Fortress.Jobs.RelMe.Verify, site: site_url, relme: &1))
7
-  end
8
-end

+ 36
- 44
lib/provider.ex View File

@@ -16,58 +16,50 @@ defmodule Fortress.Provider do
16 16
     end
17 17
   end
18 18
 
19
-  @spec fetch_for(binary()) :: no_return()
20
-  def fetch_for(url) do
19
+  defdelegate fetch_all_for(url), to: Fortress.Repo.SiteProvider
20
+
21
+   defp do_fetch_indieauth_provider(url) do
22
+    case IndieWeb.LinkRel.find(url, "authorization_endpoint") do
23
+      [endpoint] ->
24
+        %Instance{
25
+          platform: :indieauth,
26
+          me: url,
27
+          url: endpoint |> URI.parse(),
28
+          form: :federation,
29
+          status: "found"
30
+        }
31
+
32
+      [] ->
33
+        []
34
+    end
35
+    |> List.wrap()
36
+  end
37
+
38
+  @spec resolve_for(binary()) :: list() | {:error, any() | :no_rel_me_links_found}
39
+  def resolve_for(url) do
21 40
     with(
22 41
       rel_me_urls when is_list(rel_me_urls) <- IndieWeb.LinkRel.find(url, "me"),
23
-      indieauth_endpoint <- IndieWeb.LinkRel.find(url, "authorization_endpoint"),
24
-      providers <-
25
-        rel_me_urls |> Enum.map(&do_resolve_to_provider(&1, url)) |> Enum.reject(&is_nil/1)
42
+      providers <- rel_me_urls |> Enum.map(&do_resolve_to_provider(&1, url))
26 43
     ) do
27
-      indieauth_provider =
28
-        if indieauth_endpoint != [] do
29
-          [
30
-            %Fortress.Provider.Instance{
31
-              platform: :indieauth,
32
-              me: url,
33
-              url: URI.parse(List.first(indieauth_endpoint)),
34
-              form: :federation,
35
-              status: "found"
36
-            }
37
-          ]
38
-        else
39
-          []
40
-        end
41
-
42
-      providers ++ indieauth_provider
44
+      providers |> Enum.concat(do_fetch_indieauth_provider(url)) |> Enum.reject(&is_nil/1)
43 45
     else
44 46
       {:error, _} = error -> error
45
-      [] -> {:error, :no_providers_found}
47
+      [] -> {:error, :no_rel_me_links_found}
46 48
     end
47 49
   end
48 50
 
49
-  @spec check_all_for(binary()) :: no_return()
50
-  def check_all_for(url) do
51
-    url
52
-    |> fetch_for
53
-    |> Enum.each(fn instance ->
54
-      instance
55
-      |> check_for
56
-      |> broadcast
57
-    end)
58
-  end
59
-
60
-  def check_for(instance)
51
+  def update_for(instance)
61 52
 
62
-  def check_for(%Instance{form: :silo} = sioled_instance),
63
-    do: Fortress.Provider.Silo.check_for(sioled_instance)
53
+  def update_for(%Instance{form: :silo} = sioled_instance),
54
+    do: Fortress.Provider.Silo.update_for(sioled_instance)
64 55
 
65
-  def check_for(%Instance{form: :federation} = federated_instance),
66
-    do: Fortress.Provider.Federation.check_for(federated_instance)
56
+  def update_for(%Instance{form: :federation} = federated_instance),
57
+    do: Fortress.Provider.Federation.update_for(federated_instance)
67 58
 
68 59
   def broadcast(instance) do
69 60
     Logger.info("Updating status of provider for user.", instance: instance)
70 61
     Phoenix.PubSub.broadcast(Fortress.PubSub, Instance.topic_for(instance), instance)
62
+    instance
71 63
   end
72 64
 
73 65
   def supported?(platform),
@@ -83,7 +75,7 @@ defmodule Fortress.Provider do
83 75
     do: do_resolve_to_provider(URI.parse(url), me)
84 76
 
85 77
   defp do_resolve_to_provider(%URI{host: "itch.io"} = url, me),
86
-    do: %Fortress.Provider.Instance{
78
+    do: %Instance{
87 79
       platform: :itch,
88 80
       me: me,
89 81
       url: url,
@@ -92,7 +84,7 @@ defmodule Fortress.Provider do
92 84
     }
93 85
 
94 86
   defp do_resolve_to_provider(%URI{host: "github.com"} = url, me),
95
-    do: %Fortress.Provider.Instance{
87
+    do: %Instance{
96 88
       platform: :github,
97 89
       me: me,
98 90
       url: url,
@@ -101,7 +93,7 @@ defmodule Fortress.Provider do
101 93
     }
102 94
 
103 95
   defp do_resolve_to_provider(%URI{host: "gitlab.com"} = url, me),
104
-    do: %Fortress.Provider.Instance{
96
+    do: %Instance{
105 97
       platform: :gitlab,
106 98
       me: me,
107 99
       url: url,
@@ -110,7 +102,7 @@ defmodule Fortress.Provider do
110 102
     }
111 103
 
112 104
   defp do_resolve_to_provider(%URI{host: "patreon.com"} = url, me),
113
-    do: %Fortress.Provider.Instance{
105
+    do: %Instance{
114 106
       platform: :patreon,
115 107
       me: me,
116 108
       url: url,
@@ -119,7 +111,7 @@ defmodule Fortress.Provider do
119 111
     }
120 112
 
121 113
   defp do_resolve_to_provider(%URI{host: "keybase.io"} = url, me),
122
-    do: %Fortress.Provider.Instance{
114
+    do: %Instance{
123 115
       platform: :keybase,
124 116
       me: me,
125 117
       url: url,
@@ -128,7 +120,7 @@ defmodule Fortress.Provider do
128 120
     }
129 121
 
130 122
   defp do_resolve_to_provider(%URI{host: "medium.com"} = url, me),
131
-    do: %Fortress.Provider.Instance{
123
+    do: %Instance{
132 124
       platform: :medium,
133 125
       me: me,
134 126
       url: url,

+ 3
- 3
lib/provider/federation.ex View File

@@ -1,7 +1,7 @@
1 1
 defmodule Fortress.Provider.Federation do
2
-  def check_for(instance)
2
+  def update_for(instance)
3 3
 
4
-  def check_for(%Fortress.Provider.Instance{platform: :indieauth} = instance) do
4
+  def update_for(%Fortress.Provider.Instance{platform: :indieauth} = instance) do
5 5
     instance_url = URI.to_string(instance.url)
6 6
     resolved_mes = IndieWeb.LinkRel.find(instance.me, "authorization_endpoint")
7 7
 
@@ -12,7 +12,7 @@ defmodule Fortress.Provider.Federation do
12 12
     end
13 13
         %{instance | status: status}
14 14
   end
15
-  def check_for(instance), do: %{instance | status: "broken"}
15
+  def update_for(instance), do: %{instance | status: "broken"}
16 16
 
17 17
   def supported(), do: ~w(mastodon pixelfed pleroma indieauth)a
18 18
 end

+ 3
- 3
lib/provider/silo.ex View File

@@ -1,8 +1,8 @@
1 1
 defmodule Fortress.Provider.Silo do
2 2
   @friendly_silo ~w(github gitlab itch)a
3
-  def check_for(instance)
3
+  def update_for(instance)
4 4
 
5
-  def check_for(%Fortress.Provider.Instance{platform: platform} = instance)
5
+  def update_for(%Fortress.Provider.Instance{platform: platform} = instance)
6 6
       when platform in @friendly_silo do
7 7
     instance_url = URI.to_string(instance.url)
8 8
     resolved_mes = IndieWeb.LinkRel.find(instance_url, "me")
@@ -15,7 +15,7 @@ defmodule Fortress.Provider.Silo do
15 15
         %{instance | status: status}
16 16
   end
17 17
 
18
-  def check_for(instance), do: %{instance | status: "broken"}
18
+  def update_for(instance), do: %{instance | status: "broken"}
19 19
 
20 20
   def supported(), do: @friendly_silo
21 21
 end

+ 0
- 222
lib/relme.ex View File

@@ -1,222 +0,0 @@
1
-defmodule Fortress.RelMe do
2
-  defstruct ~w(uri site status checked_at supported name)a
3
-
4
-  @type t :: %__MODULE__{
5
-          uri: URI.t(),
6
-          site: Fortress.Repo.Site.t(),
7
-          status: atom(),
8
-          checked_at: DateTime.t(),
9
-          supported: boolean(),
10
-          name: binary()
11
-        }
12
-
13
-  @supported_domains ~w(github.com gitlab.com itch.io fortress.black.af keybase.io)
14
-  @supported_schemes ~w(mailto)
15
-
16
-  require Logger
17
-
18
-  def extract(uri) do
19
-    Logger.info("Obtaining MF2 info for #{uri}...")
20
-
21
-    case Fortress.MF2.find(uri) do
22
-      :error ->
23
-        Logger.info("No MF2 info for #{uri}...")
24
-        nil
25
-
26
-      mf2 ->
27
-        rels = mf2 |> Map.get(:rels, %{"me" => []}) |> Map.get("me", [])
28
-        Logger.info("Obtained #{length(rels)} rel-me links for #{uri}.")
29
-        rels
30
-    end
31
-  end
32
-
33
-  def find(uri) do
34
-    Logger.info("Looking up rel-me links for #{uri}...")
35
-
36
-    case Cachex.get(:fortress, "rel_me:#{uri}") do
37
-      {:ok, rels} when rels != [] and is_list(rels) ->
38
-        Logger.info("Pulled #{length(rels)} rel-mes for #{uri}.")
39
-        rels
40
-
41
-      {:ok, rels} when rels == nil or rels == [] ->
42
-        Logger.info("No rel-me links cached for #{uri}.")
43
-        rels = extract(uri)
44
-        Cachex.put(:fortress, "rel_me:#{uri}", rels)
45
-        []
46
-    end
47
-  end
48
-
49
-  def supported?(uri)
50
-
51
-  def supported?(%URI{scheme: "http", host: host}), do: Enum.member?(@supported_domains, host)
52
-  def supported?(%URI{scheme: "https", host: host}), do: Enum.member?(@supported_domains, host)
53
-  def supported?(%URI{scheme: scheme}), do: Enum.member?(@supported_schemes, scheme)
54
-  def supported?(uri) when is_binary(uri), do: supported?(URI.parse(uri))
55
-  def supported?(%__MODULE__{uri: uri}), do: supported?(uri)
56
-  def supported?(_), do: false
57
-
58
-  def name(uri)
59
-  def name(%URI{scheme: "mailto"}), do: "E-Mail"
60
-  def name(%URI{scheme: "sms"}), do: "SMS / Text"
61
-  def name(%URI{host: "github.com"}), do: "GitHub"
62
-  def name(%URI{host: "gitlab.com"}), do: "GitLab"
63
-  def name(%URI{host: "twitter.com"}), do: "Twitter"
64
-  def name(%URI{host: "linkedin.com"}), do: "LinkedIn"
65
-  def name(%URI{host: "patreon.com"}), do: "Patreon"
66
-  def name(%URI{host: "keybase.io"}), do: "Keybase"
67
-  def name(%URI{host: "google.com"}), do: "Google"
68
-  def name(%URI{host: "youtube.com"}), do: "YouTube"
69
-  def name(%URI{host: "steamcommunity.com"}), do: "Steam"
70
-  def name(%URI{host: "stackoverflow.com"}), do: "StackOverflow"
71
-  def name(%URI{host: "twitch.tv"}), do: "Twitch"
72
-  def name(%URI{host: "reddit.com"}), do: "Reddit"
73
-  def name(%URI{host: "open.spotify.com"}), do: "Spotify"
74
-  def name(%URI{host: "venmo.com"}), do: "Venmo"
75
-  def name(%URI{host: "last.fm"}), do: "Last.FM"
76
-  def name(%URI{host: "cash.me"}), do: "Square Cash"
77
-  def name(%URI{host: "paypal.me"}), do: "Paypal"
78
-  def name(%URI{host: "account.xbox.com"}), do: "XBox"
79
-  def name(%URI{host: "flickr.com"}), do: "Flickr"
80
-  def name(%URI{host: "foursquare.com"}), do: "Foursquare"
81
-  def name(%URI{host: "goodreads.com"}), do: "Goodreads"
82
-  def name(%URI{host: "facebook.com"}), do: "Facebook"
83
-  def name(%URI{host: "m.me"}), do: "Facebook Messenger"
84
-  def name(%URI{host: "instagram.com"}), do: "Instagram"
85
-  def name(%URI{host: "slideshare.net"}), do: "Slideshare"
86
-  def name(%URI{host: "" <> "eventbrite.com"}), do: "Eventbrite"
87
-  def name(%URI{host: "w3.org"}), do: ""
88
-  def name(%URI{host: "vim.org"}), do: "Vim"
89
-  def name(%URI{host: "www." <> host}), do: name(%URI{host: host})
90
-  def name(uri) when is_binary(uri), do: name(URI.parse(uri))
91
-  def name(%URI{host: host}), do: host
92
-
93
-  def persist(site, relme, status \\ :inactive) do
94
-    dt = DateTime.utc_now()
95
-    {:ok, site} = Fortress.Site.resolve(site)
96
-    Logger.info("Updating #{site.url}'s rel-me of #{relme} to have a status of #{status}.")
97
-
98
-    relme_to_site = %__MODULE__{
99
-      uri: URI.parse(relme),
100
-      site: site,
101
-      status: status,
102
-      checked_at: dt,
103
-      supported: supported?(relme),
104
-      name: name(relme)
105
-    }
106
-
107
-    Cachex.put(
108
-      :fortress,
109
-      do_make_site_relme_key(site.url, relme),
110
-      relme_to_site,
111
-      ttl: :timer.hours(24 * 7 * 30)
112
-    )
113
-
114
-    relme_to_site
115
-  end
116
-
117
-  def flush(site, relme) do
118
-    Logger.info("Deleting informaton about #{relme} for #{site}...")
119
-    {:ok, _} = Cachex.del(:fortress, do_make_site_relme_key(site, relme))
120
-    :ok
121
-  end
122
-
123
-  def resolve(site, relme) do
124
-    with(
125
-      {:ok, result} <- fetch!(site, relme),
126
-      {:ok, _} <- persist(site, relme, result[:status])
127
-    ) do
128
-      find(site, relme)
129
-    else
130
-      {:ok, %__MODULE__{}} = resp -> resp
131
-      {:error, _} = resp -> resp
132
-    end
133
-  end
134
-
135
-  defp do_make_site_relme_key(site, relme) do
136
-    token =
137
-      [site, relme]
138
-      |> Enum.join("=")
139
-      |> (fn str -> :crypto.hash(:sha256, str) end).()
140
-      |> Base.encode16(case: :lower, padding: false)
141
-
142
-    "relme:#{token}"
143
-  end
144
-
145
-  def status_message(relme)
146
-
147
-  def status_message(%__MODULE__{supported: true, status: :active}),
148
-    do: "Fortress can authenticate you using this option."
149
-
150
-  def status_message(%__MODULE__{supported: true, status: status}) when status in ~w(pending inactive)a,
151
-      do: "Fortress is still scanning the remote site."
152
-
153
-  def status_message(%__MODULE__{supported: true, status: :active}),
154
-    do: "Neither site is advertising links to another."
155
-
156
-  def status_message(%__MODULE__{supported: true, status: :remote_invalid}),
157
-    do: "The remote site is not pointing to your site."
158
-
159
-  def status_message(%__MODULE__{supported: true, status: :local_invalid}),
160
-    do: "Your site is not pointing to the remote site."
161
-
162
-  def status_message(%__MODULE__{supported: false}), do: "Currently not supported."
163
-  def status_message(_), do: "Unknown status."
164
-
165
-  def find(site, relme) do
166
-    case Cachex.get(:fortress, do_make_site_relme_key(site, relme)) do
167
-      {:ok, nil} ->
168
-        {:ok, persist(site, relme, :inactive)}
169
-
170
-      {:ok, val} ->
171
-        {:ok, val}
172
-    end
173
-  end
174
-
175
-  def has?(site, relme) do
176
-    {:ok, nil} != Cachex.get(:fortress, do_make_site_relme_key(site, relme))
177
-  end
178
-
179
-  def fetch(site, relme) do
180
-    Que.add(Fortress.Jobs.RelMe.Verify, site: site, relme: relme)
181
-  end
182
-
183
-  def fetch!(site, relme) do
184
-    with(
185
-      site_mf2 when is_map(site_mf2) <- Fortress.MF2.find(site),
186
-      relme_mf2 when is_map(relme_mf2) <- Fortress.MF2.find(relme)
187
-    ) do
188
-      rels_site =
189
-        do_get_mes_from_mf2(site_mf2)
190
-        |> Enum.map(&URI.to_string(URI.parse(&1)))
191
-        |> Enum.map(&String.trim_trailing(&1, "/"))
192
-        |> Enum.sort()
193
-
194
-      rels_relme =
195
-        do_get_mes_from_mf2(relme_mf2)
196
-        |> Enum.map(&URI.to_string(URI.parse(&1)))
197
-        |> Enum.map(&String.trim_trailing(&1, "/"))
198
-        |> Enum.sort()
199
-
200
-      site = String.trim_trailing(site, "/")
201
-      relme = String.trim_trailing(relme, "/")
202
-      remote_valid = site in rels_relme
203
-      local_valid = relme in rels_site
204
-
205
-      status =
206
-        cond do
207
-          !remote_valid && !local_valid -> :inactive
208
-          remote_valid && !local_valid -> :local_invalid
209
-          !remote_valid && local_valid -> :remote_invalid
210
-          true -> :active
211
-        end
212
-
213
-      {:ok, status: status, site: site, relme: relme}
214
-    else
215
-      _ -> {:error, :failed_to_fetch_mf2}
216
-    end
217
-  end
218
-
219
-  defp do_get_mes_from_mf2(mf2) do
220
-    mf2 |> Map.get(:rels, %{"me" => []}) |> Map.get("me", [])
221
-  end
222
-end

+ 6
- 3
lib/repo/site.ex View File

@@ -2,15 +2,15 @@ defmodule Fortress.Repo.Site do
2 2
   use Ecto.Schema
3 3
   use GuardianTrackable.Schema
4 4
   import Ecto.Changeset
5
+  require Protocol
5 6
 
6 7
   @required_keys ~w(url)a
7 8
   @optional_keys ~w(photo name)a
8
-  @primary_key {:id, :binary_id, autogenerate: true}
9
-  @foreign_key_type :binary_id
9
+  @primary_key {:url, :string, autogenerate: false}
10 10
   schema "sites" do
11
-    field(:url, :string)
12 11
     field(:photo, :string)
13 12
     field(:name, :string)
13
+    has_many(:providers, Fortress.Repo.SiteProvider)
14 14
 
15 15
     guardian_trackable()
16 16
     timestamps()
@@ -43,4 +43,7 @@ defmodule Fortress.Repo.Site do
43 43
     changeset(%__MODULE__{}, hcard)
44 44
     |> Fortress.Repo.insert()
45 45
   end
46
+Protocol.derive(Jason.Encoder, __MODULE__, only: ~w(url)a)
46 47
 end
48
+
49
+

+ 88
- 0
lib/repo/site_provider.ex View File

@@ -0,0 +1,88 @@
1
+defmodule Fortress.Repo.SiteProvider do
2
+  use Ecto.Schema
3
+  import Ecto.Changeset
4
+  require Ecto.Query
5
+  require Logger
6
+
7
+  @primary_key {:url, :string, autogenerate: false}
8
+  @required_keys ~w(url provider site)a
9
+  @optional_keys ~w(last_used_at status)a
10
+  @foreign_key_type :string
11
+  schema "site_providers" do
12
+    field(:last_used_at, :utc_datetime_usec)
13
+    field(:provider, :string)
14
+    field(:status, :string, default: "pending")
15
+    belongs_to(:owning_site, Fortress.Repo.Site, foreign_key: :site)
16
+
17
+    timestamps()
18
+  end
19
+
20
+  @doc false
21
+  def changeset(site_provider, attrs) do
22
+    site_provider
23
+    |> cast(
24
+      attrs,
25
+      @required_keys ++
26
+        @optional_keys
27
+    )
28
+    |> validate_required(@required_keys)
29
+    |> unique_constraint(:url)
30
+    |> foreign_key_constraint(:site, name: :site_providers_site_fkey)
31
+  end
32
+
33
+  def refresh(instance) do
34
+    case Fortress.Repo.get(__MODULE__, URI.to_string(instance.url)) do
35
+      %__MODULE__{} = record ->
36
+        record
37
+        |> Ecto.Changeset.change(%{status: instance.status})
38
+        |> Fortress.Repo.update()
39
+
40
+      nil ->
41
+        %__MODULE__{
42
+          url: URI.to_string(instance.url),
43
+          provider: Atom.to_string(instance.platform),
44
+          status: instance.status,
45
+          site: instance.me
46
+        }
47
+        |> Ecto.Changeset.change()
48
+        |> Fortress.Repo.insert()
49
+    end
50
+  end
51
+
52
+  def fetch_all_for(url) do
53
+    __MODULE__
54
+    |> Ecto.Query.where(site: ^url)
55
+    |> Fortress.Repo.all()
56
+    |> Enum.reject(&(&1.url == internal_endpoint()))
57
+    |> Enum.map(&to_instance/1)
58
+  end
59
+
60
+  def internal_endpoint(), do: Fortress.Web.Router.Helpers.indie_auth_url(Fortress.Web.Endpoint, :authorize)
61
+
62
+  def uses_system?(url) do
63
+    endpoint = internal_endpoint()
64
+    __MODULE__
65
+    |> Ecto.Query.where(site: ^url, url: ^endpoint)
66
+    |> Fortress.Repo.exists?
67
+  end
68
+
69
+  def to_instance(%__MODULE{} = record) do
70
+    %Fortress.Provider.Instance{
71
+      platform: String.to_existing_atom(record.provider),
72
+      status: record.status,
73
+      url: URI.parse(record.url),
74
+      me: record.site
75
+    }
76
+  end
77
+
78
+  def bump_last_use(site, provider) do
79
+    case Fortress.Repo.get_by(__MODULE__, site: site, provider: provider) do
80
+      %__MODULE__{} = record ->
81
+        record
82
+        |> Ecto.Changeset.change(%{last_used_at: DateTime.utc_now})
83
+        |> Fortress.Repo.update
84
+      _ ->
85
+        Logger.warn("No provider stored to be bumped.", provider: provider, site: site)
86
+    end
87
+  end
88
+end

+ 25
- 0
lib/worker/provider.ex View File

@@ -0,0 +1,25 @@
1
+defmodule Fortress.Workers.Provider do
2
+  def update_records(url) do
3
+    Task.Supervisor.start_child(
4
+      __MODULE__,
5
+      fn ->
6
+        url
7
+        |> Fortress.Provider.resolve_for()
8
+        |> Enum.each(&(check_instance(&1)))
9
+      end,
10
+      restart: :transient
11
+    )
12
+  end
13
+
14
+  def check_instance(instance) do
15
+    Task.Supervisor.start_child(
16
+      __MODULE__,
17
+      fn ->
18
+    instance
19
+          |> Fortress.Provider.update_for()
20
+          |> Fortress.Provider.broadcast()
21
+          |> Fortress.Repo.SiteProvider.refresh end,
22
+      restart: :transient
23
+    )
24
+  end
25
+end

+ 2
- 2
mix.lock View File

@@ -56,7 +56,7 @@
56 56
   "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
57 57
   "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm"},
58 58
   "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
59
-  "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
59
+  "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
60 60
   "tesla": {:git, "https://github.com/jalcine/tesla", "493f2f9394360c1e782d3e5856976f25ecaa83f6", [branch: "jalcine/check-regex-run-results"]},
61 61
   "tesla_cache": {:hex, :tesla_cache, "0.1.2", "ef5a23c10c146fffc3c2c2a1e8fbff45a64fd28447b73112d955c92e7bf0b21a", [:mix], [{:cachex, "~> 2.1", [hex: :cachex, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},
62 62
   "tesla_request_id": {:hex, :tesla_request_id, "0.2.0", "9745364ed3c850864fb2744daefb9b7104fe7f1efcf34eb350c61da805de2a46", [:mix], [{:tesla, "~> 1.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},
@@ -64,7 +64,7 @@
64 64
   "tzdata": {:hex, :tzdata, "1.0.2", "6c4242c93332b8590a7979eaf5e11e77d971e579805c44931207e32aa6ad3db1", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
65 65
   "ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
66 66
   "ueberauth_github": {:hex, :ueberauth_github, "0.7.0", "637067c5500f7b13c18caca3db66d09eba661524e0d0e9518b54151e99484bad", [:mix], [{:oauth2, "~> 0.9", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.4", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm"},
67
-  "ueberauth_indieauth": {:git, "https://git.jacky.wtf/indieweb/ueberauth_indieauth", "73359386d5cadb7e7625084f8f520eb3675b5e09", [branch: :develop]},
67
+  "ueberauth_indieauth": {:git, "https://git.jacky.wtf/indieweb/ueberauth_indieauth", "fdbdd984d498ec7976501d584d17bd3c0bfd101f", [branch: :develop]},
68 68
   "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
69 69
   "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},
70 70
 }

+ 1
- 3
priv/repo/migrations/20190302070000_create_sites.exs View File

@@ -3,13 +3,11 @@ defmodule Fortress.Repo.Migrations.CreateSites do
3 3
 
4 4
   def change do
5 5
     create table(:sites, primary_key: false) do
6
-      add :id, :binary_id, primary_key: true
7
-      add :url, :string
6
+      add :url, :string, primary_key: true
8 7
 
9 8
       timestamps()
10 9
     end
11 10
 
12 11
     create unique_index(:sites, [:url])
13
-    create index(:sites, [:id])
14 12
   end
15 13
 end

+ 17
- 0
priv/repo/migrations/20191120013944_create_site_providers.exs View File

@@ -0,0 +1,17 @@
1
+defmodule Fortress.Repo.Migrations.CreateSiteProviders do
2
+  use Ecto.Migration
3
+
4
+  def change do
5
+    create table(:site_providers, primary_key: false) do
6
+      add :url, :string, primary_key: true
7
+      add :provider, :string
8
+      add :last_used_at, :utc_datetime_usec
9
+      add :site, references(:sites, on_delete: :nothing, type: :string, column: :url)
10
+
11
+      timestamps()
12
+    end
13
+
14
+    create unique_index(:site_providers, [:url])
15
+    create index(:site_providers, [:site])
16
+  end
17
+end

+ 9
- 0
priv/repo/migrations/20191120022406_add_column_status_to_site_providers.exs View File

@@ -0,0 +1,9 @@
1
+defmodule Fortress.Repo.Migrations.AddColumnStatusToSiteProviders do
2
+  use Ecto.Migration
3
+
4
+  def change do
5
+    alter table(:site_providers) do
6
+      add :status, :string, default: "pending", null: false
7
+    end
8
+  end
9
+end

+ 259
- 0
web/controllers/indie/auth_controller.ex View File

@@ -0,0 +1,259 @@
1
+defmodule Fortress.Web.Indie.AuthController do
2
+  use Fortress.Web, :controller
3
+  alias IndieWeb.Auth.{Code, Scope}
4
+  alias IndieWeb.Cache
5
+  require Logger
6
+
7
+  def authorize(conn, params)
8
+
9
+  def authorize(conn, %{"client_id" => client_id} = params) do
10
+    with(
11
+      {:ok, app} <- get_app_info(client_id),
12
+      {:ok, redirect_uri} <- obtain_redirect_uri(params, app),
13
+      :ok <- assert_matching_domains(client_id, redirect_uri),
14
+      state when is_binary(state) <- Map.get(params, "state", {:error, :no_state})
15
+    ) do
16
+      scope = Map.get(params, "scope", ~w(read))
17
+      response_type = Map.get(params, "response_type", "id")
18
+      was_remembered = remembered?(client_id, scope)
19
+      Logger.debug("Was this client remembered in the past for logging in?", was_remembered: was_remembered, client_id: client_id)
20
+
21
+      if response_type == "code" && was_remembered do
22
+        Logger.info("Passing off authorization to be completed.")
23
+        complete_authorization(conn, params)
24
+      else
25
+        conn
26
+        |> render("authorize.html", %{
27
+          app: app,
28
+          redirect_uri: redirect_uri,
29
+          response_type: response_type
30
+        })
31
+      end
32
+    else
33
+      {:error, :redirect_uri_not_provided} ->
34
+        conn
35
+        |> put_flash(
36
+          :error,
37
+          "Could not find a valid redirect URI for calling application."
38
+        )
39
+        |> put_status(:bad_request)
40
+        |> render("error.html")
41
+
42
+      {:error, :mf2_data_fetch_failed} ->
43
+      conn
44
+      |> put_status(:bad_request)
45
+      |> put_flash(
46
+        :error,
47
+        "Could not find h-app info from requesting service."
48
+      )
49
+      |> render("error.html")
50
+
51
+      {:error, :no_state} ->
52
+      conn
53
+      |> put_status(:bad_request)
54
+      |> put_flash(:error, "No 'state' value was provided.")
55
+      |> render("error.html")
56
+
57
+      {:error, error} ->
58
+      conn
59
+      |> put_status(:unprocessable_entity)
60
+      |> put_flash(:error, "Redirect URI was invalid: #{error}")
61
+      |> render("error.html")
62
+    end
63
+  end
64
+
65
+  def authorize(conn, _params) do
66
+    conn
67
+    |> put_flash(:error, "Couldn't understand this request for authorization.")
68
+    |> put_status(:bad_request)
69
+    |> render("error.html")
70
+  end
71
+
72
+  def complete_authorization(
73
+    conn,
74
+    %{
75
+      "client_id" => client_id,
76
+      "redirect_uri" => redirect_uri
77
+    } = params
78
+  ) do
79
+    scope = Map.get(params, "scope", "read")
80
+    state = Map.get(params, "state", nil)
81
+
82
+    if is_nil(state) do
83
+      Logger.warn(
84
+        "The request to complete the authorization failed due to the lack of a 'state' parameter."
85
+      )
86
+
87
+      conn
88
+      |> Explode.bad_request("No 'state' parameter' was provided.")
89
+    else
90
+      scope_str =
91
+        cond do
92
+          is_list(scope) -> Scope.to_string(scope)
93
+          is_binary(scope) -> scope
94
+        end
95
+
96
+        code = Code.generate(client_id, redirect_uri, %{"scope" => scope_str})
97
+
98
+        if Map.get(params, "remember_me", "off") == "on" do
99
+          {:ok, "on"} = remember(client_id, scope)
100
+        end
101
+
102
+        with :ok <- Scope.persist!(code, scope),
103
+             :ok <- Code.persist(code, client_id, redirect_uri, %{"scope" => scope_str}) do
104
+          signed_uri =
105
+            URI.merge(
106
+              URI.parse(redirect_uri),
107
+              "?#{
108
+                URI.encode_query(%{
109
+                  code: code,
110
+                  state: state
111
+                })
112
+              }"
113
+            )
114
+
115
+          Logger.debug("Sending client to #{signed_uri}")
116
+
117
+          conn
118
+          |> redirect(external: signed_uri |> URI.to_string())
119
+        else
120
+          {:error, error} ->
121
+            Logger.warn("Failed to authorize client #{client_id}.", error: inspect(error))
122
+
123
+            conn
124
+            |> put_status(:bad_request)
125
+            |> put_flash(:error, "Ran into a problem authorizing this client.")
126
+            |> render("error.html")
127
+        end
128
+    end
129
+  end
130
+
131
+  def complete_authorization(conn, _) do
132
+    conn
133
+    |> put_status(:bad_request)
134
+    |> json(%{
135
+      error: :invalid_params,
136
+      error_description: "Invalid attempt to complete authorization due to missing parameters."
137
+    })
138
+  end
139
+
140
+  def verify_authorization_code(
141
+        conn,
142
+        %{
143
+          "code" => code,
144
+          "client_id" => client_id,
145
+          "redirect_uri" => redirect_uri
146
+        } = params
147
+      ) do
148
+        scope = Map.get(params, "scope", ~w(read)) |> List.wrap
149
+    scope_str = Scope.to_string(scope)
150
+
151
+    Logger.debug("Beginning authentication request.",
152
+      code: code,
153
+      client_id: client_id,
154
+      redirect_uri: redirect_uri,
155
+      scope: scope_str
156
+    )
157
+
158
+    case IndieWeb.Auth.Code.verify(code, client_id, redirect_uri, %{"scope" => scope_str}) do
159
+      {:error, error} ->
160
+        Logger.warn("Failed to authenticate this site.",
161
+          client_id: client_id,
162
+          redirect_uri: redirect_uri,
163
+          scope: scope_str,
164
+          error: error
165
+        )
166
+
167
+        conn
168
+        |> put_status(:unauthorized)
169
+        |> json(%{
170
+          error: :code_failure,
171
+          error_description: error
172
+        })
173
+
174
+      :ok ->
175
+        Logger.info("We got a successful login request.",
176
+          client_id: client_id,
177
+          scope: scope_str,
178
+          redirect_uri: redirect_uri
179
+        )
180
+
181
+        conn
182
+        |> put_status(:ok)
183
+        |> json(%{
184
+          me: params["me"]
185
+        })
186
+    end
187
+  end
188
+
189
+  defp get_app_info(client_id) do
190
+    case IndieWeb.App.retrieve(client_id) do
191
+      {:ok, _} = app ->
192
+        app
193
+
194
+      {:error, error} ->
195
+        Logger.warn("Failed to get structured info for the app: #{error}.")
196
+
197
+        {:ok,
198
+          %{
199
+            "logo" => Koype.Web.LayoutView.asset_path_for(:core, "images/logo/icon.svg"),
200
+            "name" => URI.parse(client_id).host,
201
+            "url" => client_id
202
+          }}
203
+    end
204
+  end
205
+
206
+  defp obtain_redirect_uri(params, mf2_app) do
207
+    cond do
208
+      Map.has_key?(params, "redirect_uri") ->
209
+        {:ok, params["redirect_uri"]}
210
+
211
+      Map.has_key?(mf2_app, :redirect_uri) ->
212
+        {:ok, mf2_app[:redirect_uri]}
213
+
214
+      true ->
215
+        {:error, :redirect_uri_not_provided}
216
+    end
217
+  end
218
+
219
+  defp assert_matching_domains(client_id, redirect_uri) do
220
+    parsed_client_id = URI.parse(client_id)
221
+    parsed_redirect_uri = URI.parse(redirect_uri)
222
+
223
+    cond do
224
+      parsed_client_id.host != parsed_redirect_uri.host ->
225
+        {:error, :host_mismatch}
226
+
227
+      parsed_client_id.port != parsed_redirect_uri.port ->
228
+        {:error, :port_mismatch}
229
+
230
+      parsed_client_id.scheme != parsed_redirect_uri.scheme ->
231
+        {:error, :scheme_mismatch}
232
+
233
+      true ->
234
+        :ok
235
+    end
236
+  end
237
+
238
+  defp remember_key_func({client_id, scope}) when is_binary(scope),
239
+  do: remember_key_func({client_id, scope |> String.split()})
240
+
241
+  defp remember_key_func({client_id, scope}) when is_list(scope) do
242
+    "#{client_id |> URI.parse() |> Map.get(:host)}_#{scope |> Enum.sort() |> Enum.join("#")}"
243
+  end
244
+
245
+  defp remember_key_func({client_id, scope}) when is_nil(scope),
246
+  do: "#{client_id |> URI.parse() |> Map.get(:host)}"
247
+
248
+  defp remembered?(client_id, scope) do
249
+    case Cache.get(remember_key_func({client_id, scope})) do
250
+      {:error, _} -> false
251
+      _ -> true
252
+    end
253
+  end
254
+
255
+  defp remember(client_id, scope) do
256
+    Cache.delete(remember_key_func({client_id, scope}))
257
+    Cache.set(remember_key_func({client_id, scope}))
258
+  end
259
+end

+ 133
- 0
web/controllers/indie/token_controller.ex View File

@@ -0,0 +1,133 @@
1
+defmodule Fortress.Web.Indie.TokenApiController do
2
+  use Fortress.Web, :controller
3
+  alias IndieWeb.Auth.{Code, Scope}
4
+  require Logger
5
+
6
+  defp do_extract_token_from_request(conn) do
7
+    case AuthHelper.indie_current_token(conn) do
8
+      nil -> {:error, :no_token_found}
9
+      {:ok, token} -> {:ok, token}
10
+    end
11
+  end
12
+
13
+  defp assert_me(me) do
14
+    fetched_uri = URI.parse(me)
15
+    local_uri = URI.parse(Fortress.host())
16
+
17
+    if local_uri.host == fetched_uri.host do
18
+      :ok
19
+    else
20
+      {:error, :me_not_valid}
21
+    end
22
+  end
23
+
24
+  defp do_fetch_scope(code) do
25
+    case Scope.get(code) do
26
+      nil -> {:error, :no_scope}
27
+      scope when is_list(scope) and scope != [] -> {:ok, Scope.to_string(scope)}
28
+    end
29
+  end
30
+
31
+  def create(conn, params)
32
+
33
+  def create(
34
+        conn,
35
+        %{"code" => code, "client_id" => client_id, "redirect_uri" => redirect_uri, "me" => me}
36
+      ) do
37
+    with(
38
+      :ok <- assert_me(me),
39
+      {:ok, scope_str} <- do_fetch_scope(code),
40
+      :ok <- Code.verify(code, client_id, redirect_uri, %{"scope" => scope_str})
41
+    ) do
42
+      token = indie_sign_in(client_id, scope_str)
43
+
44
+      Logger.info("Created valid token.", client_id: client_id)
45
+
46
+      conn
47
+      |> put_resp_header("authorization", "Bearer #{token}")
48
+      |> json(%{
49
+        access_token: token,
50
+        me: Fortress.host(),
51
+        scope: scope_str,
52
+        token_type: :bearer
53
+      })
54
+    else
55
+      {:error, :me_not_valid} ->
56
+        scope = Scope.get(code)
57
+        scope_str = Scope.to_string(scope)
58
+        Code.destroy(client_id, redirect_uri, %{"scope" => scope_str})
59
+
60
+        conn
61
+        |> Explode.bad_request("The 'me' provided is incorrect for this server.")
62
+
63
+      {:error, :code_not_found} ->
64
+        Logger.debug("Code provided was not found.")
65
+
66
+        conn
67
+        |> Explode.forbidden("The provided code was not valid.")
68
+
69
+      {:error, :invalid_code} ->
70
+        Logger.debug("Code provided was invalid.")
71
+
72
+        conn
73
+        |> Explode.forbidden("The provided code was not valid.")
74
+
75
+      {:error, :mismatched_client_id_for_code} ->
76
+        Logger.debug("Code provided was mismatched on the client_id.")
77
+
78
+        conn
79
+        |> Explode.forbidden("The provided code did not match the provided client ID.")
80
+
81
+      {:error, :mismatched_redirect_uri_for_code} ->
82
+        Logger.debug("Code provided was mismatched on the redirect_uri.")
83
+
84
+        conn
85
+        |> Explode.forbidden("The provided code did not match the provided redirect URI.")
86
+
87
+      {:error, :no_scope} ->
88
+        Logger.debug("Code provided has no scope defined.")
89
+
90
+        conn
91
+        |> Explode.forbidden("The provided code did not have any scopes.")
92
+    end
93
+  end
94
+
95
+  def create(conn, %{"action" => "revoke"} = params) do
96
+    AuthHelper.revoke_token(params["token"])
97
+    Logger.info("Token was successfully revoked.")
98
+
99
+    conn
100
+    |> put_status(:ok)
101
+    |> text("ok")
102
+  end
103
+
104
+  def create(conn, _params) do
105
+    conn
106
+    |> Explode.bad_request("The request made was not understood by the server.")
107
+  end
108
+
109
+  def validate(conn, _params) do
110
+    with(
111
+      {:ok, token} <- do_extract_token_from_request(conn),
112
+      {:ok, client_id} <- AuthHelper.indie_current_resource(conn),
113
+      {:ok, components} <- Fortress.Guardian.decode_and_verify(token, %{"typ" => "access"}),
114
+      %{"scope" => scope} <- Map.take(components, ~w(id scope))
115
+    ) do
116
+      Logger.info("Successfully validated code.", scope: scope, client_id: client_id)
117
+
118
+      conn
119
+      |> json(%{
120
+        client_id: client_id,
121
+        me: Fortress.host(),
122
+        scope: scope |> Scope.from_string() |> Scope.to_string()
123
+      })
124
+    else
125
+      {:error, error} ->
126
+        Logger.warn("Failed to validate code.", error: error)
127
+
128
+        conn
129
+        |> put_resp_content_type("application/json")
130
+        |> Explode.bad_request(error)
131
+    end
132
+  end
133
+end

+ 19
- 1
web/controllers/oauth2.ex View File

@@ -4,8 +4,23 @@ defmodule Fortress.Web.OAuth2Controller do
4 4
 
5 5
   alias Ueberauth.Strategy.Helpers
6 6
 
7
+  def request(%{assigns: %{ueberauth_failure: failure}} = conn, params) do
8
+    platform = params["platform"]
9
+    # TODO: Destroy state.
10
+
11
+    conn
12
+    |> put_flash(:error, "The attempt to sign in using #{platform} failed.")
13
+    |> render("request-error.html", errors: failure.errors)
14
+  end
15
+  def request(conn, params) do
16
+    # TODO: Destroy state.
17
+    conn
18
+    |> redirect(to: "/")
19
+  end
20
+
7 21
   def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
8 22
     # TODO: Navigate to callback.
23
+    # TODO: Destroy state.
9 24
     conn
10 25
     |> put_flash(:error, "Failed to authenticate.")
11 26
     |> redirect(to: "/")
@@ -18,14 +33,17 @@ defmodule Fortress.Web.OAuth2Controller do
18 33
       {:ok, hcard} <- IndieWeb.HCard.fetch_representative(user_url),
19 34
       {:ok, site} <- Fortress.Repo.Site.upsert(hcard)
20 35
     ) do
36
+    # TODO: Destroy state.
37
+      Fortress.Repo.SiteProvider.bump_last_use(user_url, Atom.to_string(auth.provider))
21 38
       conn
22 39
       |> delete_session(:requesting_user)
23 40
       |> delete_session(:requested_profile)
24 41
       |> put_flash(:success, "We got you, son.")
25
-      |> Fortress.Guardian.Plug.sign_in(site)
42
+      |> owner_sign_in
26 43
       |> redirect(to: "/")
27 44
     else
28 45
       _ ->
46
+    # TODO: Destroy state.
29 47
         conn
30 48
         |> delete_session(:requesting_user)
31 49
         |> delete_session(:requested_profile)

+ 11
- 1
web/controllers/page.ex View File

@@ -2,6 +2,16 @@ defmodule Fortress.Web.PageController do
2 2
   use Fortress.Web, :controller
3 3
 
4 4
   def index(conn, _params) do
5
-    render(conn, "index.html")
5
+    if is_signed_in?(conn) do
6
+      render(conn, "dashboard.html")
7
+    else
8
+      render(conn, "index.html")
9
+    end
10
+  end
11
+
12
+  def signout(conn, params) do
13
+    conn
14
+    |> owner_sign_out
15
+    |> redirect(to: Routes.page_path(conn, :index))
6 16
   end
7 17
 end

+ 25
- 13
web/controllers/resolver.ex View File

@@ -3,27 +3,39 @@ defmodule Fortress.Web.ResolverController do
3 3
 
4 4
   def present(conn, params)
5 5
 
6
-  def present(conn, %{"url" => url}) when is_binary(url) do
7
-    providers = Fortress.Provider.fetch_for(url)
8
-    live_render(conn, Fortress.Web.ResolverLiveView, session: %{url: url})
6
+  def present(conn, %{"me" => me}) when is_binary(me) do
7
+    instances = Fortress.Provider.fetch_all_for(me)
8
+    live_render(conn, Fortress.Web.ResolverLiveView, session: %{me: me, instances: instances})
9 9
   end
10 10
 
11
-  def present(conn, _), do:
12
-    render(conn, "present-no_url.html")
11
+  def present(conn, _), do: render(conn, "present-no_url.html")
13 12
 
14 13
   def request(conn, params)
15
-  def request(conn, %{"platform" => platform, "url" => url, "me" => me} = params) when is_binary(url) and is_binary(me) do
16
-    # TODO: Validate if we support platform.
17
-    cond do
18
-      !Fortress.Provider.supported?(platform) -> render(conn, "request-unsupported.html")
19
-      true ->
20
-        # TODO: Redirect to authorization page with session primed.
21
-        state = 32 |> :crypto.strong_rand_bytes() |> Base.encode64(case: :lower, padding: false)
14
+
15
+  def request(conn, %{"platform" => platform, "url" => url, "me" => me} = params)
16
+      when is_binary(url) and is_binary(me) do
17
+    if !Fortress.Provider.supported?(platform) do
18
+      render(conn, "request-unsupported.html")
19
+    else
20
+      state = Fortress.Auth.State.generate_token()
21
+      Fortress.Auth.State.persist(state, me, me, me)
22
+
23
+      primed_conn =
22 24
         conn
23 25
         |> put_session(:requested_profile, url)
24 26
         |> put_session(:requesting_user, me)
25
-        |> redirect(to: Routes.oauth2_path(conn, :request, platform, me: me, state: state))
27
+        |> put_session(:state, state)
28
+
29
+      redirect(primed_conn, to: resolve_oauth_path(primed_conn, platform))
26 30
     end
27 31
   end
32
+
28 33
   def request(conn, _), do: render(conn, "request-invalid.html")
34
+
35
+  defp resolve_oauth_path(conn, platform)
36
+
37
+  defp resolve_oauth_path(conn, "indieauth"),
38
+    do: Routes.oauth2_path(conn, :request, "indieauth", me: get_session(conn, "requesting_user"), state: get_session(conn, "state"))
39
+
40
+  defp resolve_oauth_path(conn, platform), do: Routes.oauth2_path(conn, :request, platform)
29 41
 end

+ 4
- 2
web/fortress.ex View File

@@ -4,6 +4,7 @@ defmodule Fortress.Web do
4 4
       use Phoenix.Controller, namespace: Fortress.Web
5 5
       import Plug.Conn
6 6
       alias Fortress.Web.Router.Helpers, as: Routes
7
+      import Fortress.Web.Helpers.Authentication
7 8
       import Fortress.Web.Gettext
8 9
       import Phoenix.LiveView.Controller, only: [live_render: 3]
9 10
     end
@@ -34,6 +35,7 @@ defmodule Fortress.Web do
34 35
       use Phoenix.HTML
35 36
 
36 37
       alias Fortress.Web.Router.Helpers, as: Routes
38
+      import Fortress.Web.Helpers.Authentication
37 39
       import Fortress.Web.Gettext
38 40
     end
39 41
   end
@@ -60,7 +62,7 @@ defmodule Fortress.Web do
60 62
       use Phoenix.HTML
61 63
       # import Fortress.Web.ErrorHelpers
62 64
       import Fortress.Web.Gettext
63
-      # import Fortress.Web.Helpers.Authentication
65
+      import Fortress.Web.Helpers.Authentication
64 66
       import Phoenix.View, only: [render: 3]
65 67
       alias Fortress.Web.Router.Helpers, as: Routes
66 68
     end
@@ -73,7 +75,7 @@ defmodule Fortress.Web do
73 75
       import Phoenix.View, only: [render: 3]
74 76
       # import Fortress.Web.ErrorHelpers
75 77
       import Fortress.Web.Gettext
76
-      # import Fortress.Web.Helpers.Authentication
78
+      import Fortress.Web.Helpers.Authentication
77 79
       alias Fortress.Web.Router.Helpers, as: Routes
78 80
     end
79 81
   end

+ 66
- 0
web/helpers/auth.ex View File

@@ -0,0 +1,66 @@
1
+defmodule Fortress.Web.Helpers.Authentication do
2
+  alias Fortress.Guardian.Plug, as: FGPlug
3
+  alias IndieWeb.Auth.Scope
4
+
5
+  def is_signed_in?(conn, type \\ :owner) do
6
+    FGPlug.authenticated?(conn, key: type)
7
+  end
8
+
9
+  def is_signed_out?(conn, type \\ :owner), do: !is_signed_in?(conn, type)
10
+
11
+  def owner_sign_in(%{assigns: %{ueberauth_auth: %Ueberauth.Auth{} = auth}} = conn) do
12
+    user_url = auth.info.urls.url
13
+    site = Fortress.Repo.Site.find_by_url(user_url)
14
+
15
+    conn
16
+    |> FGPlug.sign_in(
17
+      site,
18
+      %{},
19
+      ttl: {1, :week},
20
+      key: :owner
21
+    )
22
+    |> FGPlug.remember_me(site)
23
+  end
24
+
25
+  def owner_sign_out(conn) do
26
+    conn
27
+    |> FGPlug.sign_out()
28
+  end
29
+
30
+  def indie_sign_in(client_id, scopes)
31
+
32
+  def indie_sign_in(client_id, scopes) when is_binary(scopes),
33
+    do: indie_sign_in(client_id, Scope.from_string(scopes))
34
+
35
+  def indie_sign_in(client_id, scopes) when is_list(scopes) do
36
+    claims = %{scope: Scope.to_string(scopes)}
37
+
38
+    {:ok, token, _claims} =
39
+      Fortress.Guardian.encode_and_sign(
40
+        client_id,
41
+        claims,
42
+        key: :indie,
43
+        token_type: :access
44
+      )
45
+
46
+    token
47
+  end
48
+
49
+  def indie_current_token(conn) do
50
+    case FGPlug.current_token(conn, key: :indie) do
51
+      nil -> {:error, :no_token}
52
+      token -> {:ok, token}
53
+    end
54
+  end
55
+
56
+  def indie_current_resource(conn) do
57
+    case FGPlug.current_resource(conn, key: :indie) do
58
+      nil -> {:error, :no_resource}
59
+      resource -> {:ok, resource}
60
+    end
61
+  end
62
+
63
+  def revoke_token(token) do
64
+    Fortress.Guardian.revoke(token)
65
+  end
66
+end

+ 0
- 7
web/helpers/auth_pipeline.ex View File

@@ -1,7 +0,0 @@
1
-defmodule Fortress.Web.Plug.AuthPipeline do
2
-  use Guardian.Plug.Pipeline, otp_app: :fortress
3
-
4
-  plug Guardian.Plug.VerifySession, key: :fortress
5
-  plug Guardian.Plug.VerifyCookie, key: :fortress
6
-  plug Guardian.Plug.LoadResource, allow_blank: true
7
-end

+ 10
- 0
web/plug/auth_pipeline/indie.ex View File

@@ -0,0 +1,10 @@
1
+defmodule Fortress.Web.Plug.AuthPipeline.Indie do
2
+  use Guardian.Plug.Pipeline, otp_app: :fortress,
3
+    module: Fortress.Guardian,
4
+    error_handler: Fortress.Web.Plug.AuthErrorHandler.API
5
+
6
+  plug Guardian.Plug.VerifySession, key: :indie
7
+  plug Guardian.Plug.VerifyCookie, key: :indie
8
+  plug Guardian.Plug.VerifyHeader, key: :indie
9
+  plug Guardian.Plug.LoadResource, allow_blank: true
10
+end

+ 10
- 0
web/plug/auth_pipeline/owner.ex View File

@@ -0,0 +1,10 @@
1
+defmodule Fortress.Web.Plug.AuthPipeline.Owner do
2
+  use Guardian.Plug.Pipeline, otp_app: :fortress,
3
+    module: Fortress.Guardian,
4
+    error_handler: Fortress.Web.Plug.AuthErrorHandler.Browser
5
+
6
+  plug Guardian.Plug.VerifySession, key: :owner
7
+  plug Guardian.Plug.VerifyCookie, key: :owner
8
+  plug Guardian.Plug.VerifyHeader, key: :owner
9
+  plug Guardian.Plug.LoadResource, allow_blank: true
10
+end

+ 12
- 0
web/plug/error_handler/api.ex View File

@@ -0,0 +1,12 @@
1
+defmodule Fortress.Web.Plug.AuthErrorHandler.API do
2
+  require Logger
3
+  import Plug.Conn
4
+
5
+  def auth_error(conn, {type, reason}, _opts) do
6
+    Logger.warn("Endpoint was not authenticated.", type: type, reason: reason)
7
+
8
+    conn
9
+    |> put_resp_content_type("application/json")
10
+    |> Explode.unauthorized()
11
+  end
12
+end

+ 16
- 0
web/plug/error_handler/browser.ex View File

@@ -0,0 +1,16 @@
1
+defmodule Fortress.Web.Plug.AuthErrorHandler.Browser do
2
+  alias Fortress.Web.Router.Helpers, as: RouteHelpers
3
+  import Phoenix.Controller, only: [put_flash: 3, redirect: 2, current_url: 1]
4
+  require Logger
5
+
6
+  def auth_error(conn, {type, reason}, _opts) do
7
+    Logger.info("Provided route requires authentication; redirecting.", type: type, reason: reason)
8
+
9
+    conn
10
+    |> Plug.Conn.fetch_session
11
+    |> Phoenix.Controller.fetch_flash
12
+    |> Plug.Conn.clear_session()
13
+    |> put_flash(:warning, "Please login before continuing.")
14
+    |> redirect(to: RouteHelpers.resolver_path(conn, :present, conn.params |> Enum.to_list |> Keyword.new(fn {k, v} -> {String.to_atom(k), v} end)))
15
+  end
16
+end

+ 18
- 23
web/router.ex View File

@@ -15,55 +15,50 @@ defmodule Fortress.Web.Router do
15 15
     plug(:accepts, ["json"])
16 16
   end
17 17
 
18
-  pipeline :auth do
19
-    plug Fortress.Web.Plug.AuthPipeline
20
-  end
18
+  pipeline(:owner_auth, do: plug(Fortress.Web.Plug.AuthPipeline.Owner))
19
+  pipeline(:indie_auth, do: plug(Fortress.Web.Plug.AuthPipeline.Indie))
21 20
 
22
-  pipeline :ensure_logged_in do
23
-    plug(Guardian.Plug.EnsureAuthenticated, key: :fortress)
24
-  end
21
+  pipeline(:requires_owner_auth, do: plug(Guardian.Plug.EnsureAuthenticated, key: :owner))
22
+  pipeline(:requires_indie_auth, do: plug(Guardian.Plug.EnsureAuthenticated, key: :indie))
23
+  pipeline(:prohibit_indie_auth, do: plug(Guardian.Plug.EnsureNotAuthenticated, key: :indie))
25 24
 
26 25
   scope "/", Fortress.Web do
27
-    pipe_through([:browser, :auth])
26
+    pipe_through([:browser, :owner_auth])
28 27
 
29 28
     get("/", PageController, :index)
29
+    delete("/logout", PageController, :signout)
30 30
   end
31 31
 
32 32
   scope "/", Fortress.Web do
33
-    pipe_through([:browser, :auth])
33
+    pipe_through([:browser, :owner_auth])
34 34
 
35 35
     get("/resolve", ResolverController, :present)
36 36
     get("/resolve/via/:platform", ResolverController, :request)
37 37
   end
38 38
 
39 39
   scope "/oauth", Fortress.Web do
40
-    pipe_through([:browser, :auth])
40
+    pipe_through([:browser, :owner_auth])
41 41
     get("/:provider", OAuth2Controller, :request, as: :oauth2)
42 42
     get("/:provider/callback", OAuth2Controller, :callback, as: :oauth2)
43 43
     post("/:provider/callback", OAuth2Controller, :callback, as: :oauth2)
44 44
   end
45 45
 
46
-  # Meant for the browser, requires pre-auth by the user.
47
-  # [:browser, :owner, :requires_owner_auth]
48
-  scope "/endpoints/indieauth", Fortress.Web do
49
-    get("/", Indie.AuthController, :authorize)
50
-    post("/complete", Indie.AuthController, :complete_authorization)
51
-  end
46
+  scope "/endpoints/indieauth", Fortress.Web, as: :indie do
47
+    pipe_through([:api, :owner_auth, :requires_owner_auth])
52 48
 
53
-  # Meant for API usage, does not require authorization.
54
-  # [:browser, :owner, :requires_owner_auth]
55
-  scope "/endpoints/indieauth", Fortress.Web do
49
+    get("/", Indie.AuthController, :authorize)
56 50
     post("/", Indie.AuthController, :verify_authorization_code)
57
-    post("/token", Indie.TokenController, :create)
51
+    post("/complete", Indie.AuthController, :complete_authorization)
52
+    post("/token", Indie.TokenApiController, :create)
58 53
   end
59 54
 
60
-  # Meant for API usage, requires valid IndieAuth token
61
-  scope "/endpoints/indieauth", Fortress.Web do
62
-    get("/token", Indie.TokenController, :validate)
55
+  scope "/endpoints/indieauth", Fortress.Web, as: :indie do
56
+    pipe_through([:api, :indie_auth, :requires_indie_auth])
57
+    get("/token", Indie.TokenApiController, :validate)
63 58
   end
64 59
 
65 60
   scope "/api", Fortress.Web do
66
-    pipe_through([:api])
61
+    pipe_through([:api, :owner_auth, :requires_owner_auth])
67 62
     get("/client", Api.ClientController, :request)
68 63
     post("/client", Api.ClientController, :invoke)
69 64
   end

+ 0
- 48
web/templates/indie/auth/authorize-code.html.eex View File

@@ -1,48 +0,0 @@
1
-<% name = "ACME" %>
2
-<section class="bg-white near-black w-100">
3
-  <form class="ma0 pa0" method="post" action="<%= indie_auth_path(@conn, :complete_authorize) %>">
4
-    <input type=hidden name=_csrf_token value="<%= Phoenix.Controller.get_csrf_token %>" />
5
-    <input type=hidden name=client_id value="<%= @conn.params["client_id"] %>" />
6
-    <input type=hidden name=redirect_uri value="<%= @conn.params["redirect_uri"] %>" />
7
-    <input type=hidden name=response_type value="<%= @conn.params["response_type"] %>" />
8
-    <%= if @conn.params["state"] do %><input type=hidden name=state value="<%= @conn.params["state"] %>" /><% end %>
9
-
10
-    <article class="ph3 ph5-ns">
11
-      <div class="cf center w-100 tc-m flex flex-column flex-row-l justify-around items-start">
12
-        <img src="<%= static_path(@conn, "/images/marginalia/marginalia-sign-in.png") %>" class="v-mid order-1 w-100 dn db-ns self-center mw6">
13
-        <div class="pb3 pb4-ns w-50-l mt4 black-70 order-2 self-start">
14
-          <h1 class="f2 f1-l tracked-tight measure lh-title black-40">
15
-            Signing In
16
-          </h1>
17
-          <p class="f3 lh-copy v-mid black-70 measure tracked-tight" id="mainMessage">
18
-            <strong><img class="mw2 v-mid mh1 br-100" src="<%= @mf2[:app]["logo"] %>" /><%= @mf2[:app]["name"] %></strong>
19
-            is requesting to sign in
20
-            <strong><img class="mw2 v-mid mh1 br-100" src="<%= @mf2[:user]["photo"] %>" /><%= @mf2[:user]["name"] %></strong>.
21
-          </p>
22
-          <h3 class="f4 f3-l tracked-tight measure lh-subtitle black-60">
23
-            Scopes Requested
24
-          </h3>
25
-          <p class="f5 f4-l v-mid black-70 measure tracked-tight">
26
-            <%= @mf2[:app]["name"] %> is requesting use of the following scopes:
27
-          </p>
28
-          <dl class="mv4 list">
29
-            <%= for scope <- scopes(@conn) do %>
30
-              <div class="hide-child">
31
-                <dt class="fw6 code lh-copy">
32
-                <div class="pretty p-default p-primary p-round">
33
-                  <input type="checkbox" name="scope[]" value="<%= scope.name %>"/>
34
-                  <div class="state ml2">
35
-                    <label><%= scope.name %></label>
36
-                  </div>
37
-                </div>
38
-                </dt>
39
-                <dd class="fw3 child f7 serif lh-copy ml4 pb2 mv1"><%= scope.description %></dd>
40
-              </div>
41
-            <% end %>
42
-          </dl>
43
-          <button class="f4 input-reset shadow-2 grow pa3 bg-dark-green near-white ttu b--dark-green pointer b--solid bw2 br2" type="submit">Continue</button>
44
-        </div>
45
-      </div>
46
-    </article>
47
-  </form>
48
-</section>

web/templates/indie/auth/authorize-id.html.eex → web/templates/indie/auth/authorize.html.eex View File


+ 1
- 25
web/templates/indie/auth/error.html.eex View File

@@ -1,25 +1 @@
1
-<section class="bg-white near-black w-100">
2
-  <article class="pb4">
3
-    <div class="ph3 ph5-ns">
4
-      <div class="cf center mw8 w-100 tc-m flex flex-column flex-row-l justify-around items-start">
5
-        <img src="<%= static_path(@conn, "/images/marginalia/marginalia-fatal-error.png") %>" class="v-mid order-1 w-100 dn db-ns self-center mw6">
6
-        <div class="pb3 pb4-ns mt4 black-70 order-2 w-60-l self-top">
7
-          <h1 class="f2 tracked ttu lh-title gray">
8
-            That Didn't Work
9
-          </h1>
10
-          <p class="f4 lh-copy tracked-tight" id="mainMessage">
11
-            Fortress ran into a problem when attempting to authenticate you.
12
-          </p>
13
-          <ul class="list">
14
-            <li class="lh-copy pv1">
15
-              <a href="<%= @conn.params["me"] %>" class="link color-inherit underline">Back to your site</a>
16
-            </li>
17
-            <li class="lh-copy pv1">
18
-              <a href="<%= @conn.params["client_id"] %>" class="link color-inherit underline">Back to the app</a>
19
-            </li>
20
-          </ul>
21
-        </div>
22
-      </div>
23
-    </div>
24
-  </article>
25
-</section>
1
+

+ 4
- 9
web/templates/layout/app.html.eex View File

@@ -32,15 +32,10 @@
32 32
           </span>
33 33
         </aside>
34 34
         <nav>
35
-          <a href="#">
36
-            Apps
37
-          </a>
38
-          <a href="#">
39
-            Code
40
-          </a>
41
-          <a href="#">
42
-            Platforms
43
-          </a>
35
+          <%= link "Apps", to: "#" %>
36
+          <%= link "Code", to: "#" %>
37
+          <%= link "Platforms", to: "#" %>
38
+          <%= if is_signed_in?(@conn), do: link("Sign Out", to: Routes.page_path(@conn, :signout)) %>
44 39
       </div>
45 40
       </div>
46 41
       <div id="loader" class="absolute right-2 top-2 br-pill shadow-2 bg-dark-gray near-white pa3 dn">

+ 15
- 0
web/templates/o_auth2/request-error.html.eex View File

@@ -0,0 +1,15 @@
1
+<main class="w-100 bg-near-white near-black pa2" phx-hook="Generic">
2
+  <div class="flex flex-column items-start justify-start mw7 center pv3">
3
+    <h3 class="f3 f2-l measure fw1 sans dark-gray tracked-tight lh-title ttu tc">
4
+      Failed to Sign In
5
+    </h3>
6
+    <p class="lh-copy measure f5 tc ma0-l">
7
+      Fortress couldn't attempt to sign you in.
8
+    </p>
9
+    <ul class="ma0">
10
+      <%= for error <- @errors do %>
11
+        <li class="lh-copy measure"><%= error.message %></li>
12
+      <% end %>
13
+    </ul>
14
+  </div>
15
+</main>

+ 7
- 0
web/templates/page/dashboard.html.eex View File

@@ -0,0 +1,7 @@
1
+<main class="w-100 bg-near-white near-black pa2">
2
+  <div class="flex flex-column items-start justify-start mw7 center pv3">
3
+    <h1 class="f3 measure fw1 dark-gray tracked-tight lh-title ttu">
4
+      Device
5
+    </h1>
6
+  </div>
7
+</main>

+ 11
- 7
web/templates/page/homepage-login.html.leex View File

@@ -4,17 +4,21 @@
4 4
 <%= f = form_for :scanner, "#", [phx_submit: :scan, method: :get] %>
5 5
 <%= label f, :url, class: "f4" do %>
6 6
   URL
7
-  <%= text_input f, :url, class: "w-100 measure-wide input-reset bg-washed-yellow navy mv2 pa2 bw1 b--navy b--solid f4 f2-l code", value: @scanner["url"] %>
7
+  <%= text_input f, :url, class: "w-100 measure-wide input-reset bg-washed-yellow navy mv2 pa2 bw1 b--navy b--solid f4 f2-l code", value: (@scanner["url"] || @scanner["hcard"]["url"]) %>
8 8
 <% end %>
9
-<div class="flex flex-row items-center justify-between mw6 w-100" phx-hook="Generic">
9
+<div class="flex flex-row items-center justify-between center mw6 w-100" phx-hook="Generic">
10 10
   <%= if @scanner["hcard"] do %>
11 11
     <button phx-click="begin-signin" class="sans b ph3 pv2 input-reset ba near-white b--dark-green bg-dark-green grow pointer f4 dib">Sign In</button>
12
-    <span class="v-mid pa2 animated fadeInUp">
13
-      <img src="<%= @scanner["hcard"]["photo"] %>" class="w2 h2 v-mid ma2" />
14
-      <span class="f4 fw1"><%= @scanner["hcard"]["name"] %></span>
15
-    </span>
12
+    <div class="flex flex-column">
13
+      <span class="v-mid pa2 animated fadeInUp">
14
+        <img src="<%= @scanner["hcard"]["photo"] %>" class="w2 h2 v-mid ma2" />
15
+        <span class="f4 fw1"><%= @scanner["hcard"]["name"] %></span>
16
+      </span>
17
+      <span>
18
+        <%= if @provider_count == 0, do: "No stored providers", else: "#{@provider_count} providers found." %>
19
+      </span>
16 20
   <% else %>
17
-    <%= submit "Continue", class: "sans b ph3 pv2 input-reset ba navy b--navy bg-transparent grow pointer f4 dib" %>
21
+    <%= submit "Continue", class: "sans ph3 pv2 input-reset bw1 ba b--navy bg-navy near-white hover-blue grow pointer f4 dib center" %>
18 22
   <% end %>
19 23
 </div>
20 24
 </form>

+ 1
- 1
web/templates/page/index.html.eex View File

@@ -6,7 +6,7 @@
6 6
     <p class="f5 f3-l gray mt0 lh-copy center tc">
7 7
       Interact with the open Web with your authentic self.
8 8
     </p>
9
-    <%= live_render(@conn, Fortress.Web.HomepageAuthLiveView, container: {:div, class: "form--homepageLogin flex-auto mw7 tc center"}) %>
9
+    <%= live_render(@conn, Fortress.Web.HomepageAuthLiveView, container: {:div, class: "form--homepageLogin flex-auto mw7 tc center"}, session: %{scanner: %{"url" => @conn.params["url"]}}) %>
10 10
   </div>
11 11
 </main>
12 12
 <main class="w-100 bg-near-black near-white pa2">

+ 4
- 2
web/templates/resolver/present.html.leex View File

@@ -4,12 +4,14 @@
4 4
       Providers
5 5
     </h3>
6 6
     <p class="lh-copy measure f5 tc center ma0-l">
7
-      Pick a method of logging in that you'd like to use.
7
+      Pick one of the <%= Enum.count(@instances) %> providers that you'd
8
+      like to use for <code><%= @me %></code>
8 9
     </p>
9
-    <div class="flex flex-column mw5 w-100 center">
10
+    <div class="flex flex-column mw6 w-100 center pa3">
10 11
       <%= for instance <- @instances do %>
11 12
         <%= live_component @socket, Fortress.Web.ProviderLiveComponent, id: instance.url, instance: instance %>
12 13
       <% end %>
13 14
     </div>
15
+    <button phx-click="refresh-all-providers">Update</button>
14 16
   </div>
15 17
 </main>

+ 3
- 0
web/views/indie/auth.ex View File

@@ -0,0 +1,3 @@
1
+defmodule Fortress.Web.Indie.AuthView do
2
+  use Fortress.Web, :view
3
+end

+ 26
- 4
web/views/live/homepage_auth_view.ex View File

@@ -18,11 +18,33 @@ defmodule Fortress.Web.HomepageAuthLiveView do
18 18
   def handle_event("scan", %{"scanner" => scanner}, socket) do
19 19
     %{"url" => url} = scanner
20 20
 
21
-    case IndieWeb.HCard.fetch_representative(url) do
22
-      {:ok, hcard} ->
23
-        {:noreply, socket |> assign(:scanner, %{"hcard" => hcard, "url" => hcard["url"]})}
21
+    with(
22
+      {:ok, hcard} <- IndieWeb.HCard.fetch_representative(url),
23
+      {:ok, site} <- Fortress.Repo.Site.upsert(hcard),
24
+      _ <- Fortress.Workers.Provider.update_records(url),
25
+      providers <- Fortress.Provider.fetch_all_for(site.url),
26
+      indieauth_provider <- Enum.find(providers, &(&1.platform == :indieauth))
27
+    ) do
28
+      if indieauth_provider == nil do
29
+        {:noreply,
30
+         socket
31
+         |> assign(:scanner, %{"hcard" => hcard, "url" => hcard["url"]})
32
+         |> assign(:provider_count, Enum.count(providers))}
33
+      else
34
+        {:stop,
35
+         socket
36
+         |> redirect(
37
+           to:
38
+             Routes.resolver_path(socket, :request, indieauth_provider.platform,
39
+               me: indieauth_provider.me,
40
+               url: URI.to_string(indieauth_provider.url)
41
+             )
42
+         )}
43
+      end
44
+    else
24 45
       nil ->
25
-        {:noreply, socket |> assign(:scanner, %{"error" => "Your URL didn't seem to provide a h-card."})}
46
+        {:noreply,
47
+         socket |> assign(:scanner, %{"error" => "Your URL didn't seem to provide a h-card."})}
26 48
     end
27 49
   end
28 50
 

+ 3
- 3
web/views/live/provider-component.ex View File

@@ -18,7 +18,7 @@ defmodule Fortress.Web.ProviderLiveComponent do
18 18
 
19 19
   def get_message_for(instance)
20 20
   def get_message_for(%{status: "pending"}), do: "Resolving..."
21
-  def get_message_for(%{status: "broken"}), do: "It's not working."
22
-  def get_message_for(%{status: "found"}), do: "Ready to connect!"
23
-  def get_message_for(_), do: "Not sure tbh"
21
+  def get_message_for(%{status: "broken"}), do: "The remote link to your site is broken."
22
+  def get_message_for(%{status: "found"}), do: "Sign in is supported."
23
+  def get_message_for(_), do: "Undefined status for this platform."
24 24
 end

+ 6
- 9
web/views/live/resolver.ex View File

@@ -7,30 +7,27 @@ defmodule Fortress.Web.ResolverLiveView do
7 7
 
8 8
   def mount(params, socket)
9 9
 
10
-  def mount(%{url: url}, socket) do
11
-    instances = Fortress.Provider.fetch_for(url)
12
-
10
+  def mount(%{me: me, instances: instances} = assigns, socket) do
13 11
     if connected?(socket) do
14 12
       Enum.each(
15 13
         instances,
16 14
         &Phoenix.PubSub.subscribe(Fortress.PubSub, self(), Fortress.Provider.Instance.topic_for(&1))
17 15
       )
18
-      Fortress.Provider.check_all_for(url)
19 16
     end
20 17
 
21
-    {:ok, socket |> assign(:instances, instances) |> assign(:url, url)}
18
+    {:ok, socket |> assign(assigns)}
22 19
   end
23 20
 
24
-  def handle_event("refresh-all-providers", %{"resolver" => params}, socket) do
25
-    url = params["url"]
26
-    Fortress.Provider.check_all_for(url)
21
+  def handle_event("refresh-all-providers",_, socket) do
22
+    url = socket.assigns[:me]
23
+    Fortress.Workers.Provider.update_records(url)
27 24
     {:noreply, socket}
28 25
   end
29 26
 
30 27
   def handle_event(_, _, socket), do: {:noreply, socket}
31 28
 
32 29
   def handle_info({"refresh-single-provider", instance}, socket) do
33
-    Fortress.Provider.check_for(instance)
30
+    Fortress.Workers.Provider.check_instance(instance)
34 31
     instances = socket.assigns[:instances]
35 32
     index = Enum.find_index(instances, fn inst -> instance.url == inst.url end)
36 33
     {:noreply, socket |> assign(:instances, List.replace_at(instances, index, %{instance | status: "pending"}))}

Loading…
Cancel
Save