Browse Source

Support syndication and category links.

jackyalcine 4 months ago
parent
commit
e642fcf85c
Signed by: Jacky Alciné <yo@jacky.wtf> GPG Key ID: 537A4F904B15268D

+ 2
- 1
.formatter.exs View File

@@ -1,5 +1,6 @@
1 1
 [
2 2
   import_deps: [:ecto, :phoenix],
3 3
   inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
4
-  subdirectories: ["priv/*/migrations"]
4
+  subdirectories: ["priv/*/migrations"],
5
+  line_length: 100
5 6
 ]

+ 4
- 2
config/config.exs View File

@@ -11,7 +11,9 @@ config :koype_publish, Koype.Publish.Web.Endpoint,
11 11
     scheme: {:system, "CANONICAL_SCHEME", "http"},
12 12
     port: {:system, "CANONICAL_PORT", 5000}
13 13
   ],
14
-  secret_key_base: "sa9LA8od8eoD+I45nyuTJr6Nf+DRilitEzdZa1BM+oODt0kqeNcB+8t9s9eE/1zU",
14
+  secret_key_base:
15
+    {:system, :string, "SECRET_KEY_BASE",
16
+     64 |> :crypto.strong_rand_bytes() |> Base.encode64(case: :lower, padding: false)},
15 17
   render_errors: [view: Koype.Publish.Web.ErrorView, accepts: ~w(html json)],
16 18
   pubsub: [name: Koype.Publish.PubSub, adapter: Phoenix.PubSub.PG2]
17 19
 
@@ -29,7 +31,7 @@ config :koype_publish, Koype.Publish.Guardian,
29 31
   error_handler: Koype.Publish.Web.Plug.BrowserAuthErrorHandler,
30 32
   secret_key:
31 33
     {:system, :string, "GUARDIAN_SECRET_KEY",
32
-     32 |> :crypto.strong_rand_bytes() |> Base.encode64(case: :lower, padding: false)},
34
+     64 |> :crypto.strong_rand_bytes() |> Base.encode64(case: :lower, padding: false)},
33 35
   schema_name: "guardian_tokens",
34 36
   token_types: ["refresh_token"],
35 37
   sweep_interval: 3600

+ 2
- 0
docker-compose.yml View File

@@ -17,6 +17,8 @@ services:
17 17
       - ./:/opt/koype/publish/:z
18 18
       - ./deps:/opt/koype/deps/:rw
19 19
       - ./_build:/opt/koype/_build/:rw
20
+    networks:
21
+      - network
20 22
     healthcheck:
21 23
       test: ["CMD", "/tmp/koype-publish-docker/healthcheck.sh"]
22 24
       interval: 60s

+ 55
- 44
lib/ueberauth/strategy/indieauth/remote.ex View File

@@ -31,19 +31,24 @@ defmodule Ueberauth.Strategy.IndieAuth.Remote do
31 31
       client_id: client_id,
32 32
       state: conn.params["state"] || nil
33 33
     ]
34
+    case IndieWeb.Http.get(me) do
35
+      {:ok, _} ->
36
+        case apply(module, :authorize_url!, [opts]) do
37
+          nil ->
38
+            conn
39
+            |> set_errors!(
40
+              error("no_authorization_endpoint", "The authorization endpoint could not be resolved.")
41
+            )
42
+
43
+          authorize_url when is_binary(authorize_url) ->
44
+            conn
45
+            |> Plug.Conn.put_session(:indieauth_me, me)
46
+            |> redirect!(authorize_url)
47
+        end
48
+      _ -> 
49
+        Logger.info("Failed to connect to the IndieAuth client site.", me: me)
50
+        conn |> set_errors!(error("site_not_responding", "Your site doesn't seem to be responding."))
34 51
 
35
-
36
-    case apply(module, :authorize_url!, [opts]) do
37
-      nil ->
38
-        conn
39
-        |> set_errors!(
40
-          error("no_authorization_endpoint", "The authorization endpoint could not be resolved.")
41
-        )
42
-
43
-      authorize_url when is_binary(authorize_url) ->
44
-        conn
45
-        |> Plug.Conn.put_session(:indieauth_me, me)
46
-        |> redirect!(authorize_url)
47 52
     end
48 53
   end
49 54
 
@@ -51,40 +56,46 @@ defmodule Ueberauth.Strategy.IndieAuth.Remote do
51 56
     module = option(conn, :oauth2_module)
52 57
     client_id = do_get_client_id(conn)
53 58
     me = do_get_user(conn)
59
+    case IndieWeb.Http.get(me) do
60
+      {:ok, _} ->
61
+        resp =
62
+          apply(module, :get_token!, [
63
+            [
64
+              code: code,
65
+              redirect_uri: callback_url(conn),
66
+              client_id: client_id,
67
+              scope: "read",
68
+              me: me
69
+            ]
70
+          ])
71
+
72
+        case resp do
73
+          %OAuth2.AccessToken{access_token: token, other_params: %{"me" => me}} ->
74
+            conn
75
+            |> Plug.Conn.delete_session(:indieauth_me)
76
+            |> Plug.Conn.put_private(:indieauth_user, me)
77
+            |> Plug.Conn.put_private(:indieauth_access_token, token)
78
+            |> Plug.Conn.put_private(:indieauth_token, resp)
79
+
80
+          {:error, :no_user_specified} ->
81
+            conn
82
+            |> Plug.Conn.delete_session(:indieauth_me)
83
+            |> set_errors!([
84
+              error("no_user_found", "The session was cleaned")
85
+            ])
86
+
87
+          resp ->
88
+            Logger.error("Failed to login.", error: inspect(resp))
89
+            conn
90
+            |> Plug.Conn.delete_session(:indieauth_me)
91
+            |> set_errors!([
92
+              error(resp.other_params["error"] || "unknown", resp.other_params["error_description"] || "An unspecified error occurred.")
93
+            ])
94
+        end
95
+        _ -> conn |> set_errors!(error("site_not_responding", "Your site doesn't seem to be responding."))
54 96
 
55
-    resp =
56
-      apply(module, :get_token!, [
57
-        [
58
-          code: code,
59
-          redirect_uri: callback_url(conn),
60
-          client_id: client_id,
61
-          scope: "read",
62
-          me: me
63
-        ]
64
-      ])
65
-
66
-    case resp do
67
-      %OAuth2.AccessToken{access_token: token, other_params: %{"me" => me}} ->
68
-        conn
69
-        |> Plug.Conn.delete_session(:indieauth_me)
70
-        |> Plug.Conn.put_private(:indieauth_user, me)
71
-        |> Plug.Conn.put_private(:indieauth_token, resp)
72
-
73
-      {:error, :no_user_specified} ->
74
-        conn
75
-        |> Plug.Conn.delete_session(:indieauth_me)
76
-        |> set_errors!([
77
-          error("no_user_found", "The session was cleaned")
78
-        ])
79
-
80
-      resp ->
81
-        Logger.error("Failed to login.", error: inspect(resp))
82
-        conn
83
-        |> Plug.Conn.delete_session(:indieauth_me)
84
-        |> set_errors!([
85
-          error(resp.other_params["error"] || "unknown", resp.other_params["error_description"] || "An unspecified error occurred.")
86
-        ])
87 97
     end
98
+
88 99
   end
89 100
 
90 101
   def handle_callback!(conn) do

+ 14
- 5
lib/ueberauth/strategy/indieauth/remote/oauth2.ex View File

@@ -56,11 +56,16 @@ defmodule Ueberauth.Strategy.IndieAuth.Remote.OAuth2 do
56 56
 
57 57
     case do_get_endpoint(me, :authorization) do
58 58
       nil ->
59
-        %{params: []}
59
+        client
60
+        |> Map.put(:authorize_url, nil)
61
+        |> OAuth2.Strategy.AuthCode.authorize_url(
62
+          params
63
+        )
60 64
 
61
-      endpoint ->
62
-        OAuth2.Strategy.AuthCode.authorize_url(
63
-          client |> struct(authorize_url: endpoint),
65
+      endpoint when is_binary(endpoint) ->
66
+        client
67
+        |> Map.put(:authorize_url, endpoint)
68
+        |> OAuth2.Strategy.AuthCode.authorize_url(
64 69
           params
65 70
         )
66 71
     end
@@ -71,7 +76,11 @@ defmodule Ueberauth.Strategy.IndieAuth.Remote.OAuth2 do
71 76
 
72 77
     case do_get_endpoint(me, :token) do
73 78
       nil ->
74
-        :no_endpoint
79
+        client
80
+        |> Map.put(:token_url, nil)
81
+        |> OAuth2.Strategy.AuthCode.authorize_url(
82
+          params
83
+        )
75 84
 
76 85
       endpoint ->
77 86
         client

+ 0
- 1
web/controllers/auth_controller.ex View File

@@ -49,7 +49,6 @@ defmodule Koype.Publish.Web.AuthController do
49 49
   def delete(conn, _params) do
50 50
     conn
51 51
     |> put_flash(:info, "You have been logged out!")
52
-    |> configure_session(drop: true)
53 52
     |> KGPlug.sign_out
54 53
     |> redirect(to: "/")
55 54
   end

+ 5
- 1
web/controllers/editor_controller.ex View File

@@ -23,7 +23,11 @@ defmodule Koype.Publish.Web.EditorController do
23 23
 
24 24
   def publish(conn, params) do
25 25
     client = do_get_client(conn)
26
-    resp = MicropubClient.create(client, :entry, %{"content" => %{html: params["content"]}, "category" => params["categories"]})
26
+    payload = %{"content" => %{html: params["content"]},
27
+      "category" => params["categories"],
28
+      "slug" => params["slug"],
29
+      "mp-syndicate-to" => params["mp-syndicate-to"]} |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Map.new
30
+    resp = MicropubClient.create(client, :entry, payload)
27 31
 
28 32
     json(conn, %{status: :ok})
29 33
   end

+ 0
- 2
web/controllers/page_controller.ex View File

@@ -4,6 +4,4 @@ defmodule Koype.Publish.Web.PageController do
4 4
   def index(conn, _params) do
5 5
     render(conn, "index.html")
6 6
   end
7
-
8
-  def about(conn, _), do: render(conn, "about.html")
9 7
 end

+ 1
- 1
web/endpoint.ex View File

@@ -45,6 +45,6 @@ defmodule Koype.Publish.Web.Endpoint do
45 45
   plug Koype.Publish.Web.Router
46 46
 
47 47
   def init(_key, config) do
48
-    {:ok, new_config} = Confex.Resolver.resolve(config)
48
+    {:ok, _} = Confex.Resolver.resolve(config)
49 49
   end
50 50
 end

+ 5
- 4
web/router.ex View File

@@ -22,8 +22,8 @@ defmodule Koype.Publish.Web.Router do
22 22
     plug(Koype.Publish.Web.Plug.Guardian.Indie)
23 23
   end
24 24
 
25
-  pipeline :ensure_logged_out, do:  plug(Guardian.Plug.EnsureNotAuthenticated)
26
-  pipeline :ensure_logged_in, do:  plug(Guardian.Plug.EnsureAuthenticated)
25
+  pipeline :ensure_logged_out, do: plug(Guardian.Plug.EnsureNotAuthenticated)
26
+  pipeline :ensure_logged_in, do: plug(Guardian.Plug.EnsureAuthenticated)
27 27
 
28 28
 
29 29
   scope "/", Koype.Publish.Web do
@@ -33,7 +33,7 @@ defmodule Koype.Publish.Web.Router do
33 33
   end
34 34
 
35 35
   scope "/auth", Koype.Publish.Web do
36
-    pipe_through [:browser, :ensure_logged_out]
36
+    pipe_through [:browser, :auth]
37 37
     get "/new", AuthController, :new
38 38
     get "/:provider", AuthController, :request
39 39
     post "/:provider/callback", AuthController, :callback
@@ -41,8 +41,9 @@ defmodule Koype.Publish.Web.Router do
41 41
   end
42 42
 
43 43
   scope "/auth" do
44
-    pipe_through [:browser, :auth, :ensure_logged_in]
44
+    pipe_through [:browser, :auth]
45 45
     delete "/logout", AuthController, :delete
46
+    get "/logout", AuthController, :delete
46 47
   end
47 48
 
48 49
   scope "/~/", Koype.Publish.Web do

+ 17
- 18
web/static/assets/.eslintrc.json View File

@@ -1,21 +1,20 @@
1 1
 {
2 2
   "parser": "@typescript-eslint/parser",
3
-    "env": {
4
-        "browser": true,
5
-        "es6": true
6
-    },
7
-    "extends": "standard",
8
-    "globals": {
9
-        "Atomics": "readonly",
10
-        "SharedArrayBuffer": "readonly"
11
-    },
12
-    "parserOptions": {
13
-        "ecmaVersion": 2018,
14
-        "sourceType": "module"
15
-    },
16
-    "plugins": [
17
-        "vue"
18
-    ],
19
-    "rules": {
20
-    }
3
+  "env": {
4
+    "browser": true,
5
+    "es6": true
6
+  },
7
+  "extends": "standard",
8
+  "globals": {
9
+    "Atomics": "readonly",
10
+    "SharedArrayBuffer": "readonly"
11
+  },
12
+  "parserOptions": {
13
+    "ecmaVersion": 2018,
14
+    "sourceType": "module"
15
+  },
16
+  "plugins": [
17
+    "vue"
18
+  ],
19
+  "rules": {}
21 20
 }

+ 92
- 44
web/static/assets/js/editor.ts View File

@@ -1,66 +1,114 @@
1
-import Quill from 'quill'
2
-
3
-function compileRequest (quill: Quill) {
4
-  const publishButton = document.querySelector('#publish')!
5
-  publishButton.setAttribute('disabled', true)
6
-  publishButton.classList.add('o-40')
7
-  const allHtml = quill.root.innerHTML
8
-  const data = {
9
-    content: allHtml,
10
-    syndicationTargets: [],
11
-    categories: [],
12
-    slug: []
13
-  }
14
-
15
-  const csrfToken = document.querySelector('input[name=csrfToken]')!.getAttribute('value')
16
-
17
-  fetch('/~/editor/post', {
18
-    method: 'PUT',
19
-    mode: 'same-origin',
20
-    cache: 'no-cache',
21
-    credentials: 'same-origin',
1
+import Quill from "quill";
2
+
3
+function disablePublishButton() {
4
+  const publishButton = document.querySelector("#publish")!;
5
+  publishButton.setAttribute("disabled", "disabled");
6
+  publishButton.classList.add("o-20");
7
+}
8
+
9
+function getSelectedCategories() {
10
+  const categoryElem: HTMLSelectElement = document.querySelector(
11
+    "#categories"
12
+  )!;
13
+  const categories = Array.from(categoryElem!.selectedOptions).map(
14
+    (o: HTMLOptionElement) => o.value
15
+  );
16
+  console.log("categories", categories);
17
+  return categories;
18
+}
19
+
20
+function getContentHtml(quill: Quill) {
21
+  return quill.root.innerHTML;
22
+}
23
+
24
+function getSlug() {
25
+  const slugElem: HTMLInputElment = document.querySelector("#slug");
26
+  return slugElem!.value || null;
27
+}
28
+
29
+function getSyndicationTargets() {
30
+  const syndicationTargetElem: HTMLSelectElement = document.querySelector(
31
+    "#syndicationTargets"
32
+  );
33
+  return Array.from(syndicationTargetElem!.selectedOptions).map(
34
+    (o: HTMLOptionElement) => o.value
35
+  );
36
+}
37
+
38
+function getMicropubBody(quill: Quill) {
39
+  return {
40
+    content: getContentHtml(quill),
41
+    categories: getSelectedCategories(),
42
+    slug: getSlug(),
43
+    "mp-syndicate-to": getSyndicationTargets()
44
+  };
45
+}
46
+
47
+function getCsrfToken() {
48
+  return document.querySelector("input[name=csrfToken]")!.getAttribute("value");
49
+}
50
+
51
+function enablePublishButton() {
52
+  const publishButton = document.querySelector("#publish")!;
53
+  publishButton.removeAttribute("disabled");
54
+  publishButton.classList.remove("o-20");
55
+}
56
+
57
+function clearContent(quill: Quill) {
58
+  quill.root.innerHTML = "";
59
+}
60
+
61
+function compileRequest(quill: Quill) {
62
+  disablePublishButton();
63
+  const bodyString = JSON.stringify(getMicropubBody(quill));
64
+  console.log(bodyString);
65
+
66
+  fetch("/~/editor/post", {
67
+    method: "PUT",
68
+    mode: "same-origin",
69
+    cache: "no-cache",
70
+    credentials: "same-origin",
22 71
     headers: {
23
-      'content-type': 'application/json',
24
-      'x-csrf-token': csrfToken || ''
72
+      "content-type": "application/json",
73
+      "x-csrf-token": getCsrfToken()
25 74
     },
26
-    redirect: 'follow',
27
-    body: JSON.stringify(data)
28
-  }).then(resp => {
29
-    publishButton.removeAttribute('disabled')
30
-    publishButton.classList.remove('o-40')
75
+    body: bodyString
76
+  }).then((resp: Response) => {
77
+    enablePublishButton();
78
+    console.log(resp);
31 79
     switch (resp.status) {
32 80
       case 200:
33
-        quill.root.innerHTML = ''
34
-        break
81
+        clearContent(quill);
82
+        break;
35 83
       default:
36
-        break
84
+        break;
37 85
     }
38
-  })
86
+  });
39 87
 }
40 88
 
41 89
 // TODO: Add custom handling for images
42 90
 // TODO: Add custom handling for mentioning a person.
43 91
 // TODO: Add custom handling for changing the type of post.
44
-function startEditorPage () {
92
+function startEditorPage() {
45 93
   const toolbarOptions = {
46
-    container: '#toolbar',
47
-    handlers: { 'emoji': function () {} }
48
-  }
94
+    container: "#toolbar",
95
+    handlers: { emoji: function() {} }
96
+  };
49 97
 
50 98
   const options = {
51
-    placeholder: 'Compose an epic...',
99
+    placeholder: "Compose an epic...",
52 100
     readOnly: false,
53
-    scrollingContainer: '#editorContainer',
54
-    theme: 'snow',
101
+    scrollingContainer: "#editorContainer",
102
+    theme: "snow",
55 103
     modules: {
56 104
       toolbar: toolbarOptions
57 105
     }
58
-  }
106
+  };
59 107
 
60
-  const quill = new Quill('#editor', options)
108
+  const quill = new Quill("#editor", options);
61 109
 
62
-  const publishButton = document.querySelector('#publish')!
63
-  publishButton.addEventListener('click', () => compileRequest(quill))
110
+  const publishButton = document.querySelector("#publish")!;
111
+  publishButton.addEventListener("click", () => compileRequest(quill));
64 112
 }
65 113
 
66
-window.addEventListener('load', startEditorPage)
114
+window.addEventListener("load", startEditorPage);

+ 8
- 7
web/templates/editor/index.html.eex View File

@@ -18,6 +18,7 @@
18 18
       <%= current_user!(@conn)["card"]["name"] %>
19 19
     </a>
20 20
   </div>
21
+  <div class="flex flex-row">
21 22
   <div class="flex flex-column flex-auto flex-grow">
22 23
     <div id="toolbar" class="db bg-white">
23 24
       <select class="ql-size">
@@ -37,9 +38,9 @@
37 38
       <div id="editor" class="bg-white"></div>
38 39
     </div>
39 40
   </div>
40
-  <div class="w-100 flex flex-row pa2 pv3 ph0-l">
41
+  <div class="w-third mw4 flex flex-column pa2 pv3 ph0-l">
41 42
     <div class="flex-auto flex-grow flex-wrap flex-row flex">
42
-      <div class="pa1 w-third">
43
+      <div class="pa1">
43 44
         <details class="w-100">
44 45
           <summary class="w-100 near-black tc o-30 glow pointer f6 b db mb2">Also Post To&#8615;</summary>
45 46
           <select multiple size=3 id="syndicationTargets" class="v-mid pa1 input-reset bw1 b--solid b--black-40 overflow-auto mw5 w-100">
@@ -49,7 +50,7 @@
49 50
           </select>
50 51
         </details>
51 52
       </div>
52
-      <div class="pa1 w-third">
53
+      <div class="pa1">
53 54
         <details class="w-100">
54 55
           <summary class="w-100 near-black tc o-30 glow pointer f6 b db mb2">Categories &#8615;</summary>
55 56
           <select multiple size=3 id="categories" class="v-mid pa1 input-reset bw1 b--solid b--black-40 overflow-auto mw5 w-100">
@@ -59,13 +60,13 @@
59 60
           </select>
60 61
         </details>
61 62
       </div>
62
-      <div class="pa1 w-third">
63
+      <div class="pa1">
63 64
         <details class="w-100">
64 65
           <summary class="w-100 near-black tc f6 b o-30 glow pointer db mb2">Slug  &#8615;</summary>
65
-          <input type="text" class="input-reset pa1 dib b--black-40 b--solid bw1 v-mid w-100">
66
+          <input id="slug" type="text" class="input-reset pa1 dib b--black-40 b--solid bw1 v-mid w-100">
66 67
         </details>
67 68
       </div>
68
-      <div class="pa1 w-third">
69
+      <div class="pa1">
69 70
         <details class="w-100">
70 71
           <summary class="w-100 near-black tc f6 b o-30 glow pointer db mb2">Date &#8615;</summary>
71 72
           <div class="flex flex-column">
@@ -75,7 +76,7 @@
75 76
         </details>
76 77
       </div>
77 78
       <%= if "visibility" in @properties do %>
78
-      <div class="pa1 w-third">
79
+      <div class="pa1">
79 80
         <details class="w-100">
80 81
           <summary class="w-100 near-black tc f6 b o-30 glow pointer db mb2">Visibility &#8615;</summary>
81 82
           Not yet supported.

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

@@ -1,4 +1,4 @@
1
-<header>
1
+<header class="bg-white-80">
2 2
   <nav class="flex flex-row justify-between items-center center w-100 pa1 h-x-app h-app">
3 3
     <a class="u-url p-name link dark-blue pa2 v-mid br2 f3" href="#"><span class="i serif v-mid fw1">k</span>&nbsp;<span class="ttu v-mid f3 f2-l tracked fw9">Publish</span></a>
4 4
     <div class="pa3 dark-blue f5 br2 flex flex-row items-center">

+ 0
- 1
web/views/layout_view.ex View File

@@ -1,4 +1,3 @@
1 1
 defmodule Koype.Publish.Web.LayoutView do
2 2
   use Koype.Publish.Web, :view
3
-  alias Koype.Publish.Guardian.Plug, as: KGPlug
4 3
 end

Loading…
Cancel
Save