Browse Source

feat(setup): Add flexible system.

jackyalcine 11 months ago
parent
commit
f8f73d60a7
Signed by: Jacky Alciné <yo@jacky.wtf> GPG Key ID: 36CD7728BDFD66FF
59 changed files with 833 additions and 495 deletions
  1. 6
    6
      .postcssrc.js
  2. 6
    4
      Dockerfile
  3. 0
    0
      LICENSE.Koype
  4. 2
    2
      config/config.exs
  5. 1
    2
      config/dev.exs
  6. 6
    1
      config/test.exs
  7. 5
    3
      docker-compose.yml
  8. 9
    1
      docker/rootfs/root/.npmrc
  9. 5
    1
      docker/scripts/build.sh
  10. 0
    0
      docker/scripts/cleanup.sh
  11. 0
    1
      docker/scripts/docker-healthcheck.sh
  12. 0
    4
      docker/scripts/docker-post-deploy.sh
  13. 0
    18
      docker/scripts/docker-pre-deploy.sh
  14. 0
    0
      docker/scripts/entrypoint.sh
  15. 6
    0
      docker/scripts/healthcheck.sh
  16. 4
    0
      docker/scripts/post-deploy.sh
  17. 18
    0
      docker/scripts/pre-deploy.sh
  18. 12
    7
      docker/scripts/prepare.sh
  19. 12
    13
      lib/indieweb/post.ex
  20. 1
    9
      lib/koype.ex
  21. 7
    0
      lib/profile.ex
  22. 6
    4
      lib/repo/otp_secret.ex
  23. 15
    0
      lib/setup.ex
  24. 1
    2
      mix.exs
  25. 1
    1
      mix.lock
  26. 50
    4
      package-lock.json
  27. 6
    3
      package.json
  28. 16
    2
      test/acceptance/setup_flow_test.exs
  29. 101
    35
      test/integration/controllers/setup_controller_test.exs
  30. 3
    3
      test/support/factory.ex
  31. 9
    3
      test/unit/repo/otp_secret_test.exs
  32. 0
    0
      tmp/apk/lib/.gitkeep
  33. 1
    1
      web/channels/user_socket.ex
  34. 9
    14
      web/controllers/page_controller.ex
  35. 7
    0
      web/controllers/rel_me_controller.ex
  36. 97
    116
      web/controllers/setup_controller.ex
  37. 3
    7
      web/router.ex
  38. 12
    13
      web/static/css/koype.scss
  39. 6
    0
      web/static/css/vendor.scss
  40. 0
    3
      web/static/js/koype.test.ts
  41. 4
    1
      web/static/js/page.ts
  42. 0
    47
      web/static/js/request.test.ts
  43. 1
    1
      web/static/js/request.ts
  44. 151
    0
      web/static/js/setup.ts
  45. 1
    1
      web/templates/entry/_category.html.eex
  46. 3
    3
      web/templates/entry/_sparkline.html.eex
  47. 1
    1
      web/templates/entry/_webmentions.html.eex
  48. 3
    2
      web/templates/layout/app.html.eex
  49. 10
    8
      web/templates/page/_footer.html.eex
  50. 20
    22
      web/templates/page/_header.html.eex
  51. 11
    0
      web/templates/rel_me/view.html.eex
  52. 0
    7
      web/templates/setup/index.html.eex
  53. 39
    0
      web/templates/setup/setup-auth.html.eex
  54. 0
    48
      web/templates/setup/setup-otp.html.eex
  55. 80
    43
      web/templates/setup/setup-profile.html.eex
  56. 25
    0
      web/templates/setup/view.html.eex
  57. 17
    16
      web/views/entry_view.ex
  58. 5
    0
      web/views/rel_me_view.ex
  59. 19
    12
      web/views/setup_view.ex

+ 6
- 6
.postcssrc.js View File

@@ -16,16 +16,16 @@ module.exports = {
16 16
     'postcss-urlrewrite': {
17 17
       imports: true,
18 18
       rules: [{
19
-        from: /source-/,
20
-        to: '/assets/css/source-'
19
+        from: /css\/images/,
20
+        to: '/assets/images/'
21 21
       }]
22 22
     },
23 23
     'postcss-pxtorem': {
24
-      unitPrecision: 4,
24
+      unitPrecision: 8,
25 25
       propWhiteList: [],
26 26
       replace: true,
27 27
       selectorBlackList: [],
28
-      minPixelValue: 4
28
+      minPixelValue: 8
29 29
     },
30 30
     cssnano: ENV === 'production' ? {
31 31
       preset: [
@@ -40,10 +40,10 @@ module.exports = {
40 40
     } : false,
41 41
     'css-mqpacker': [],
42 42
     'postcss-url': [{
43
-      filter: './files/source**',
43
+      filter: './css/images/*',
44 44
       url: 'copy',
45 45
       assetsPath: './',
46
-      useHash: false
46
+      useHash: true
47 47
     }, ],
48 48
     'mdcss': {
49 49
       theme: require('mdcss-theme-github')

+ 6
- 4
Dockerfile View File

@@ -7,19 +7,21 @@ ENV MIX_ENV=${MIX_ENV:-prod} \
7 7
   TZ=Etc/UTC \
8 8
   REPLACE_OS_VARS=true
9 9
 
10
+ADD docker/rootfs/ /
11
+
10 12
 RUN mkdir /tmp/koype-docker
11 13
 COPY docker/rootfs /
12 14
 ADD docker/scripts/ /tmp/koype-docker/
13 15
 
14
-RUN sh /tmp/koype-docker/docker-prepare.sh
16
+RUN sh /tmp/koype-docker/prepare.sh
15 17
 
16 18
 WORKDIR /opt/koype
17 19
 
18 20
 COPY . /opt/koype/
19
-RUN sh /tmp/koype-docker/docker-build.sh
21
+RUN sh /tmp/koype-docker/build.sh
20 22
 
21 23
 VOLUME /opt/koype/priv/repo/db
22
-RUN sh /tmp/koype-docker/docker-cleanup.sh
24
+RUN sh /tmp/koype-docker/cleanup.sh
23 25
 
24 26
 SHELL ["/bin/bash"]
25
-CMD ["/tmp/koype-docker/docker-entrypoint.sh"]
27
+CMD ["/tmp/koype-docker/entrypoint.sh"]

LICENSE → LICENSE.Koype View File


+ 2
- 2
config/config.exs View File

@@ -27,7 +27,7 @@ config :koype, Koype.Guardian,
27 27
   secret_key: {:system, :string, "GUARDIAN_SECRET_KEY"},
28 28
   schema_name: "guardian_tokens",
29 29
   token_types: ["refresh_token"],
30
-  sweep_interval: 60
30
+  sweep_interval: 15
31 31
 
32 32
 config :logger, :console,
33 33
   format: "$time $metadata[$level] $message\n",
@@ -58,7 +58,7 @@ config :arc,
58 58
 
59 59
 config :ex_aws, :hackney_opts,
60 60
   follow_redirect: true,
61
-  recv_timeout: 30_000
61
+  recv_timeout: 3_000
62 62
 
63 63
 config :ex_aws,
64 64
   storage_dir: "koype",

+ 1
- 2
config/dev.exs View File

@@ -13,10 +13,9 @@ config :koype, Koype.Web.Endpoint,
13 13
     :elixir
14 14
   ],
15 15
   live_reload: [
16
-    url: "ws://localhost:5000",
17 16
     interval: 100,
18 17
     patterns: [
19
-      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
18
+      ~r{priv/static/assets/*/.*(js|css|png|jpeg|jpg|gif|svg)$},
20 19
       ~r{priv/gettext/.*(po)$},
21 20
       ~r{web/views/.*(ex)$},
22 21
       ~r{web/templates/.*(eex)$}

+ 6
- 1
config/test.exs View File

@@ -7,6 +7,11 @@ config :koype, Koype.Repo,
7 7
   pool: Ecto.Adapters.SQL.Sandbox,
8 8
   database: "priv/repo/db/test.db"
9 9
 
10
+config :koype, Koype.Web.Endpoint,
11
+  debug_errors: true,
12
+  code_reloader: false,
13
+  check_origin: false
14
+
10 15
 config :logger, level: :debug
11 16
 
12 17
 config :exvcr,
@@ -22,7 +27,7 @@ config :exvcr,
22 27
 config :hound,
23 28
   driver: "chrome_driver",
24 29
   host: "webdriver",
25
-  app_host: "http://site",
30
+  app_host: "http://localhost",
26 31
   app_port: 5000,
27 32
   port: 4444,
28 33
   path_prefix: "wd/hub/"

+ 5
- 3
docker-compose.yml View File

@@ -38,6 +38,8 @@ services:
38 38
       - "./tmp/docker/redis:/data"
39 39
     networks:
40 40
       - network
41
+    ports:
42
+      - 6379
41 43
   objectstorage:
42 44
     image: "minio/minio:RELEASE.2018-11-22T02-51-56Z"
43 45
     command: server /data
@@ -51,7 +53,7 @@ services:
51 53
       MINIO_REGION: "local"
52 54
       MINIO_BROWSER: "on"
53 55
     ports:
54
-      - "4001:9000"
56
+      - "9000:9000"
55 57
     networks:
56 58
       - network
57 59
   site:
@@ -85,8 +87,8 @@ services:
85 87
       - "./:/opt/koype"
86 88
       - "./priv/repo/db:/opt/koype/priv/repo/db"
87 89
     healthcheck:
88
-      test: ["CMD", "/tmp/koype-docker/docker-healthcheck.sh"]
89
-      interval: 10s
90
+      test: ["CMD", "/tmp/koype-docker/healthcheck.sh"]
91
+      interval: 60s
90 92
       timeout: 5s
91 93
       retries: 5
92 94
     networks:

+ 9
- 1
docker/rootfs/root/.npmrc View File

@@ -1,3 +1,11 @@
1
-verbose=true
2 1
 color=false
3 2
 exact=true
3
+link=false
4
+long=true
5
+optional=false
6
+progress=true
7
+save-exact=true
8
+usage=false
9
+verbose=true
10
+version=true
11
+viewer=less

docker/scripts/docker-build.sh → docker/scripts/build.sh View File

@@ -1,7 +1,11 @@
1 1
 #!/bin/sh
2 2
 
3
+HEX_HTTP_CONCURRENCY=5
4
+HEX_HTTP_TIMEOUT=600
5
+NODE_ENV=${ENV}
6
+
3 7
 echo " ---> [npm] Pulling dependencies..."
4
-NODE_ENV=${ENV} npm install --no-bin-links || exit 50
8
+npm install --no-bin-links || exit 50
5 9
 
6 10
 echo " ---> [mix] Preparing..."
7 11
 mix local.hex --force || exit 10

docker/scripts/docker-cleanup.sh → docker/scripts/cleanup.sh View File


+ 0
- 1
docker/scripts/docker-healthcheck.sh View File

@@ -2,5 +2,4 @@
2 2
 
3 3
 # TODO: Hit an endpoint and expect "OK", 200 to come back.
4 4
 
5
-
6 5
 curl -f "http://${HOST}:5000/version"

+ 0
- 4
docker/scripts/docker-post-deploy.sh View File

@@ -1,4 +0,0 @@
1
-#!/bin/sh
2
-
3
-echo "[npm] Building client JavaScript..."
4
-npm run parcel:build

+ 0
- 18
docker/scripts/docker-pre-deploy.sh View File

@@ -1,18 +0,0 @@
1
-#!/bin/sh
2
-
3
-cd /opt/koype || exit 10
4
-
5
-echo " ---> [deploy:pre] Creating database file..."
6
-touch "/opt/koype/priv/repo/db/${MIX_ENV:-prod}.db" || exit 15
7
-
8
-echo " ---> [deploy:pre] Setting up database..."
9
-mix ecto.setup || exit 20
10
-
11
-echo " ---> [deploy:pre] Update static digests..."
12
-mix phx.digest || exit 40
13
-
14
-echo " ---> [deploy:pre] Confirm permissions on object storage..."
15
-# mix koype.storage_check || exit 50
16
-
17
-echo " ---> [deploy:pre] Run production-ready checks... "
18
-# mix koype.smoke_test || exit 60

docker/scripts/docker-entrypoint.sh → docker/scripts/entrypoint.sh View File


+ 6
- 0
docker/scripts/healthcheck.sh View File

@@ -0,0 +1,6 @@
1
+#!/bin/sh
2
+
3
+# TODO: Hit an endpoint and expect "OK", 200 to come back.
4
+
5
+echo " ----> [healthcheck] <skip> Creating database file..."
6
+curl -f "http://${HOST}:5000/version"

+ 4
- 0
docker/scripts/post-deploy.sh View File

@@ -0,0 +1,4 @@
1
+#!/bin/sh
2
+
3
+echo " ----> [npm] Building client JavaScript..."
4
+npm run parcel:build

+ 18
- 0
docker/scripts/pre-deploy.sh View File

@@ -0,0 +1,18 @@
1
+#!/bin/sh
2
+
3
+cd /opt/koype || exit 10
4
+
5
+echo " ----> [deploy:pre] Creating database file..."
6
+touch "/opt/koype/priv/repo/db/${MIX_ENV:-prod}.db" || exit 15
7
+
8
+echo " ----> [deploy:pre] Setting up database..."
9
+mix ecto.setup || exit 20
10
+
11
+echo " ----> [deploy:pre] Update static digests..."
12
+mix phx.digest || exit 40
13
+
14
+echo " ----> [deploy:pre] <skip> Confirm permissions on object storage..."
15
+# mix koype.storage_check || exit 50
16
+
17
+echo " ----> [deploy:pre] <skip> Run production-ready checks... "
18
+# mix koype.smoke_test || exit 60

docker/scripts/docker-prepare.sh → docker/scripts/prepare.sh View File

@@ -1,30 +1,35 @@
1 1
 #!/bin/sh
2 2
 
3
+ONE_WEEK_IN_MINUTES=10080 
4
+
5
+echo " ---> [apk] Syncing repos (allowed to fail)..."
6
+apk update --cache-max-age="${ONE_WEEK_IN_MINUTES}" --verbose
7
+
3 8
 echo " ---> [apk] Fetching baseline packages..."
4
-apk add --update --no-cache --verbose \
9
+apk add --verbose \
5 10
   bash \
6 11
   ca-certificates \
7 12
   coreutils \
8
-  shadow \
9 13
   tzdata \
10 14
   curl \
11 15
   sqlite-libs \
12 16
   inotify-tools \
13 17
   imagemagick \
14
-  ffmpeg \
15
-  sox \
16
-  gcc \
17
-  util-linux \
18 18
   nodejs=8.14.0-r0 \
19 19
   npm=8.14.0-r0 \
20 20
   || exit 20
21 21
 
22 22
 echo " ---> [apk] Fetching development packages..."
23
-apk add --update --no-cache --virtual=build --verbose \
23
+apk add --virtual=build --verbose \
24
+  gcc \
25
+  util-linux \
24 26
   build-base \
25 27
   binutils-dev \
26 28
   libelf-dev \
27 29
   sqlite-dev \
28 30
   || exit 30
29 31
 
32
+echo " ---> [apk] Syncing cache... "
33
+apk cache sync
34
+
30 35
 mkdir -p /opt/koype

+ 12
- 13
lib/indieweb/post.ex View File

@@ -17,30 +17,31 @@ defmodule IndieWeb.Post do
17 17
   def known_types do
18 18
     [
19 19
       %{type: "article", name: "Article", names: "Articles"},
20
-      # %{type: "audio", name: "Audio", names: "Audio"},
20
+      %{type: "audio", name: "Audio", names: "Audio"},
21 21
       %{type: "bookmark", name: "Bookmark", names: "Bookmarks"},
22 22
       %{type: "checkin", name: "Check In", names: "Check Ins"},
23 23
       %{type: "like", name: "Like", names: "Likes"},
24
-      # %{type: "listen", name: "Listen", names: "Listens"},
24
+      %{type: "listen", name: "Listen", names: "Listens"},
25 25
       %{type: "note", name: "Note", names: "Notes"},
26 26
       %{type: "reply", name: "Reply", names: "Replies"},
27 27
       %{type: "repost", name: "Repost", names: "Reposts"},
28
-      # %{type: "donation", name: "Donation", names: "Donations"},
29
-      # %{type: "payment", name: "Payment", names: "Payments"},
30
-      %{type: "event", name: "Event", names: "Events"}
28
+      %{type: "donation", name: "Donation", names: "Donations"},
29
+      %{type: "payment", name: "Payment", names: "Payments"},
30
+      %{type: "event", name: "Event", names: "Events"},
31 31
       # %{type: "exercise", name: "Workout", names: "Workouts"},
32
-      # %{type: "follow", name: "Follow", names: "Follows"},
32
+      %{type: "follow", name: "Follow", names: "Follows"},
33 33
       # %{type: "food", name: "Food", names: "Meals"},
34
-      # %{type: "gameplay", name: "Game Play", names: "Game Plays"},
34
+      %{type: "gameplay", name: "Game Play", names: "Game Plays"},
35 35
       # %{type: "issue", name: "Issue", names: "Issues"},
36
-      # %{type: "photo", name: "Photo", names: "Photos"},
36
+      %{type: "photo", name: "Photo", names: "Photos"},
37 37
       # %{type: "presentation", name: "Presentation", names: "Presentations"},
38 38
       # %{type: "quotation", name: "Quotation", names: "Quotations"},
39
-      # %{type: "read", name: "Read", names: "Reads"},
39
+      %{type: "read", name: "Read", names: "Reads"},
40 40
       # %{type: "sleep", name: "Sleep", names: "Sleeps"},
41 41
       # %{type: "venue", name: "Venue", names: "Venues"},
42
-      # %{type: "watch", name: "Watch", names: "Watches"},
43
-      # %{type: "video", name: "Video", names: "Videos"},
42
+      %{type: "watch", name: "Watch", names: "Watches"},
43
+      %{type: "video", name: "Video", names: "Videos"},
44
+      %{type: "rsvp", name: "RSVP", names: "RSVPs"}
44 45
     ]
45 46
   end
46 47
 
@@ -152,8 +153,6 @@ defmodule IndieWeb.Post do
152 153
     do: bookmark
153 154
 
154 155
   def get_properties_for_type(:checkin, %{location: location} = data) when is_binary(location) do
155
-    Apex.ap(data)
156
-
157 156
     if String.starts_with?(location, "geo:") do
158 157
       regex = ~r/geo\:c(?<lat>\d+)\,c(?<lng>\d+)/
159 158
       %{lat: lat, lng: lng} = Regex.named_captures(regex, location)

+ 1
- 9
lib/koype.ex View File

@@ -13,19 +13,11 @@ defmodule Koype do
13 13
         scheme <> "://" <> host
14 14
 
15 15
       _ ->
16
-        "http://localhost"
16
+        Koype.Web.Endpoint.url()
17 17
     end
18 18
   end
19 19
 
20 20
   def version do
21 21
     Koype.Mixfile.project()[:version]
22 22
   end
23
-
24
-  def ready_for_use? do
25
-    cond do
26
-      Profile.complete?() == false -> {:error, :profile}
27
-      OtpSecret.current() == nil -> {:error, :otp}
28
-      true -> :ok
29
-    end
30
-  end
31 23
 end

+ 7
- 0
lib/profile.ex View File

@@ -52,6 +52,13 @@ defmodule Koype.Profile do
52 52
   def note(), do: get("note")
53 53
   def email(), do: get("email")
54 54
 
55
+  def photo() do
56
+    case get("photo") do
57
+      nil -> nil
58
+      path -> Koype.Storage.Photo.url({path, :floating}, :original, signed: false)
59
+    end
60
+  end
61
+
55 62
   def flagship_entry() do
56 63
     case get("flagship_entry_id") do
57 64
       nil -> nil

+ 6
- 4
lib/repo/otp_secret.ex View File

@@ -1,6 +1,8 @@
1 1
 defmodule Koype.Repo.OtpSecret do
2
+  @moduledoc "Logic for OTP secrets."
2 3
   @secret_age 60 * 60 * 24 * 30
3 4
   @required_attrs ~w(secret expired_at)a
5
+  @otp_secret_size 16
4 6
 
5 7
   use Ecto.Schema
6 8
   import Ecto.Changeset
@@ -74,12 +76,12 @@ defmodule Koype.Repo.OtpSecret do
74 76
     end
75 77
   end
76 78
 
77
-  def generate() do
78
-    Base.encode32(:crypto.strong_rand_bytes(16))
79
-  end
79
+  def generate(), do: @otp_secret_size |> :crypto.strong_rand_bytes() |> Base.encode32()
80 80
 
81 81
   def confirm_against_secret(secret, code) do
82
-    if Totpex.generate_totp(secret) == code do
82
+    Apex.ap([secret, Totpex.generate_totp(secret), code])
83
+
84
+    if Totpex.validate_totp(secret, code, grace_periods: 2) do
83 85
       :ok
84 86
     else
85 87
       {:error, :otp_code_mismatch}

+ 15
- 0
lib/setup.ex View File

@@ -0,0 +1,15 @@
1
+defmodule Koype.Setup do
2
+  @moduledoc """
3
+  Provides logic for discovering and handling prerequistie configuration of Koype.
4
+  """
5
+
6
+  def complete?(), do: state() == :ok
7
+
8
+  def state() do
9
+    cond do
10
+      Koype.Profile.complete?() == false -> :profile
11
+      Koype.Repo.OtpSecret.current() == nil -> :auth
12
+      true -> :ok
13
+    end
14
+  end
15
+end

+ 1
- 2
mix.exs View File

@@ -94,8 +94,7 @@ defmodule Koype.Mixfile do
94 94
       {:phoenix_live_reload, "~> 1.0", only: :dev},
95 95
       {:phoenix_pubsub, "~> 1.0"},
96 96
       {:plug_cowboy, "~> 1.0.0"},
97
-      {:plug_ribbon, "~> 0.2.0"},
98
-      {:pretty_print_formatter, "~> 0.1.0", only: :dev},
97
+      {:pretty_print_formatter, "~> 0.1.5", only: :dev},
99 98
       {:redix, "~> 0.8.0"},
100 99
       {:scrivener_ecto, "~> 1.0"},
101 100
       {:seedex, "~> 0.1.2"},

+ 1
- 1
mix.lock View File

@@ -78,7 +78,7 @@
78 78
   "plug_ribbon": {:hex, :plug_ribbon, "0.2.1", "98b08f5c67c93dc3c2c635baec95b68f9697ceca4b7a6839e5870874bdb9a405", [:mix], [{:plug, ">= 0.13.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
79 79
   "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
80 80
   "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
81
-  "pretty_print_formatter": {:hex, :pretty_print_formatter, "0.1.4", "64ed3ccd3bd870af2068457116b24e5fcf013154e9fc0dbfac5724bcb38a39cf", [:mix], [], "hexpm"},
81
+  "pretty_print_formatter": {:hex, :pretty_print_formatter, "0.1.5", "e1aa7f0e03c5b4fc33c32083beea4d7ece1070e9250a2e9e692cead6940f2eaf", [:mix], [], "hexpm"},
82 82
   "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
83 83
   "recase": {:hex, :recase, "0.3.0", "a3a6b2bfc9a1c3047b6f37d49ea52027ea59fd256505254b8e9d63c68d09ab89", [:mix], [], "hexpm"},
84 84
   "redix": {:hex, :redix, "0.8.2", "c25158f905bcf8842e9a11411d65b9257ac70057c4330521d1a4d2a44b4f7ecf", [:mix], [], "hexpm"},

+ 50
- 4
package-lock.json View File

@@ -2672,8 +2672,7 @@
2672 2672
     "deep-equal": {
2673 2673
       "version": "1.0.1",
2674 2674
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
2675
-      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
2676
-      "dev": true
2675
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
2677 2676
     },
2678 2677
     "deep-is": {
2679 2678
       "version": "0.1.3",
@@ -3093,6 +3092,11 @@
3093 3092
       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
3094 3093
       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
3095 3094
     },
3095
+    "eventemitter3": {
3096
+      "version": "2.0.3",
3097
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
3098
+      "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo="
3099
+    },
3096 3100
     "events": {
3097 3101
       "version": "1.1.1",
3098 3102
       "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
@@ -3276,8 +3280,7 @@
3276 3280
     "extend": {
3277 3281
       "version": "3.0.2",
3278 3282
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
3279
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
3280
-      "dev": true
3283
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
3281 3284
     },
3282 3285
     "extend-shallow": {
3283 3286
       "version": "3.0.2",
@@ -3447,6 +3450,11 @@
3447 3450
       "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
3448 3451
       "dev": true
3449 3452
     },
3453
+    "fast-diff": {
3454
+      "version": "1.1.2",
3455
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
3456
+      "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
3457
+    },
3450 3458
     "fast-glob": {
3451 3459
       "version": "2.2.4",
3452 3460
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.4.tgz",
@@ -8216,6 +8224,11 @@
8216 8224
         }
8217 8225
       }
8218 8226
     },
8227
+    "parchment": {
8228
+      "version": "1.1.4",
8229
+      "resolved": "http://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
8230
+      "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
8231
+    },
8219 8232
     "parse-asn1": {
8220 8233
       "version": "5.1.1",
8221 8234
       "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
@@ -9886,6 +9899,11 @@
9886 9899
       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
9887 9900
       "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
9888 9901
     },
9902
+    "qartjs": {
9903
+      "version": "1.1.2",
9904
+      "resolved": "https://registry.npmjs.org/qartjs/-/qartjs-1.1.2.tgz",
9905
+      "integrity": "sha1-UWU6WjAQiJ69NmL9knwnps6ntgQ="
9906
+    },
9889 9907
     "qrcode-generator": {
9890 9908
       "version": "1.4.1",
9891 9909
       "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.1.tgz",
@@ -9922,6 +9940,29 @@
9922 9940
       "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=",
9923 9941
       "dev": true
9924 9942
     },
9943
+    "quill": {
9944
+      "version": "1.3.6",
9945
+      "resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
9946
+      "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
9947
+      "requires": {
9948
+        "clone": "^2.1.1",
9949
+        "deep-equal": "^1.0.1",
9950
+        "eventemitter3": "^2.0.3",
9951
+        "extend": "^3.0.1",
9952
+        "parchment": "^1.1.4",
9953
+        "quill-delta": "^3.6.2"
9954
+      }
9955
+    },
9956
+    "quill-delta": {
9957
+      "version": "3.6.3",
9958
+      "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
9959
+      "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
9960
+      "requires": {
9961
+        "deep-equal": "^1.0.1",
9962
+        "extend": "^3.0.2",
9963
+        "fast-diff": "1.1.2"
9964
+      }
9965
+    },
9925 9966
     "quote-stream": {
9926 9967
       "version": "1.0.2",
9927 9968
       "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz",
@@ -12441,6 +12482,11 @@
12441 12482
         "indexof": "0.0.1"
12442 12483
       }
12443 12484
     },
12485
+    "voca": {
12486
+      "version": "1.4.0",
12487
+      "resolved": "https://registry.npmjs.org/voca/-/voca-1.4.0.tgz",
12488
+      "integrity": "sha512-8Xz4H3vhYRGbFupLtl6dHwMx0ojUcjt0HYkqZ9oBCfipd/5mD7Md58m2/dq7uPuZU/0T3Gb1m66KS9jn+I+14Q=="
12489
+    },
12444 12490
     "w3c-hr-time": {
12445 12491
       "version": "1.0.1",
12446 12492
       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",

+ 6
- 3
package.json View File

@@ -8,8 +8,8 @@
8 8
     "test": "test/web"
9 9
   },
10 10
   "scripts": {
11
-    "parcel:watch": "parcel serve web/static/js/koype.ts web/static/css/koype.scss --out-dir priv/static/assets --public-url ./ --cache-dir tmp/parcel --no-cache --log-level 5 --global Koype",
12
-    "parcel:build": "parcel build web/static/js/koype.ts web/static/css/koype.scss --out-dir priv/static/assets --public-url ./ --cache-dir tmp/parcel --no-cache --log-level 4 --global Koype --detailed-report",
11
+    "parcel:watch": "parcel serve web/static/js/*.ts web/static/css/*.scss --out-dir priv/static/assets --public-url ./ --cache-dir tmp/parcel --no-cache --log-level 5 --global Koype",
12
+    "parcel:build": "parcel build web/static/js/*.ts web/static/css/*.scss --out-dir priv/static/assets --public-url ./ --cache-dir tmp/parcel --no-cache --log-level 4 --global Koype --detailed-report",
13 13
     "test": "jest"
14 14
   },
15 15
   "repository": {
@@ -37,18 +37,21 @@
37 37
     "postcss-scss": "2.0.0",
38 38
     "postcss-strip-inline-comments": "0.1.5",
39 39
     "pretty-checkbox": "3.0.3",
40
+    "qartjs": "1.1.2",
40 41
     "qrcode-generator": "1.4.1",
42
+    "quill": "^1.3.6",
41 43
     "sweet-alert": "^2.0.5",
42 44
     "sweetalert": "^2.1.2",
43 45
     "tachyons": "4.10.0",
44 46
     "tachyons-sass": "4.9.5",
45 47
     "timeago.js": "4.0.0-beta.1",
46 48
     "ts-node": "7.0.1",
49
+    "voca": "1.4.0",
47 50
     "webfontloader": "^1.6.28"
48 51
   },
49 52
   "engines": {
50 53
     "node": "8.14.0",
51
-    "npm": "6.4.0"
54
+    "npm": "6.5.0"
52 55
   },
53 56
   "devDependencies": {
54 57
     "@types/jest": "23.3.10",

+ 16
- 2
test/acceptance/setup_flow_test.exs View File

@@ -4,9 +4,23 @@ defmodule Koype.Acceptance.SetupFlowTest do
4 4
   hound_session()
5 5
 
6 6
   describe "navigating to setup for first test" do
7
-    test "try it out" do
7
+    test "fully setup Koype on first run" do
8
+      # TODO: Wipe database clean.
8 9
       navigate_to("/")
9
-      assert page_title() =~ "Getting Started"
10
+      assert page_title() =~ "Setting Up - koype"
11
+      assert current_path() == setup_path(Koype.Web.Endpoint, :view)
12
+
13
+      # On /setup
14
+      email = Faker.Internet.email()
15
+      name = Faker.Name.first_name()
16
+      nickname = Faker.Internet.user_name()
17
+      fill_field({:name, "name"}, name)
18
+      fill_field({:name, "nickname"}, nickname)
19
+      fill_field({:name, "email"}, email)
20
+      fill_field({:name, "note"}, Faker.Lorem.sentence())
21
+      # fill_field({:name, "photo"}, Faker.Avatar.image_url)
22
+      submit_element({:name, "name"})
23
+      assert page_title() =~ "Setting Up - koype"
10 24
       assert current_path() == setup_path(Koype.Web.Endpoint, :view)
11 25
     end
12 26
   end

+ 101
- 35
test/integration/controllers/setup_controller_test.exs View File

@@ -2,65 +2,131 @@ defmodule Koype.Web.SetupControllerTest do
2 2
   use Koype.Web.ConnCase
3 3
   use Plug.Test
4 4
   import Mock
5
+  import Koype.Factory
5 6
 
6
-  @actions %{
7
-    otp: %{"code" => 000_000},
8
-    name: %{"name" => Faker.Name.first_name()}
9
-  }
10 7
   describe ".view/2" do
11
-    test "renders page when setup is incomplete" do
8
+    @route setup_path(Koype.Web.Endpoint, :view)
9
+
10
+    test "redirects user to their homepage when setup is complete" do
12 11
       with_mock(
13 12
         Koype.Setup,
14
-        [:passthrough],
15 13
         complete?: fn -> true end
16 14
       ) do
17
-        conn = get(build_conn(), "/setup")
15
+        home_path = page_path(Koype.Web.Endpoint, :index)
16
+        conn = get(build_conn(), @route)
17
+
18
+        assert redirected_to(conn, 302) == home_path
19
+      end
20
+    end
18 21
 
19
-        assert html_response(conn, 200)
22
+    test "renders setup when setup is incomplete" do
23
+      with_mock(Koype.Setup, complete?: fn -> false end, state: fn -> :profile end) do
24
+        conn = get(build_conn(), @route)
25
+
26
+        assert html_response(conn, :ok)
27
+        assert called(Koype.Setup.state())
20 28
       end
21 29
     end
22 30
 
23
-    test "redirects user to their homepage" do
24
-      with_mock(Koype.Setup, [:passthrough], complete?: fn -> false end) do
25
-        conn = get(build_conn(), "/setup")
31
+    test "renders setup for auth when profile setup is complete" do
32
+      with_mock(Koype.Setup, complete?: fn -> false end, state: fn -> :auth end) do
33
+        conn = get(build_conn(), @route)
26 34
 
27
-        assert redirected_to(conn, 302) == "/"
35
+        assert html_response(conn, :ok) =~ "Authentication"
36
+        assert called(Koype.Setup.state())
28 37
       end
29 38
     end
30 39
   end
31 40
 
32
-  describe ".handle/3" do
33
-    test "successfully sets the option" do
34
-      for {state, params} <- @actions do
35
-        route = setup_path(Koype.Web.Endpoint, :handle, state)
36
-        conn = build_conn() |> add_csrf_token |> put(route, params)
37
-        assert resp = json_response(conn, 201)
41
+  describe ".handle/2" do
42
+    test "fails to upload photo for profile" do
43
+      route = setup_path(Koype.Web.Endpoint, :handle)
44
+
45
+      with_mock(Koype.Storage.Photo, store: fn _ -> {:error, :upload_error} end) do
46
+        conn =
47
+          build_conn()
48
+          |> add_csrf_token
49
+          |> put(route, %{
50
+            name: Faker.Name.name(),
51
+            nickname: Faker.Internet.user_name(),
52
+            note: Faker.Lorem.sentence(),
53
+            photo: nil,
54
+            state: "profile"
55
+          })
56
+
57
+        assert html_response(conn, 422)
38 58
       end
39 59
     end
40 60
 
41
-    test "fails when provides invalid value for profile"
42
-    test "fails if no secret is in the session"
43
-    test "fails if code is invalid"
44
-  end
61
+    test "successfully sets up profile" do
62
+      route = setup_path(Koype.Web.Endpoint, :handle)
63
+      photo = make_mock_upload(:image)
64
+      mock_file_name = Faker.File.file_name(:image)
45 65
 
46
-  describe ".check/2" do
47
-    @route setup_path(Koype.Web.Endpoint, :check)
66
+      with_mock(
67
+        Koype.Storage.Photo,
68
+        [:passthrough],
69
+        store: fn _val -> {:ok, mock_file_name} end
70
+      ) do
71
+        conn =
72
+          build_conn()
73
+          |> put_req_header("content-type", "multipart/form-data")
74
+          |> Plug.Test.init_test_session(%{})
75
+          |> add_csrf_token
76
+          |> put(route, %{
77
+            name: Faker.Name.name(),
78
+            nickname: Faker.Internet.user_name(),
79
+            note: Faker.Lorem.sentence(),
80
+            photo: photo,
81
+            prefer_nickname: true,
82
+            state: "profile"
83
+          })
84
+
85
+        assert html_response(conn, 200) =~ ""
86
+        assert get_flash(conn, :success) =~ "Let's continue with the setup"
87
+        assert Koype.Setup.state() == :auth
88
+      end
89
+    end
48 90
 
49
-    test "reports status about incomplete state" do
50
-      conn = build_conn() |> add_csrf_token |> get(@route)
91
+    test "fails if no secret is in the session" do
92
+      route = setup_path(Koype.Web.Endpoint, :handle)
93
+      code = Enum.random(100_000..999_999)
51 94
 
52
-      assert resp = json_response(conn, 200)
53
-      assert Enum.all?(~w(incomplete complete), fn key -> Map.has_key?(resp, key) end)
54
-      refute %{incomplete: []} = resp
95
+      Koype.Profile.set("nickname", Faker.Internet.user_name())
96
+      Koype.Profile.set("note", Faker.Lorem.sentence())
97
+
98
+      conn =
99
+        build_conn()
100
+        |> Plug.Test.init_test_session(%{})
101
+        |> add_csrf_token
102
+        |> put(route, %{
103
+          code: code,
104
+          state: "auth"
105
+        })
106
+
107
+      assert html_response(conn, 422)
108
+      assert Koype.Setup.state() == :auth
55 109
     end
56 110
 
57
-    test "reports nothing if setup is complete" do
58
-      conn = build_conn() |> add_csrf_token |> get(@route)
111
+    test "fails if code is invalid" do
112
+      route = setup_path(Koype.Web.Endpoint, :handle)
113
+      code = Enum.random(100_000..999_999)
114
+      otp_secret = Koype.Repo.OtpSecret.generate()
115
+
116
+      Koype.Profile.set("nickname", Faker.Internet.user_name())
117
+      Koype.Profile.set("note", Faker.Lorem.sentence())
118
+
119
+      conn =
120
+        build_conn()
121
+        |> Plug.Test.init_test_session(%{otp_secret: otp_secret})
122
+        |> add_csrf_token
123
+        |> put(route, %{
124
+          code: code,
125
+          state: "auth"
126
+        })
59 127
 
60
-      assert resp = json_response(conn, 200)
61
-      assert Enum.all?(~w(incomplete complete), fn key -> Map.has_key?(resp, key) end)
62
-      assert %{complete: "all"} = resp
63
-      assert %{incomplete: []} = resp
128
+      assert html_response(conn, 422)
129
+      assert Koype.Setup.state() == :auth
64 130
     end
65 131
   end
66 132
 end

+ 3
- 3
test/support/factory.ex View File

@@ -143,7 +143,7 @@ defmodule Koype.Factory do
143 143
     }
144 144
 
145 145
     {:ok, body} = File.read(Path.absname(type_to_file[type]))
146
-    File.write!(file, body)
146
+    :ok = File.write!(file, body)
147 147
 
148 148
     content_type =
149 149
       case type do
@@ -152,8 +152,8 @@ defmodule Koype.Factory do
152 152
       end
153 153
 
154 154
     %Plug.Upload{
155
-      filename: Faker.File.file_name(type),
156
-      path: file,
155
+      filename: Faker.File.file_name(type) <> Path.extname(type_to_file[type]),
156
+      path: file <> Path.extname(type_to_file[type]),
157 157
       content_type: content_type
158 158
     }
159 159
   end

+ 9
- 3
test/unit/repo/otp_secret_test.exs View File

@@ -8,7 +8,7 @@ defmodule Koype.Repo.OtpSecretTest do
8 8
   @now Calendar.DateTime.now!("UTC")
9 9
   @expired_time @now |> Calendar.DateTime.subtract!(@offset)
10 10
   @future_time @now |> Calendar.DateTime.add!(@offset)
11
-  @secret :crypto.strong_rand_bytes(16) |> Base.encode32()
11
+  @secret OtpSecret.generate()
12 12
 
13 13
   describe ".current/0" do
14 14
     test "returns nil if no secrets are currently configured" do
@@ -59,12 +59,18 @@ defmodule Koype.Repo.OtpSecretTest do
59 59
     test "confirms that code is valid for secret" do
60 60
       code = Totpex.generate_totp(@secret)
61 61
       assert insert(:otp_secret, secret: @secret)
62
-      assert OtpSecret.valid?(code)
63
-      refute OtpSecret.valid?(000_000)
62
+      assert :ok = OtpSecret.valid?(code)
63
+      assert {:error, :otp_code_mismatch} = OtpSecret.valid?(000_000)
64 64
     end
65 65
 
66 66
     test "fails if no secret is set" do
67 67
       assert {:error, :no_secret} = OtpSecret.valid?(000_000)
68 68
     end
69 69
   end
70
+
71
+  describe ".confirm_against_secret/2" do
72
+    test "passes for a secret and a valid code" do
73
+      assert OtpSecret.confirm_against_secret(@secret, Totpex.generate_totp(@secret))
74
+    end
75
+  end
70 76
 end

+ 0
- 0
tmp/apk/lib/.gitkeep View File


+ 1
- 1
web/channels/user_socket.ex View File

@@ -6,7 +6,7 @@ defmodule Koype.Web.UserSocket do
6 6
 
7 7
   ## Transports
8 8
   transport(:websocket, Phoenix.Transports.WebSocket)
9
-  # transport :longpoll, Phoenix.Transports.LongPoll
9
+  transport :longpoll, Phoenix.Transports.LongPoll
10 10
 
11 11
   # Socket params are passed from the client and can
12 12
   # be used to verify and authenticate a user. After

+ 9
- 14
web/controllers/page_controller.ex View File

@@ -25,20 +25,15 @@ defmodule Koype.Web.PageController do
25 25
   end
26 26
 
27 27
   def index(conn, _params) do
28
-    case Koype.ready_for_use?() do
29
-      {:error, :db_error} ->
30
-        conn
31
-        |> Plug.Conn.put_resp_header("content-type", "application/json")
32
-        |> Explode.bad_request()
33
-
34
-      {:error, _error} ->
35
-        conn
36
-        |> put_flash(:info, "Hey there! Let's finish setting up your site.")
37
-        |> redirect(to: "/setup")
38
-
39
-      :ok ->
40
-        conn
41
-        |> render("index.html", entries: loaded_entries())
28
+    Logger.info("Is the setup process complete? #{Koype.Setup.complete?()}")
29
+
30
+    if !Koype.Setup.complete?() do
31
+      conn
32
+      |> put_flash(:info, "Hey there! Let's finish setting up your site.")
33
+      |> redirect(to: "/setup")
34
+    else
35
+      conn
36
+      |> render("index.html", entries: loaded_entries())
42 37
     end
43 38
   end
44 39
 

+ 7
- 0
web/controllers/rel_me_controller.ex View File

@@ -0,0 +1,7 @@
1
+defmodule Koype.Web.RelMeController do
2
+  use Koype.Web, :controller
3
+
4
+  def view(conn, params) do
5
+    render(conn, "view.html", params)
6
+  end
7
+end

+ 97
- 116
web/controllers/setup_controller.ex View File

@@ -1,152 +1,133 @@
1 1
 defmodule Koype.Web.SetupController do
2 2
   use Koype.Web, :controller
3
-  alias Koype.Repo.OtpSecret
4
-  alias Koype.Profile
5
-  alias Koype.Repo
3
+  alias Koype.{Setup, Profile}
4
+  alias Koype.Repo.{OtpSecret, Setting}
5
+  alias Koype.Storage.Photo
6 6
 
7
-  @error_to_uri %{
8
-    otp: "/setup/otp",
9
-    profile: "/setup/profile"
10
-  }
7
+  defp do_handle_photo_upload(photo)
8
+  defp do_handle_photo_upload(nil), do: {:error, :no_photo_provided}
11 9
 
12
-  def check(conn, _params), do: json(conn, %{})
10
+  defp do_handle_photo_upload(%Plug.Upload{} = photo) do
11
+    case Photo.store({photo, :floating}) do
12
+      {:ok, url} ->
13
+        Logger.info("Uploaded #{url} as the user's avatar.")
14
+        :ok = Profile.set("photo", url)
13 15
 
14
-  def index(conn, _params) do
15
-    case Koype.ready_for_use?() do
16 16
       {:error, error} ->
17
-        conn
18
-        |> put_flash(:info, "Your site's not yet fully set up.")
19
-        |> render("index.html", %{
20
-          next: %{
21
-            state: error,
22
-            uri: @error_to_uri[error]
23
-          }
24
-        })
17
+        {:error, error}
18
+    end
19
+  end
20
+
21
+  defp do_update_profile(params) do
22
+    Enum.all?(params, fn {key, value} -> Profile.set(key, value) end)
23
+  end
24
+
25
+  defp do_apply_preferred_display_name(params) do
26
+    result =
27
+      if Map.get(params, "prefer_nickname", false) do
28
+        Setting.set("displayed_name", "nickname")
29
+      else
30
+        Setting.set("displayed_name", "name")
31
+      end
32
+
33
+    case result do
34
+      {:ok, _setting} -> :ok
35
+      {:error, _cs} -> :error
36
+    end
37
+  end
25 38
 
39
+  defp do_render_next(conn) do
40
+    case Koype.Setup.state() do
26 41
       :ok ->
27
-        redirect(conn, to: "/")
42
+        conn
43
+        |> put_flash(:success, "And just like that; you're done! Welcome.")
44
+        |> redirect(to: page_path(conn, :index))
45
+
46
+      next_state ->
47
+        render(conn, "view.html", state: next_state)
28 48
     end
29 49
   end
30 50
 
31
-  def view(conn, params)
51
+  defp do_create_otp_secret(code, secret)
52
+  defp do_create_otp_secret(_code, nil), do: {:error, :no_otp_secret}
32 53
 
33
-  def view(conn, %{"page" => page} = _params) do
34
-    case Koype.ready_for_use?() do
35
-      :ok ->
36
-        redirect(conn, to: "/")
37
-
38
-      _ ->
39
-        case page do
40
-          "profile" ->
41
-            render(conn, "setup-profile.html")
42
-
43
-          "otp" ->
44
-            secret =
45
-              case get_session(conn, :otp_secret) do
46
-                nil -> OtpSecret.generate()
47
-                existing_secret -> existing_secret
48
-              end
49
-
50
-            conn
51
-            |> put_session(:otp_secret, secret)
52
-            |> assign(:otp_secret, secret)
53
-            |> render("setup-otp.html")
54
-
55
-          _ ->
56
-            redirect(conn, to: "/setup")
57
-        end
54
+  defp do_create_otp_secret(code, secret) do
55
+    with(
56
+      :ok <- OtpSecret.confirm_against_secret(secret, code),
57
+      {:ok, _otp} <- OtpSecret.create(secret)
58
+    ) do
59
+      :ok
60
+    else
61
+      {:error, :otp_code_mismatch} -> {:error, :otp_code_mismatch}
62
+      {:error, %Ecto.Changeset{} = cs} -> {:error, cs.errors}
58 63
     end
59 64
   end
60 65
 
61
-  def view(conn, _params), do: render(conn, "index.html")
66
+  defp do_attach_otp_secret(conn) do
67
+    secret =
68
+      case get_session(conn, :otp_secret) do
69
+        nil -> OtpSecret.generate()
70
+        existing_secret -> existing_secret
71
+      end
62 72
 
63
-  def complete(conn, params)
73
+    conn
74
+    |> assign(:otp_secret, secret)
75
+    |> put_session(:otp_secret, secret)
76
+  end
64 77
 
65
-  def complete(conn, %{"page" => "otp", "code" => code} = _params) do
66
-    secret = get_session(conn, :otp_secret)
78
+  defp do_humanize_auth_error(error)
79
+  defp do_humanize_auth_error(:otp_code_mismatch), do: "The code you provided didn't match the one Koype expected."
67 80
 
68
-    assert_secret = fn ->
69
-      if secret != nil do
70
-        :ok
71
-      else
72
-        {:error, :no_otp_secret}
73
-      end
81
+  def view(conn, params) do
82
+    if Setup.complete?() do
83
+      redirect(conn, to: page_path(conn, :index))
84
+    else
85
+      state = Map.get(params, "state", Koype.Setup.state())
86
+
87
+      conn
88
+      |> do_attach_otp_secret
89
+      |> render("view.html", state: state)
74 90
     end
91
+  end
75 92
 
93
+  def handle(conn, params)
94
+  def handle(conn, %{"state" => ""} = params),
95
+    do: handle(conn, Map.put(params, "state", Koype.Setup.state() |> Atom.to_string()))
96
+  def handle(conn, %{"state" => "profile"} = params) do
76 97
     with(
77
-      :ok <- assert_secret.(),
78
-      :ok <- OtpSecret.confirm_against_secret(secret, code),
79
-      {:ok, _otp} <- OtpSecret.create(secret)
98
+      :ok <- do_handle_photo_upload(params["photo"]),
99
+      true <- do_update_profile(Map.take(params, ~w(name nickname note email))),
100
+      :ok <- do_apply_preferred_display_name(Map.take(params, ~w(nickname name prefer_nickname)))
80 101
     ) do
81 102
       conn
82
-      |> put_session(:otp_secret, nil)
103
+      |> put_status(:ok)
83 104
       |> put_flash(
84 105
         :success,
85
-        "Awesome! You've secured your website. Nice work."
106
+        "Awesome work, #{Koype.Profile.displayed_name()}! Let's continue with the setup."
86 107
       )
87
-      |> redirect(to: "/setup")
108
+      |> do_render_next
88 109
     else
89
-      {:error, :no_otp_secret} ->
90
-        conn
91
-        |> put_status(:bad_request)
92
-        |> put_flash(:warn, "OTP secret not found; applying a new one.")
93
-        |> put_session(:otp_secret, OtpSecret.generate())
94
-        |> render("setup-otp.html")
110
+      {:error, error} ->
111
+        conn |> put_flash(:error, error) |> put_status(422) |> render("view.html", state: :profile)
95 112
 
96
-      {:error, :otp_code_mismatch} ->
97
-        conn
98
-        |> put_status(:unprocessable_entity)
99
-        |> assign(:otp_secret, secret)
100
-        |> put_flash(
101
-          :error,
102
-          "The code you provided wasn't correct. Please try again."
103
-        )
104
-        |> render("setup-otp.html")
105
-
106
-      {:error, %Ecto.Changeset{} = cs} ->
113
+      false ->
114
+        # TODO: Add more specifics about the failure here.
107 115
         conn
108
-        |> put_status(:internal_server_error)
109
-        |> put_flash(:warn, "Something weird happened.")
110
-        |> render("setup-otp.html", %{error: cs.errors})
116
+        |> put_flash(:error, "There was a problem updating your profile.")
117
+        |> put_status(422)
118
+        |> render("view.html", state: :profile)
111 119
     end
112 120
   end
113
-
114
-  def complete(
115
-        conn,
116
-        %{"page" => "profile", "nickname" => _nickname, "email" => _email} = params
117
-      ) do
118
-    insert_properties = fn ->
119
-      for property <- ~w(name nickname email note) do
120
-        case Profile.set(property, params[property]) do
121
-          {:error, error} ->
122
-            Repo.rollback({:setting_invalid, property, error})
123
-
124
-          _ ->
125
-            :ok
126
-        end
127
-      end
128
-    end
129
-
130
-    case Repo.transaction(insert_properties) do
131
-      {:ok, _} ->
132
-        conn
133
-        |> put_flash(
134
-          :success,
135
-          "You've got your profile all set up. Congrats #{Koype.Profile.nickname()}!"
136
-        )
137
-        |> redirect(to: "/setup")
138
-
121
+  def handle(conn, %{"state" => "auth"} = params) do
122
+    with(:ok <- do_create_otp_secret(params["code"], get_session(conn, :otp_secret))) do
123
+      do_render_next(conn)
124
+    else
139 125
       {:error, error} ->
140 126
         conn
141
-        |> put_status(:unprocessable_entity)
142
-        |> put_flash(:error, "Failed to set a value for your profile.")
143
-        |> render("setup-profile.html", %{error: error})
127
+        |> do_attach_otp_secret
128
+        |> put_flash(:error, do_humanize_auth_error(error))
129
+        |> put_status(422)
130
+        |> render("view.html", state: :auth)
144 131
     end
145 132
   end
146
-
147
-  def complete(conn, _params) do
148
-    conn
149
-    |> put_flash(:error, "Invalid operation.")
150
-    |> redirect(to: "/setup")
151
-  end
152 133
 end

+ 3
- 7
web/router.ex View File

@@ -8,7 +8,6 @@ defmodule Koype.Web.Router do
8 8
     plug(:put_secure_browser_headers)
9 9
     plug(:protect_from_forgery)
10 10
     plug(Koype.Web.Plug.Guardian.Owner)
11
-    plug(Plug.Ribbon, [:dev, :test])
12 11
   end
13 12
 
14 13
   pipeline :client do
@@ -34,17 +33,12 @@ defmodule Koype.Web.Router do
34 33
   scope "/", Koype.Web do
35 34
     pipe_through([:browser, :owner, :prohibit_owner_auth])
36 35
     get("/setup", SetupController, :view)
36
+    put("/setup", SetupController, :handle)
37 37
 
38 38
     get("/auth", AuthController, :new)
39 39
     post("/auth", AuthController, :submit)
40 40
   end
41 41
 
42
-  scope "/api", Koype.Web do
43
-    pipe_through([:client, :owner, :prohibit_owner_auth])
44
-    get("/setup/check", SetupController, :check)
45
-    put("/setup/:state", SetupController, :handle)
46
-  end
47
-
48 42
   scope "/", Koype.Web do
49 43
     pipe_through([:browser, :owner])
50 44
 
@@ -68,6 +62,8 @@ defmodule Koype.Web.Router do
68 62
 
69 63
     get("/~/settings/", SettingsController, :view)
70 64
     put("/~/settings/", SettingsController, :apply)
65
+
66
+    get("/~/rel-me", RelMeController, :view)
71 67
   end
72 68
 
73 69
   scope "/indie", Koype.Web, as: :indie do

+ 12
- 13
web/static/css/koype.scss View File

@@ -1,4 +1,8 @@
1
-@import "tachyons-custom/src/tachyons.css";
1
+:root {
2
+  --font-family-sans: "Source Sans Pro";
3
+  --font-family-serif: "Source Serif Pro";
4
+  --font-family-mono: "Source Code Pro";
5
+}
2 6
 
3 7
 .e-content {
4 8
   p {
@@ -16,8 +20,8 @@
16 20
   }
17 21
 
18 22
   pre {
19
-    background-color: black;
20
-    color: green;
23
+    background-color: var(--black);
24
+    color: var(--green);
21 25
     padding: 1.5rem 0rem;
22 26
     line-height: 1;
23 27
   }
@@ -26,21 +30,16 @@
26 30
 html.wf-active {
27 31
   h1, h2, h3, h4, h5, h6, p
28 32
   blockquote, .sans-serif {
29
-    font-family: "Source Sans Pro", sans-serif;
33
+    font-family: var(--font-family-sans), sans-serif;
30 34
   }
31 35
 
32 36
   input, button, form,
33 37
   article, aside,
34
-  main, footer, header,
35
-  .serif {
36
-    font-family: "Source Serif Pro", serif;
38
+  main, footer, header, .serif {
39
+    font-family: var(--font-family-serif), serif;
37 40
   }
38 41
 
39
-  pre, code, kbd,
40
-  .mono {
41
-    font-family: "Source Mono Pro", monospace;
42
+  pre, code, kbd, .code {
43
+    font-family: var(--font-family-mono), monospace;
42 44
   }
43 45
 }
44
-
45
-// @import "leaflet/dist/leaflet.css";
46
-@import "ladda/css/ladda";

+ 6
- 0
web/static/css/vendor.scss View File

@@ -0,0 +1,6 @@
1
+@import "tachyons-sass/tachyons.scss";
2
+@import "ladda/css/ladda";
3
+@import "pretty-checkbox/src/pretty-checkbox";
4
+@import "quill/dist/quill.core.css";
5
+@import "quill/dist/quill.bubble.css";
6
+// @import "leaflet/dist/leaflet.css";

+ 0
- 3
web/static/js/koype.test.ts View File

@@ -1,3 +0,0 @@
1
-test('adds 1 + 2 to equal 3', () => {
2
-  expect(1 + 2).toBe(3);
3
-});

web/static/js/koype.ts → web/static/js/page.ts View File

@@ -4,7 +4,7 @@ import swal from 'sweetalert';
4 4
 import timeago from 'timeago.js';
5 5
 import WebFont from 'webfontloader';
6 6
 
7
-import { Socket } from 'phoenix';
7
+// import { Socket } from 'phoenix';
8 8
 import 'phoenix_html';
9 9
 
10 10
 function buildHiddenInput(name, value) {
@@ -59,3 +59,6 @@ window.addEventListener('load', () => {
59 59
   feather.replace();
60 60
   Ladda.bind('button[type=submit]');
61 61
 }, false);
62
+
63
+window.Ladda = Ladda;
64
+window.swal = swal;

+ 0
- 47
web/static/js/request.test.ts View File

@@ -1,47 +0,0 @@
1
-import axios from "axios";
2
-import MockAdapter from "axios-mock-adapter";
3
-
4
-import { extractCSRFToken, request } from "./request";
5
-
6
-function addBaseHref(): void {
7
-  const baseElem = document.createElement("base");
8
-  const headElem = document.querySelector("head");
9
-
10
-  headElem.appendChild(baseElem);
11
-  baseElem.setAttribute("href", "https://example.koype");
12
-}
13
-
14
-function addMockToken(token: string = "fakeToken"): void {
15
-  const metaCsrfToken = document.createElement("meta");
16
-  const headElem = document.querySelector("head");
17
-
18
-  headElem.appendChild(metaCsrfToken);
19
-  metaCsrfToken.setAttribute("name", "_csrf_token");
20
-  metaCsrfToken.setAttribute("value", token);
21
-}
22
-
23
-test("extracts current CSRF token from the page", () => {
24
-  addMockToken();
25
-
26
-  const obtainedToken = extractCSRFToken();
27
-  expect(obtainedToken).toBe("fakeToken");
28
-});
29
-
30
-test("sends request", async () => {
31
-  addMockToken();
32
-  addBaseHref();
33
-
34
-  const route = "__test__";
35
-  const mockedResponse = "testing";
36
-  const mock = new MockAdapter(axios);
37
-
38
-  mock.onGet(route).reply(200, mockedResponse, {
39
-    "x-csrf-token": "crazy"
40
-  });
41
-
42
-  const response = await request("GET", route);
43
-  const data = response.data;
44
-
45
-  expect(data).toBe(mockedResponse);
46
-  expect(extractCSRFToken()).toBe("crazy");
47
-});

+ 1
- 1
web/static/js/request.ts View File

@@ -17,7 +17,7 @@ function http() {
17 17
 
18 18
   instance.interceptors.response.use(
19 19
     response => {
20
-      const token = response.headers["x-csrf-token"];
20
+      const token: string = response.headers["x-csrf-token"];
21 21
       setTokenInMeta(token);
22 22
       return response;
23 23
     },

+ 151
- 0
web/static/js/setup.ts View File

@@ -0,0 +1,151 @@
1
+import Quill from "quill";
2
+import { slugify, snakeCase } from "voca";
3
+const qrcode = require('qrcode-generator');
4
+
5
+const placeHolderText = `Koype is a self-hostable IndieWeb engine for everyone.`;
6
+
7
+function usernameify(str): string {
8
+  return snakeCase(str);
9
+}
10
+
11
+function preferNickname(): boolean {
12
+  const preferredElem: HTMLInputElement = document.querySelector(
13
+    "input[name=prefer_nickname]"
14
+  );
15
+  return preferredElem.checked;
16
+}
17
+
18
+function configureNoteEditor(): void {
19
+  const editorElem = document.querySelector("input[name=note]");
20
+  const notePreview = document.querySelector("#note");
21
+  const quillOptions: object = {
22
+    modules: {
23
+      toolbar: {
24
+        container: [["bold", "italic", "underline", "strike"]]
25
+      }
26
+    },
27
+    placeholder: "Tell me about yourself.",
28
+    readOnly: false,
29
+    theme: "bubble"
30
+  };
31
+  const editor: Quill = new Quill("#noteEditor", quillOptions);
32
+
33
+  editor.on("text-change", (delta, oldContents, source) => {
34
+    const editorText: string = editor.getText();
35
+
36
+    editorElem.setAttribute("value", editorText);
37
+
38
+    if (editorText.length > 0) {
39
+      notePreview.innerHTML = editorText;
40
+    } else {
41
+      notePreview.textContent = placeHolderText;
42
+    }
43
+  });
44
+}
45
+
46
+function configureNameFields(): void {
47
+  const nameElem: HTMLInputElement = document.querySelector("input[name=name]");
48
+  const nicknameElem: HTMLInputElement = document.querySelector(
49
+    "input[name=nickname]"
50
+  );
51
+  const preferredElem: HTMLHeadingElement = document.querySelector(
52
+    "#preferredName"
53
+  );
54
+
55
+  nameElem.addEventListener("keyup", e => {
56
+    if (!preferNickname()) {
57
+      preferredElem.textContent = nameElem.value;
58
+      nicknameElem.value = usernameify(nameElem.value);
59
+    }
60
+
61
+    if (nicknameElem.value === "") {
62
+      nicknameElem.value = usernameify(nameElem.value);
63
+    }
64
+
65
+    updateDid();
66
+  });
67
+
68
+  nicknameElem.addEventListener("keyup", e => {
69
+    nicknameElem.value = usernameify(nicknameElem.value);
70
+
71
+    if (preferNickname()) {
72
+      preferredElem.textContent = nicknameElem.value;
73
+    }
74
+
75
+    updateDid();
76
+  });
77
+}
78
+
79
+function updateDid(): void {
80
+  const didElem: HTMLSpanElement = document.querySelector("#did");
81
+  const nicknameElem: HTMLInputElement = document.querySelector(
82
+    "input[name=nickname]"
83
+  );
84
+
85
+  didElem.textContent = nicknameElem.value;
86
+}
87
+
88
+function configureNamePreferenceToggle(): void {
89
+  const preferredChoiceElem: HTMLInputElement = document.querySelector(
90
+    "input[name=prefer_nickname]"
91
+  );
92
+
93
+  preferredChoiceElem.addEventListener("change", () => {
94
+    if (!preferredChoiceElem.checked) {
95
+      const nameElem: HTMLInputElement = document.querySelector(
96
+        "input[name=name]"
97
+      );
98
+      const nicknameElem: HTMLInputElement = document.querySelector(
99
+        "input[name=nickname]"
100
+      );
101
+
102
+      nicknameElem.value = usernameify(nameElem.value);
103
+    }
104
+
105
+    updateDid();
106
+  });
107
+}
108
+
109
+function renderTotpQrCode(
110
+  imagePath: string = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>'
111
+) {
112
+  const qrContainer: HTMLElement = document.querySelector("#qrCode");
113
+  const qrTotpUriElem: HTMLInputElement = document.querySelector("#totpUri");
114
+  const value: string = qrTotpUriElem.value;
115
+  const qr = qrcode(0, 'H');
116
+  qr.addData(value);
117
+  console.log(qr.make());
118
+  const dataUri = "url(" + qr.createDataURL(5, 4) + ")";
119
+  qrContainer.style.setProperty('background-image', dataUri);
120
+  qrContainer.style.setProperty('background-size', 'cover');
121
+  qrContainer.style.setProperty('background-repeat', 'none');
122
+}
123
+
124
+function configureAvatarRendering(): void {
125
+  const avatarInputElem: HTMLInputElement = document.querySelector(
126
+    "input[name=photo]"
127
+  );
128
+  const avatarRenderer: HTMLImageElement = document.querySelector(
129
+    "img#profileImage"
130
+  );
131
+
132
+  avatarInputElem.addEventListener("change", () => {
133
+    const fileReader = new FileReader();
134
+    fileReader.readAsDataURL(avatarInputElem.files[0]);
135
+    fileReader.addEventListener("load", () => {
136
+      const result = fileReader.result as string;
137
+      avatarRenderer.src = result;
138
+    });
139
+  });
140
+}
141
+
142
+window.addEventListener("load", function startSetupPage() {
143
+  if (document.querySelector("#regionId")) {
144
+    configureNameFields();
145
+    configureNamePreferenceToggle();
146
+    configureNoteEditor();
147
+    configureAvatarRendering();
148
+  } else if (document.querySelector("#regionAuth")) {
149
+    renderTotpQrCode();
150
+  }
151
+});

+ 1
- 1
web/templates/entry/_category.html.eex View File

@@ -11,6 +11,6 @@
11 11
     <% end %>
12 12
   </ul>
13 13
 <% else %>
14
-  <p class="lh-copy pa0 ma0 f6 gray">An uncategorized post.</p>
14
+  <p class="dn-l lh-copy pa0 ma0 f6 gray">An uncategorized post.</p>
15 15
 <% end %>
16 16
 </div>

+ 3
- 3
web/templates/entry/_sparkline.html.eex View File

@@ -1,12 +1,12 @@
1 1
 <div class="db bg-near-black mt3 mt0-m ma3-l self-start w-100 w-auto-ns order-2 order-3-l self-stretch items-center sans-serif">
2
-  <ul class="flex flex-column w-100 h-100 justify-center w-auto-l relative-l left--1-l top--1-l self-end-l self-center ma0 pa2 f6 gray list lh-copy bg-lightest-blue near-black ba b--mid-gray">
2
+  <ul class="flex flex-column w-100 h-100 justify-center w-auto-l relative-l left--1-l top--1-l self-end-l self-center ma0 pa2 f6 dark-gray list lh-copy bg-lightest-blue near-black ba b--mid-gray">
3 3
     <li class="lh-copy">
4 4
     <%= if !is_nil(@entry[:published]) do %>
5 5
       published
6 6
       <time class="fw5 dt-published" title="<%= @entry[:published] %>" datetime="<%= @entry[:published] %>">
7 7
         <%= get_time_ago_of_rfc3339(@entry[:published]) %>
8 8
       </time>
9
-      <%= else %>
9
+      <% else %>
10 10
       created
11 11
       <time class="fw5 dt-published" title="<%= @model.inserted_at %>" datetime="<%= @model.inserted_at %>">
12 12
         <%= time_ago_in_words(@model.inserted_at) %>
@@ -35,7 +35,7 @@
35 35
     <li class="lh-copy mt1">
36 36
       <span class="v-mid">by</span>
37 37
       <a class="ml1 u-author h-card v-mid link fw5 navy" rel="me" href="/">
38
-        <img class="v-mid u-photo br-100 h1 w1 b--near-black" src="https://api.adorable.io/avatars/128/<%= Koype.Profile.email %>.png" />
38
+        <img class="v-mid u-photo br-100 h1 w1 b--near-black" src="<%= Koype.Profile.photo %>" />
39 39
         <span class="v-mid"><%= Koype.Profile.name %></span>
40 40
       </a>
41 41
     </li>

+ 1
- 1
web/templates/entry/_webmentions.html.eex View File

@@ -4,7 +4,7 @@
4 4
     <span class="v-mid">Responses</span>
5 5
   </h3>
6 6
   <p class="f5 lh-copy measure">
7
-    Here's what people had to say about this post. Want to join in?
7
+    Here's what people had to say about this <%= determine_type(@model) %>. Want to join in?
8 8
     <a target="_new" href="https://indieweb.org/reply#How_To" class="link underline navy fw7">Learn how</a>.
9 9
   </p>
10 10
 </div>

+ 3
- 2
web/templates/layout/app.html.eex View File

@@ -3,7 +3,7 @@
3 3
   <head>
4 4
     <meta charset="utf-8">
5 5
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
-    <meta name="viewport" content="initial-scale=1.0,user-scalable=yes,maximum-scale=1,width=device-width,height=device-height" />
6
+    <meta name="viewport" content="initial-scale=1.0,user-scalable=yes,width=device-width,height=device-height" />
7 7
     <%= for name <- Koype.Web.tags(:meta) ++ @view_module.tags(:meta, @view_template, assigns) do %>
8 8
     <meta name="<%= name %>" content="<%= @view_module.tag({:meta, name}, @view_template, assigns) %>" />
9 9
     <% end %>
@@ -14,6 +14,7 @@
14 14
     <% end %>
15 15
     <link rel="icon" type="image/png" href="<%= static_path(@conn, "/images/favicon.png") %>" sizes="100x100" />
16 16
     <link rel="icon" type="image/x-icon" href="<%= static_path(@conn, "/images/favicon.png") %>" />
17
+    <link rel="stylesheet" href="<%= static_path(@conn, "/assets/css/vendor.css") %>" />
17 18
     <link rel="stylesheet" href="<%= static_path(@conn, "/assets/css/koype.css") %>" />
18 19
   </head>
19 20
   <body class="w-100 min-h-100 min-vh-100 flex flex-column near-black bg-near-white sans-serif">
@@ -24,6 +25,6 @@
24 25
     </main>
25 26
     <%= render Koype.Web.PageView, "_now-sparkline.html", assigns %>
26 27
     <%= render Koype.Web.PageView, "_footer.html", assigns %>
27
-    <script async defer type="text/javascript" src="<%= static_path(@conn, "/assets/js/koype.js") %>"></script>
28
+    <script async defer type="text/javascript" src="<%= static_path(@conn, "/assets/js/page.js") %>"></script>
28 29
   </body>
29 30
 </html>

+ 10
- 8
web/templates/page/_footer.html.eex View File

@@ -3,14 +3,16 @@
3 3
     <%= if Koype.Profile.complete? do %>
4 4
       <div class="h-card pa2 items-center flex flex-column flex-row-l flex-auto flex-grow items-start">
5 5
         <a href="<%= Koype.host %>" rel="author me authn" class="u-url db pa2 link lightest-blue">
6
-          <img alt="<%= Koype.Profile.displayed_name %>"
7
-               class="center ba bw2 b--lightest-blue bg--near-white br-100 w-auto h3-l u-photo db-l"
8
-               src="https://api.adorable.io/avatars/128/<%= Koype.Profile.email %>.png" />
6
+          <%= if Koype.Profile.photo do %>
7
+            <img alt="<%= Koype.Profile.displayed_name %>"
8
+                class="center ba bw1 b--lightest-blue bg--near-white br-100 w-auto h4 u-photo db"
9
+                src="<%= Koype.Profile.photo %>" />
10
+          <% end %>
9 11
           <span class="p-name tc db fw7 mv2 f6"><%= Koype.Profile.name %></span>
10 12
         </a>
11
-        <small class="db self-end-l flex-grow flex-auto dib-ns ml2-l v-mid lh-copy tj p-note measure-narrow"><%= Koype.Profile.note %></small>
13
+        <small class="db f5 self-center-l flex-grow flex-auto dib-ns ml2-l v-mid lh-copy tj tc-l p-note measure-narrow"><%= Koype.Profile.note %></small>
12 14
       </div>
13
-      <ul class="list pa2 f6 center flex flex-auto flex-row w-100 w-third-l flex-wrap justify-center mh2-l br-l bl-l bw1-l b--lightest-blue">
15
+      <ul class="list pa2 mw5 f6 center flex flex-auto flex-row w-100 w-third-l flex-wrap justify-center mh2-l br-l bl-l bw1-l b--lightest-blue">
14 16
         <li class="lh-copy ttu fw7 w-100 tc">
15 17
           <a class="link lightest-blue" href="<% entry_path(@conn, :stream) %>">
16 18
             <i class="near-white h1 w1 pa1 v-mid" data-feather="activity"></i>
@@ -19,8 +21,8 @@
19 21
         </li>
20 22
         <%= for %{type: type, names: name} <- IndieWeb.Post.known_types do %>
21 23
           <li class="lh-copy mv1 w-auto tc">
22
-            <a title="<%= name %>" class="flex justify-center items-center w2 h2 grow animate bg-animate hover-bg-near-white hover-navy br-100 dib link lightest-blue" href="<%= entry_path(@conn, :view_by_type, type) %>">
23
-              <i class="h1 w1 ma1 v-mid" data-feather="<%= Koype.Web.EntryView.icon_for_type(type) %>"></i>
24
+            <a title="<%= name %>" class="flex justify-center items-center w2 h1 grow animate br-100 dib link lightest-blue" href="<%= entry_path(@conn, :view_by_type, type) %>">
25
+              <i role="none" class="h1 w1 mb1 mr1 v-mid" data-feather="<%= Koype.Web.EntryView.icon_for_type(type) %>"></i>
24 26
             </a>
25 27
           </li>
26 28
         <% end %>
@@ -47,7 +49,7 @@
47 49
           </li>
48 50
         <% end %>
49 51
       </ul>
50
-    <%= else %>
52
+    <% else %>
51 53
       <p class="lh-copy f7 tc v-mid">
52 54
         Koype
53 55
         <code>v<%= Koype.version %></code>

+ 20
- 22
web/templates/page/_header.html.eex View File

@@ -1,24 +1,22 @@
1
-<header role="main" class="w-100 order-1 items-center">
2
-  <div class="flex flex-column flex-row-l justify-between items-center pa2 center mw7 w-100">
3
-    <a class="pa1 mv2 w-100 w-auto-l grow dim link gray tc" href="<%= page_path(@conn, :index) %>">
4
-      <span class="sans-serif fw1 f4 f3-l tracked-tight v-mid">
5
-        <%= Koype.Profile.displayed_name %>
6
-      </span>
7
-    </a>
8
-    <div class="pa1">
9
-      <%= if is_signed_in?(@conn, :owner) do %>
10
-        <a class="link ma1 dib dim" href="<%= settings_path(@conn, :view) %>">
11
-          <i class="bg-blue white-80 h1 w1 pa2 br-100 v-mid" data-feather="settings"></i>
12
-        </a>
13
-        <a class="link ma1 dib dim" href="<%= auth_path(@conn, :logout, redirect_to: Base.url_encode64(Phoenix.Controller.current_path(@conn))) %>">
14
-          <i class="bg-red white-80 h1 w1 pa2 br-100 v-mid" data-feather="log-out"></i>
15
-        </a>
16
-      <% end %>
17
-      <%= if is_signed_out?(@conn, :owner) do %>
18
-        <a class="link ma1 dib dim" href="<%= auth_path(@conn, :new, redirect_to: Base.url_encode64(Phoenix.Controller.current_path(@conn))) %>">
19
-          <i class="bg-dark-green white-80 h1 w1 pa2 br-100 v-mid" data-feather="log-in"></i>
20
-        </a>
21
-      <% end %>
22
-    </div>
1
+<header role="main" class="mw7 w-100 order-1 flex flex-row flex-wrap pa2 mt2-l justify-center items-center self-center">
2
+  <a class="grow dim pv2 pv0-l link dark-gray w-100 w-auto-ns flex-auto order-1 tc tl-l" href="<%= page_path(@conn, :index) %>">
3
+    <span class="sans-serif fw1 f4 f3-l tracked-tight v-mid">
4
+      <%= Koype.Profile.displayed_name %>
5
+    </span>
6
+  </a>
7
+  <div class="w-auto flex-auto order-2 tc tr-l">
8
+    <%= if is_signed_in?(@conn, :owner) do %>
9
+      <a role="presentation" class="link ma1 dim" href="<%= settings_path(@conn, :view) %>">
10
+        <i class="bg-blue white-80 h1 w1 pa2 br-100 v-mid" data-feather="settings">Settings</i>
11
+      </a>
12
+      <a role="presentation" class="link ma1 dim" href="<%= auth_path(@conn, :logout, redirect_to: Base.url_encode64(Phoenix.Controller.current_path(@conn))) %>">
13
+        <i class="bg-red white-80 h1 w1 pa2 br-100 v-mid" data-feather="log-out">Log Out</i>
14
+      </a>
15
+    <% end %>
16
+    <%= if is_signed_out?(@conn, :owner) do %>
17
+      <a role="presentation" class="link ma1 dim" href="<%= auth_path(@conn, :new, redirect_to: Base.url_encode64(Phoenix.Controller.current_path(@conn))) %>">
18
+        <i class="bg-dark-green white-80 h1 w1 pa2 br-100 v-mid" data-feather="log-in">Log In</i>
19
+      </a>
20
+    <% end %>
23 21
   </div>
24 22
 </header>

+ 11
- 0
web/templates/rel_me/view.html.eex View File

@@ -0,0 +1,11 @@
1
+<section class="w-100 mw7 center pa2">
2
+  <h1 class="f1 lh-title">Verified Links</h1>
3
+  <p class="lh-copy gray f4">
4
+    <strong>Verified Links</strong> provide your site with a form of <em>distributed identity</em>.
5
+    Learn more about it on the IndieWeb wiki.
6
+  </p>
7
+  <ul class="list">
8
+    <%= for link <- Koype.Profile.relme do %>
9
+    <% end %>
10
+  </ul>
11
+</section>

+ 0
- 7
web/templates/setup/index.html.eex View File

@@ -1,7 +0,0 @@
1
-<section class="mw7 center ma2">
2
-  <h1 class="f1 tc lh-title">Setting Up</h1>
3
-  <p class="lh-copy f4 measure-wide center">
4
-    Hey there. We're almost done getting your personal place on the Web up
5
-    and running. Below are some things you'd want to complete before continuing.
6
-  </p>
7
-</section>

+ 39
- 0
web/templates/setup/setup-auth.html.eex View File

@@ -0,0 +1,39 @@
1
+<input id="totpUri" type="hidden" value="<%= totp_uri(@conn) %>" autocomplete="off">
2
+<section id="regionAuth" class="w-100 mw8 mv3 center pa2 pb3 bt b--moon-gray flex flex-row flex-wrap justify-center items-center">
3
+  <h2 class="lh-title tracked tc f2 order-1 flex-auto flex-grow w-100">Authentication</h2>
4
+  <div class="flex-grow order-2 w-100 flex-auto ph1">
5
+    <p class="f5 lh-copy measure center tj">
6
+      Your site, your rules! But in order to play by those rules; Koype needs to confirm that you
7
+      are who you say you are. Koype handles authorization via TOTP. Scanning the QR code with a
8
+      <a href="https://git.jacky.wtf/indieweb/koype/wiki/AvailableAuthenticationApps" class="link navy fw5">compatible app</a> will give you a code that you can then
9
+      enter below.
10
+    </p>
11
+  </div>
12
+  <noscript>
13
+    <p class="flex items-center justify-center pa4 bg-lightest-blue navy">
14
+      This portion of the page requires <a href="https://js.org/">JavaScript</a> in order
15
+      to render the QR code information client side.
16
+    </p>
17
+  </noscript>
18
+  <div class="order-3 self-center pa2">
19
+    <div class="measure">
20
+      <label class="f6 b db mb2 sans" for="email">Generated Code</label>
21
+      <input maxlength=6 autocomplete="off" class="tc code pa2 f4 input-reset ba b--black-20 bg-white measure tracked-mega" type="text" name="code" id="code" placeholder="000000" />
22
+      <small class="db mv1 f6 gray">The code generated by your application.</small>
23
+    </div>
24
+    <details class="measure mt3">
25
+      <summary class="sans">Technical Info</summary>
26
+      <label class="f6 b db mb2 sans" for="totpSecret">Secret</label>
27
+      <input id="totpSecret" readonly autocomplete="off" class="code pa2 f5 input-reset ba b--black-20 bg-white w-100 measure tracked-mega" type="text" value="<%= get_session(@conn, :otp_secret) %>" />
28
+      <small class="db mv1 f6 gray">The secret to use with your OTP application.</small>
29
+    </details>
30
+  </div>
31
+  <div class="order-3">
32
+    <div id="qrCode" class="grow order-1 flex items-center justify-center h5 w5 center"></div>
33
+    <a class="pointer db tc link mv2 v-mid ba w-100 w-third-l b--black pa3 bg-near-black near-white sans-serif" href="<%= totp_uri(@conn) %>">
34
+      <i class="v-mid w1 h1" data-feather="gift"></i>
35
+      Auto Configure Your App
36
+    </a>
37
+    <small class="order-2 center db gray mb2 i w-100">Scan the code above to get your one time passcode.</small>
38
+  </div>
39
+</section>

+ 0
- 48
web/templates/setup/setup-otp.html.eex View File

@@ -1,48 +0,0 @@
1
-<section class="mw7 center pa2">
2
-  <h1 class="f1 lh-title serif">Securing Your Site</h1>
3
-  <p class="lh-copy f4 measure sans-serif">
4
-    In order to confirm it's you making actions on your site, we'll need you
5
-    to set up an <a class="link blue" href="https://en.wikipedia.org/wiki/Software_token">authenticator app</a> 
6
-    that supports the generation of
7
-    <a class="link blue" href="https://indieweb.org/Time-based_One-time_Password_Algorithm">one time passwords</a>.
8
-  </p>
9
-  <form method="post" class="flex flex-column flex-row flex-wrap justify-around items-start">
10
-    <input type=hidden name=_csrf_token value="<%= Phoenix.Controller.get_csrf_token %>" />
11
-    <script>
12
-      window.addEventListener('load', function () {
13
-        var qrTotpUri = "<%= totp_uri(@conn) %>"
14
-        var qrCode = qrcode(0, 'H');
15
-        qrCode.addData(qrTotpUri);
16
-        qrCode.make();
17
-
18
-        var qrHtml = qrCode.createSvgTag(3, 2);
19
-        document.querySelector('#otpSecretWell').innerHTML = qrHtml;
20
-      });
21
-    </script>
22
-    <div id="otpSecretWell" class="db w-100 h-auto order-1 w-third-l tc v-mid bg-near-white grow">
23
-    </div>
24
-    <div class="order-2 flex-grow">
25
-      <div class="mv3">
26
-        <label class="db ttu fw4 lh-copy f5" for="email">Generated Code</label>
27
-        <input maxlength=6 autocomplete="off" class="tc code pa2 f4 input-reset ba b--black-20 bg-white w-100 measure tracked-mega" type="text" name="code" id="code" placeholder="000000" />
28
-        <small class="db mv1 f6 gray">The code generated by your application.</small>
29
-      </div>
30
-      <details class="mv3">
31
-        <summary>Technical Info</summary>
32
-        <label class="db ttu fw4 lh-copy f5" for="email">Secret</label>
33
-        <input readonly autocomplete="off" class="code pa2 f5 input-reset ba b--black-20 bg-white w-100 measure tracked-mega" type="text" value="<%= @conn.assigns[:otp_secret] %>" />
34
-        <small class="db mv1 f6 gray">The secret to use with your OTP application.</small>
35
-      </details>
36
-    </div>
37
-    <div class="w-100 order-3 mv3 flex flex-row-l flex-column items-center justify-around">
38
-      <a class="link tc v-mid mv2 ba w-100 w-third-l b--dark-green pa3 bg-dark-green near-white sans-serif" href="<%= totp_uri(@conn) %>">
39
-        <i class="v-mid h1 w1 mr2" data-feather="gift"></i>
40
-        <span class="v-mid fw5">Automatic Setup</span>
41
-      </a>
42
-      <button class="pointer mv2 v-mid ba w-100 w-third-l b--black pa3 bg-near-black near-white sans-serif" type="submit">
43
-        <i class="v-mid h1 w1 mr2" data-feather="unlock"></i>
44
-        <span class="v-mid fw5">Confirm Code</span>
45
-      </button>
46
-    </div>
47
-  </form>
48
-</section>

+ 80
- 43
web/templates/setup/setup-profile.html.eex View File

@@ -1,44 +1,81 @@
1
-<section class="mw7 w-100 pa2 center">
2
-  <h1 class="tc f1 lh-title serif">Welcome</h1>
3
-  <p class="lh-copy f3 measure sans-serif">
4
-    Hey there! So glad you've decided to begin your journey into the
5
-    <a class="link blue" href="https://indieweb.org">IndieWeb</a>.
6
-    We'll need some information about you before we can continue.
7
-  </p>
8
-  <p class="lh-copy f4 measure gray sans-serif">
9
-    Some fields below are required to be a good <a class="link blue" href="http://archive.is/Mc929" target="_blank">netizen</a>.
10
-    <br />
11
-    Learn more <a class="link blue" href="#" target="_blank">about why Koype uses this information</a>.
12
-  </p>
13
-  <form method="post">
14
-    <input type=hidden name=_csrf_token value="<%= Phoenix.Controller.get_csrf_token %>" />
15
-
16
-    <div class="mv3">
17
-      <label class="db ttu fw4 lh-copy f5" for="email">Email Address</label>
18
-      <input autocomplete="email" class="pa2 f5 input-reset ba b--black-20 bg-white w-100 measure" type="email" name="email" id="email" placeholder="you@<%= @conn.host %>" />
19
-      <small class="db mv1 f6 gray">The address that you'd be using to receive messaging.</small>
20
-    </div>
21
-    <div class="mv3">
22
-      <label class="db ttu fw4 lh-copy f5" for="name">Full Name</label>
23
-      <input autocomplete="name" required class="pa2 f5 input-reset ba b--black-20 bg-white w-100 measure" type="text" name="name" id="name" placeholder="Jane S. Doe" />
24
-      <small class="db mv1 f6 gray">The name you'd want people on the Web to refer to you as.</small>
25
-    </div>
26
-    <div class="mv3">
27
-      <label class="db ttu fw4 lh-copy f5" for="nickname">Nickname / Alias</label>
28
-      <input autocomplete="nickname" required class="pa2 f5 input-reset ba b--black-20 bg-white w-100 measure" type="text" name="nickname" id="nickname" placeholder="Jane S. Doe" />
29
-      <small class="db mv1 f6 gray">The name you'd want to use as opposed to your given/chosen name.</small>
30
-    </div>
31
-    <div class="mv3">
32
-      <label class="db ttu fw4 lh-copy f5" for="note">Short Description</label>
33
-      <textarea required class="pa2 f5 input-reset ba b--black-20 bg-white w-100 measure" name="note" id="note" placeholder="I want to be independent.">
34
-      </textarea>
35
-      <small class="db mv1 f6 gray">A super-brief description of yourself.</small>
36
-    </div>
37
-    <div class="w-100 mv3 flex flex-row-l flex-column items-start justify-between">
38
-      <button class="pointer v-mid ba w-100 w-third-l b--black pa3 bg-near-black near-white sans-serif" type="submit">
39
-        <i class="v-mid h1 w1 mr2" data-feather="save"></i>
40
-        <span class="v-mid">Save</span>
41
-      </button>
42
-    </div>
43
-  </form>
1
+<section id="regionId" class="w-100 mw8 mv3 center pa2 pb3 bt b--moon-gray flex flex-row flex-wrap">
2
+  <h2 class="lh-title tracked tc f2 flex-auto flex-grow w-100">Identification</h2>
3
+  <div class="flex-grow flex-auto ph1">
4
+    <p class="f5 lh-copy measure center tj">
5
+      Koype would like to know a bit about you. This information will
6
+      help build your <a target="_new" href="https://indieweb.org/h-card" class="link navy fw7"><code>h-card</code></a>.
7
+      This controls how you're seen to other websites and services on the Web.
8
+      <a target="_new" class="link navy fw6" href="https://git.jacky.wtf/indieweb/koype/wiki/UsingHcards">Learn more about how <u>Koype uses this info</u></a>.
9
+    </p>
10
+    <fieldset class="measure b--moon-gray b--solid center">
11
+      <legend class="center f4 ttu sans-serif pa2 ba bw1 b--moon-gray">Naming</legend>
12
+      <div class="measure mt3">
13
+        <label id="email-label" for="email" class="sans f6 b db mb2">Email Address<span class="normal gray">(optional)</span></label>
14
+        <input name="email" autocomplete="email" class="input-reset ba b--moon-gray pa2 mb2 db w-100" type="text" aria-describedby="email-desc" aria-labelledby="email-label">
15
+        <small id="email-desc" class="sans f6 gray db mb2">An address at which you'd want your website to be referenced by.</small>
16
+      <div class="measure">
17
+        <label for="name" class="sans f6 b db mb2">Name <span class="normal gray">(optional)</span></label>
18
+        <input name="name" autocomplete="name" class="input-reset ba b--moon-gray pa2 mb2 db w-100" type="text" aria-describedby="name-desc">
19
+        <small id="name-desc" class="sans f6 gray db mb2">Your first, middle and last name.</small>
20
+      </div>
21
+      <div class="measure mt3">
22
+        <label for="nickname" class="sans f6 b db mb2">Nickname <span class="normal gray">(optional)</span></label>
23
+        <input name="nickname"  autocomplete="nickname" class="input-reset ba b--moon-gray pa2 mb2 db w-100" type="text" aria-describedby="nickname-desc">
24
+        <small id="nickname-desc" class="sans f6 gray db mb2">A shorter, alternative name you'd like to go by.</small>
25
+      </div>
26
+      <div class="measure mt3">
27
+        <div class="pretty p-default p-switch">
28
+          <input checked type="checkbox" name="prefer_nickname">
29
+          <div class="state p-primary">
30
+            <label class="f5">Use my nickname when showing my name.</label>
31
+          </div>
32
+        </div>
33
+      </div>
34
+      <p class="f5 tc mt3 bg-light-blue dark-gray b--blue bw1 ba pa1 lh-copy">
35
+        <i class="w1 h1 v-mid" data-feather="info"></i>
36
+        You don't have to fill in both! Only one value is needed.
37
+      </p>
38
+    </fieldset>
39
+    <fieldset class="measure b--moon-gray b--solid mt3 center">
40
+      <legend class="center f4 ttu sans-serif pa2 ba bw1 b--moon-gray">Presentation</legend>
41
+      <div class="measure">
42
+        <label for="photo" class="f6 b db mb2 sans">Avatar <span class="normal gray">(optional)</span></label>
43
+        <input accept="image/*" name="photo" class="input-reset bn mb2 db w-auto" type="file" aria-describedby="photo-desc">
44
+        <small id="photo-desc" class="f6 gray db mb2 sans">The image that'll be used to present you.</small>
45
+      </div>
46
+      <div class="measure mt3">
47
+        <label for="note" class="sans f6 b db mb2">Note <span class="normal gray">(optional)</span></label>
48
+        <div id="noteEditor" class="w-100 bg-white pa1 mb2 b--moon-gray ba h4">
49
+          <div id="noteToolbar"></div>
50
+        </div>
51
+        <textarea name="note" class="dn border-box hover-black w-100 measure ba b--moon-gray pa2 br2"></textarea>
52
+        <small id="note-desc" class="sans f6 gray db mv2">
53
+          A short text about yourself. <strong>Rich</strong> <u>text</u> is <em>supported</em>.
54
+          Just select some text and a toolbar will appear.
55
+        </small>
56
+      </div>
57
+    </fieldset>
58
+  </div>
59
+  <div class="center self-start pa2">
60
+    <p class="f5 w-100 measure-narrow-l lh-copy tc tl-l">This'll what your resulting card would look like to other users.</p>
61
+    <article class="mw5 bg-white br3 ph2 pv3 mv3 ba b--black-10 center">
62
+      <div class="tc">
63
+        <img id="profileImage" src="https://loremflickr.com/g/512/512/dog" class="br-100 h-auto w4 dib" title="Photo of a kitty staring at you">
64
+        <h4 id="preferredName" class="f4 mb2">Mimi Whitehouse</h4>
65
+        <h6 class="ma0 lh-solid gray code f6">@<span id="did">mimi</span>@<%= URI.parse(Koype.host).host %></h6>
66
+        <hr class="mw3 bb bw1 b--black-10">
67
+      </div>
68
+      <p id="note" class="lh-copy measure center f6 black-70 tj mb1">
69
+        Quite affectionate and outgoing.
70
+        She loves to get chin scratches and will
71
+        roll around on the floor waiting for you give her more of them.
72
+      </p>
73
+      <hr class="mw3 bb bw1 b--black-10">
74
+      <p class="lh-copy measure center tc f6 black-50 sans-serif flex flex-row flex-wrap justify-around">
75
+        <a href="#" class="link dim navy fw7"><i class="w1 h1 v-mid" data-feather="file-text"></i>&nbsp;About</a>
76
+        <a href="#" class="link dim navy fw7"><i class="w1 h1 v-mid" data-feather="rss"></i>&nbsp;Follow</a>
77
+        <a href="#" class="link dim navy fw7"><i class="w1 h1 v-mid" data-feather="at-sign"></i>&nbsp;Contact</a>
78
+      </p>
79
+    </article>
80
+  </div>
44 81
 </section>

+ 25
- 0
web/templates/setup/view.html.eex View File

@@ -0,0 +1,25 @@
1
+<section class="dn w-100 mw8 mv3 center pa2 pb3 justify-center items-center flex flex-column">
2
+  <h1 class="lh-title f1 tracked-tight">Setting Up</h1>
3
+  <p class="f4 lh-copy measure tj">
4
+    Hey there! So glad you've chosen Koype as your platform for
5
+    the <a target="_new" class="link navy fw7" href="https://indieweb.org/why">independent Web</a>.
6
+    This page aims to set up some important bits of information to faciliate
7
+    your site's compability with other services.
8
+  </p>
9
+</section>
10
+<form enctype="<%= form_enctype(assigns[:state]) %>" accept-charset="utf-8" autocomplete="on" id="setupForm" class="w-100" method="post" action="<%= setup_path(@conn, :handle) %>">
11
+  <%= render @view_module, "setup-#{assigns[:state]}.html", assigns %>
12
+  <footer class="flex flex-row flex-wrap justify-end center w-100 mw7 pv3">
13
+    <p id="errorMessage" class="w-100 w-auto-l flex-auto flex-grow dn"></p>
14
+    <button data-style="expand-right" class="ladda-button ttu pointer w-100 w-third-ns dim v-mid ba b--black pa3 bg-near-black near-white sans-serif" type="submit">
15
+      <span class="ladda-label">
16
+        <i class="v-mid h1 w1 mr2" data-feather="save">Save</i>
17
+        <span class="v-mid">Continue</span>
18
+      </span>
19
+    </button>
20
+  </footer>
21
+  <input type="hidden" name="_method" value="put" />
22
+  <input type="hidden" name="_csrf_token" value="<%= Phoenix.Controller.get_csrf_token %>" />
23
+  <input type="hidden" name="state" value="<%= assigns[:state] %>" />
24
+</form>
25
+<script async defer src="<%= static_path(@conn, "/assets/js/setup.js") %>"></script>

+ 17
- 16
web/views/entry_view.ex View File

@@ -16,32 +16,33 @@ defmodule Koype.Web.EntryView do
16 16
     video: "video",
17 17
     checkin: "map-pin",
18 18
     listen: "headphones",
19
-    watch: "monitor"
19
+    watch: "monitor",
20
+    follow: "user-plus",
21
+    gameplay: "award",
22
+    read: "book-open",
23
+    rsvp: "check-circle",
24
+    payment: "shopping-cart",
25
+    donation: "gift"
20 26
   }
21 27
 
22 28
   def title("view.html", %{type: type, entry: entry} = _assigns), do: determine_title(type, entry)
23 29
 
24
-  @impl true
25
-  def tags(:link, _template, _assigns), do: ~w(bookmark canonical)a
26
-
27
-  @impl true
28
-  def tags(:meta, _template, _assigns), do: []
29
-
30
-  @impl true
31
-  def tag({:meta, type}, "view.html", %{type: type, entry: entry} = _assigns) do
30
+  def tag(attr, template, assigns)
31
+  def tag({:link, type}, "view.html", %{type: type, entry: entry} = _assigns) do
32 32
     case type do
33
-      :description -> extract_page_description(type, entry)
33
+      :bookmark -> Entry.get_uri(entry)
34
+      :canonical -> Entry.get_uri(entry)
34 35
     end
35 36
   end
36
-
37
-  @impl true
38
-  def tag({:link, type}, "view.html", %{type: type, entry: entry} = _assigns) do
37
+  def tag({:meta, type}, "view.html", %{type: type, entry: entry} = _assigns) do
39 38
     case type do
40
-      :bookmark -> Entry.get_url(entry)
41
-      :canonical -> Entry.get_url(entry)
39
+      :description -> extract_page_description(type, entry)
42 40
     end
43 41
   end
44 42
 
43
+  def tags(:meta, _template, _assigns), do: []
44
+  def tags(:link, _template, _assigns), do: ~w(bookmark canonical)a
45
+
45 46
   def humanize_date(str) when is_binary(str) do
46 47
     {:ok, dt} = Calendar.DateTime.Parse.rfc3339_utc(str)
47 48
     Calendar.DateTime.Format.httpdate(dt)
@@ -104,7 +105,7 @@ defmodule Koype.Web.EntryView do
104 105
       {:ok, dt} ->
105 106
         time_ago_in_words(dt)
106 107
 
107
-      {:error, error} ->
108
+      {:error, _error} ->
108 109
         date_str
109 110
     end
110 111
   end

+ 5
- 0
web/views/rel_me_view.ex View File

@@ -0,0 +1,5 @@
1
+defmodule Koype.Web.RelMeView do
2
+  use Koype.Web, :view
3
+
4
+  def title("view.html", _assigns), do: "RelMe"
5
+end

+ 19
- 12
web/views/setup_view.ex View File

@@ -1,22 +1,29 @@
1 1
 defmodule Koype.Web.SetupView do
2 2
   use Koype.Web, :view
3
+  import Plug.Conn, only: [get_session: 2]
3 4
 
4
-  def title("index.html", _assigns), do: "Getting Started"
5
-  def title("setup-profile.html", _assigns), do: "Profile - Getting Started"
6
-  def title("setup-otp.html", _assigns), do: "Authentication - Getting Started"
5
+  def title("view.html", _assigns), do: "Setting Up"
7 6
 
8 7
   def totp_uri(conn) do
9 8
     host = URI.parse(Koype.host()).host
9
+    secret = get_session(conn, :otp_secret)
10
+    totp_uri = %URI{
11
+      scheme: "otpauth",
12
+      host: "totp",
13
+      path: "/#{host}:#{Koype.Profile.nickname()}",
14
+      query: URI.encode_query(%{
15
+          secret: secret,
16
+          issuer: host,
17
+          period: 30,
18
+          digits: 6,
19
+          algorithm: "SHA256"
20
+        })
21
+    }
10 22
 
11
-    "otpauth://totp/#{host}:#{Koype.Profile.get(:email)}?secret=#{conn.assigns[:otp_secret]}&issuer=#{
12
-      host
13
-    }&period=30&digits=6&algorithm=SHA256"
23
+    URI.to_string(totp_uri)
14 24
   end
15 25
 
16
-  def current_setup_state do
17
-    case Koype.ready_for_use?() do
18
-      {:error, error} -> error
19
-      :ok -> :ready
20
-    end
21
-  end
26
+  def form_enctype(state)
27
+  def form_enctype(:profile), do: "multipart/form-data"
28
+  def form_enctype(_), do: "application/x-www-form-urlencoded"
22 29
 end

Loading…
Cancel
Save