Browse Source

feat(setup): Add flexible system.

tags/v0.0.1^2
jackyalcine 1 year ago
parent
commit
f8f73d60a7
Signed by: me <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 = {
'postcss-urlrewrite': {
imports: true,
rules: [{
from: /source-/,
to: '/assets/css/source-'
from: /css\/images/,
to: '/assets/images/'
}]
},
'postcss-pxtorem': {
unitPrecision: 4,
unitPrecision: 8,
propWhiteList: [],
replace: true,
selectorBlackList: [],
minPixelValue: 4
minPixelValue: 8
},
cssnano: ENV === 'production' ? {
preset: [
@@ -40,10 +40,10 @@ module.exports = {
} : false,
'css-mqpacker': [],
'postcss-url': [{
filter: './files/source**',
filter: './css/images/*',
url: 'copy',
assetsPath: './',
useHash: false
useHash: true
}, ],
'mdcss': {
theme: require('mdcss-theme-github')


+ 6
- 4
Dockerfile View File

@@ -7,19 +7,21 @@ ENV MIX_ENV=${MIX_ENV:-prod} \
TZ=Etc/UTC \
REPLACE_OS_VARS=true

ADD docker/rootfs/ /

RUN mkdir /tmp/koype-docker
COPY docker/rootfs /
ADD docker/scripts/ /tmp/koype-docker/

RUN sh /tmp/koype-docker/docker-prepare.sh
RUN sh /tmp/koype-docker/prepare.sh

WORKDIR /opt/koype

COPY . /opt/koype/
RUN sh /tmp/koype-docker/docker-build.sh
RUN sh /tmp/koype-docker/build.sh

VOLUME /opt/koype/priv/repo/db
RUN sh /tmp/koype-docker/docker-cleanup.sh
RUN sh /tmp/koype-docker/cleanup.sh

SHELL ["/bin/bash"]
CMD ["/tmp/koype-docker/docker-entrypoint.sh"]
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,
secret_key: {:system, :string, "GUARDIAN_SECRET_KEY"},
schema_name: "guardian_tokens",
token_types: ["refresh_token"],
sweep_interval: 60
sweep_interval: 15

config :logger, :console,
format: "$time $metadata[$level] $message\n",
@@ -58,7 +58,7 @@ config :arc,

config :ex_aws, :hackney_opts,
follow_redirect: true,
recv_timeout: 30_000
recv_timeout: 3_000

config :ex_aws,
storage_dir: "koype",


+ 1
- 2
config/dev.exs View File

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


+ 6
- 1
config/test.exs View File

@@ -7,6 +7,11 @@ config :koype, Koype.Repo,
pool: Ecto.Adapters.SQL.Sandbox,
database: "priv/repo/db/test.db"

config :koype, Koype.Web.Endpoint,
debug_errors: true,
code_reloader: false,
check_origin: false

config :logger, level: :debug

config :exvcr,
@@ -22,7 +27,7 @@ config :exvcr,
config :hound,
driver: "chrome_driver",
host: "webdriver",
app_host: "http://site",
app_host: "http://localhost",
app_port: 5000,
port: 4444,
path_prefix: "wd/hub/"

+ 5
- 3
docker-compose.yml View File

@@ -38,6 +38,8 @@ services:
- "./tmp/docker/redis:/data"
networks:
- network
ports:
- 6379
objectstorage:
image: "minio/minio:RELEASE.2018-11-22T02-51-56Z"
command: server /data
@@ -51,7 +53,7 @@ services:
MINIO_REGION: "local"
MINIO_BROWSER: "on"
ports:
- "4001:9000"
- "9000:9000"
networks:
- network
site:
@@ -85,8 +87,8 @@ services:
- "./:/opt/koype"
- "./priv/repo/db:/opt/koype/priv/repo/db"
healthcheck:
test: ["CMD", "/tmp/koype-docker/docker-healthcheck.sh"]
interval: 10s
test: ["CMD", "/tmp/koype-docker/healthcheck.sh"]
interval: 60s
timeout: 5s
retries: 5
networks:


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

@@ -1,3 +1,11 @@
verbose=true
color=false
exact=true
link=false
long=true
optional=false
progress=true
save-exact=true
usage=false
verbose=true
version=true
viewer=less

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

@@ -1,7 +1,11 @@
#!/bin/sh

HEX_HTTP_CONCURRENCY=5
HEX_HTTP_TIMEOUT=600
NODE_ENV=${ENV}

echo " ---> [npm] Pulling dependencies..."
NODE_ENV=${ENV} npm install --no-bin-links || exit 50
npm install --no-bin-links || exit 50

echo " ---> [mix] Preparing..."
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 @@

# TODO: Hit an endpoint and expect "OK", 200 to come back.


curl -f "http://${HOST}:5000/version"

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

@@ -1,4 +0,0 @@
#!/bin/sh

echo "[npm] Building client JavaScript..."
npm run parcel:build

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

@@ -1,18 +0,0 @@
#!/bin/sh

cd /opt/koype || exit 10

echo " ---> [deploy:pre] Creating database file..."
touch "/opt/koype/priv/repo/db/${MIX_ENV:-prod}.db" || exit 15

echo " ---> [deploy:pre] Setting up database..."
mix ecto.setup || exit 20

echo " ---> [deploy:pre] Update static digests..."
mix phx.digest || exit 40

echo " ---> [deploy:pre] Confirm permissions on object storage..."
# mix koype.storage_check || exit 50

echo " ---> [deploy:pre] Run production-ready checks... "
# 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 @@
#!/bin/sh

# TODO: Hit an endpoint and expect "OK", 200 to come back.

echo " ----> [healthcheck] <skip> Creating database file..."
curl -f "http://${HOST}:5000/version"

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

@@ -0,0 +1,4 @@
#!/bin/sh

echo " ----> [npm] Building client JavaScript..."
npm run parcel:build

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

@@ -0,0 +1,18 @@
#!/bin/sh

cd /opt/koype || exit 10

echo " ----> [deploy:pre] Creating database file..."
touch "/opt/koype/priv/repo/db/${MIX_ENV:-prod}.db" || exit 15

echo " ----> [deploy:pre] Setting up database..."
mix ecto.setup || exit 20

echo " ----> [deploy:pre] Update static digests..."
mix phx.digest || exit 40

echo " ----> [deploy:pre] <skip> Confirm permissions on object storage..."
# mix koype.storage_check || exit 50

echo " ----> [deploy:pre] <skip> Run production-ready checks... "
# mix koype.smoke_test || exit 60

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

@@ -1,30 +1,35 @@
#!/bin/sh

ONE_WEEK_IN_MINUTES=10080

echo " ---> [apk] Syncing repos (allowed to fail)..."
apk update --cache-max-age="${ONE_WEEK_IN_MINUTES}" --verbose

echo " ---> [apk] Fetching baseline packages..."
apk add --update --no-cache --verbose \
apk add --verbose \
bash \
ca-certificates \
coreutils \
shadow \
tzdata \
curl \
sqlite-libs \
inotify-tools \
imagemagick \
ffmpeg \
sox \
gcc \
util-linux \
nodejs=8.14.0-r0 \
npm=8.14.0-r0 \
|| exit 20

echo " ---> [apk] Fetching development packages..."
apk add --update --no-cache --virtual=build --verbose \
apk add --virtual=build --verbose \
gcc \
util-linux \
build-base \
binutils-dev \
libelf-dev \
sqlite-dev \
|| exit 30

echo " ---> [apk] Syncing cache... "
apk cache sync

mkdir -p /opt/koype

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

@@ -17,30 +17,31 @@ defmodule IndieWeb.Post do
def known_types do
[
%{type: "article", name: "Article", names: "Articles"},
# %{type: "audio", name: "Audio", names: "Audio"},
%{type: "audio", name: "Audio", names: "Audio"},
%{type: "bookmark", name: "Bookmark", names: "Bookmarks"},
%{type: "checkin", name: "Check In", names: "Check Ins"},
%{type: "like", name: "Like", names: "Likes"},
# %{type: "listen", name: "Listen", names: "Listens"},
%{type: "listen", name: "Listen", names: "Listens"},
%{type: "note", name: "Note", names: "Notes"},
%{type: "reply", name: "Reply", names: "Replies"},
%{type: "repost", name: "Repost", names: "Reposts"},
# %{type: "donation", name: "Donation", names: "Donations"},
# %{type: "payment", name: "Payment", names: "Payments"},
%{type: "event", name: "Event", names: "Events"}
%{type: "donation", name: "Donation", names: "Donations"},
%{type: "payment", name: "Payment", names: "Payments"},
%{type: "event", name: "Event", names: "Events"},
# %{type: "exercise", name: "Workout", names: "Workouts"},
# %{type: "follow", name: "Follow", names: "Follows"},
%{type: "follow", name: "Follow", names: "Follows"},
# %{type: "food", name: "Food", names: "Meals"},
# %{type: "gameplay", name: "Game Play", names: "Game Plays"},
%{type: "gameplay", name: "Game Play", names: "Game Plays"},
# %{type: "issue", name: "Issue", names: "Issues"},
# %{type: "photo", name: "Photo", names: "Photos"},
%{type: "photo", name: "Photo", names: "Photos"},
# %{type: "presentation", name: "Presentation", names: "Presentations"},
# %{type: "quotation", name: "Quotation", names: "Quotations"},
# %{type: "read", name: "Read", names: "Reads"},
%{type: "read", name: "Read", names: "Reads"},
# %{type: "sleep", name: "Sleep", names: "Sleeps"},
# %{type: "venue", name: "Venue", names: "Venues"},
# %{type: "watch", name: "Watch", names: "Watches"},
# %{type: "video", name: "Video", names: "Videos"},
%{type: "watch", name: "Watch", names: "Watches"},
%{type: "video", name: "Video", names: "Videos"},
%{type: "rsvp", name: "RSVP", names: "RSVPs"}
]
end

@@ -152,8 +153,6 @@ defmodule IndieWeb.Post do
do: bookmark

def get_properties_for_type(:checkin, %{location: location} = data) when is_binary(location) do
Apex.ap(data)

if String.starts_with?(location, "geo:") do
regex = ~r/geo\:c(?<lat>\d+)\,c(?<lng>\d+)/
%{lat: lat, lng: lng} = Regex.named_captures(regex, location)


+ 1
- 9
lib/koype.ex View File

@@ -13,19 +13,11 @@ defmodule Koype do
scheme <> "://" <> host

_ ->
"http://localhost"
Koype.Web.Endpoint.url()
end
end

def version do
Koype.Mixfile.project()[:version]
end

def ready_for_use? do
cond do
Profile.complete?() == false -> {:error, :profile}
OtpSecret.current() == nil -> {:error, :otp}
true -> :ok
end
end
end

+ 7
- 0
lib/profile.ex View File

@@ -52,6 +52,13 @@ defmodule Koype.Profile do
def note(), do: get("note")
def email(), do: get("email")

def photo() do
case get("photo") do
nil -> nil
path -> Koype.Storage.Photo.url({path, :floating}, :original, signed: false)
end
end

def flagship_entry() do
case get("flagship_entry_id") do
nil -> nil


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

@@ -1,6 +1,8 @@
defmodule Koype.Repo.OtpSecret do
@moduledoc "Logic for OTP secrets."
@secret_age 60 * 60 * 24 * 30
@required_attrs ~w(secret expired_at)a
@otp_secret_size 16

use Ecto.Schema
import Ecto.Changeset
@@ -74,12 +76,12 @@ defmodule Koype.Repo.OtpSecret do
end
end

def generate() do
Base.encode32(:crypto.strong_rand_bytes(16))
end
def generate(), do: @otp_secret_size |> :crypto.strong_rand_bytes() |> Base.encode32()

def confirm_against_secret(secret, code) do
if Totpex.generate_totp(secret) == code do
Apex.ap([secret, Totpex.generate_totp(secret), code])

if Totpex.validate_totp(secret, code, grace_periods: 2) do
:ok
else
{:error, :otp_code_mismatch}


+ 15
- 0
lib/setup.ex View File

@@ -0,0 +1,15 @@
defmodule Koype.Setup do
@moduledoc """
Provides logic for discovering and handling prerequistie configuration of Koype.
"""

def complete?(), do: state() == :ok

def state() do
cond do
Koype.Profile.complete?() == false -> :profile
Koype.Repo.OtpSecret.current() == nil -> :auth
true -> :ok
end
end
end

+ 1
- 2
mix.exs View File

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


+ 1
- 1
mix.lock View File

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


+ 50
- 4
package-lock.json View File

@@ -2672,8 +2672,7 @@
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
},
"deep-is": {
"version": "0.1.3",
@@ -3093,6 +3092,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo="
},
"events": {
"version": "1.1.1",
"resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
@@ -3276,8 +3280,7 @@
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extend-shallow": {
"version": "3.0.2",
@@ -3447,6 +3450,11 @@
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
},
"fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
},
"fast-glob": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.4.tgz",
@@ -8216,6 +8224,11 @@
}
}
},
"parchment": {
"version": "1.1.4",
"resolved": "http://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
@@ -9886,6 +9899,11 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
"qartjs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/qartjs/-/qartjs-1.1.2.tgz",
"integrity": "sha1-UWU6WjAQiJ69NmL9knwnps6ntgQ="
},
"qrcode-generator": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.1.tgz",
@@ -9922,6 +9940,29 @@
"integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=",
"dev": true
},
"quill": {
"version": "1.3.6",
"resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
"integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
"requires": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.1",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"requires": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
}
},
"quote-stream": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz",
@@ -12441,6 +12482,11 @@
"indexof": "0.0.1"
}
},
"voca": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/voca/-/voca-1.4.0.tgz",
"integrity": "sha512-8Xz4H3vhYRGbFupLtl6dHwMx0ojUcjt0HYkqZ9oBCfipd/5mD7Md58m2/dq7uPuZU/0T3Gb1m66KS9jn+I+14Q=="
},
"w3c-hr-time": {
"version": "1.0.1",
"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 @@
"test": "test/web"
},
"scripts": {
"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",
"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",
"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",
"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",
"test": "jest"
},
"repository": {
@@ -37,18 +37,21 @@
"postcss-scss": "2.0.0",
"postcss-strip-inline-comments": "0.1.5",
"pretty-checkbox": "3.0.3",
"qartjs": "1.1.2",
"qrcode-generator": "1.4.1",
"quill": "^1.3.6",
"sweet-alert": "^2.0.5",
"sweetalert": "^2.1.2",
"tachyons": "4.10.0",
"tachyons-sass": "4.9.5",
"timeago.js": "4.0.0-beta.1",
"ts-node": "7.0.1",
"voca": "1.4.0",
"webfontloader": "^1.6.28"
},
"engines": {
"node": "8.14.0",
"npm": "6.4.0"
"npm": "6.5.0"
},
"devDependencies": {
"@types/jest": "23.3.10",


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

@@ -4,9 +4,23 @@ defmodule Koype.Acceptance.SetupFlowTest do
hound_session()

describe "navigating to setup for first test" do
test "try it out" do
test "fully setup Koype on first run" do
# TODO: Wipe database clean.
navigate_to("/")
assert page_title() =~ "Getting Started"
assert page_title() =~ "Setting Up - koype"
assert current_path() == setup_path(Koype.Web.Endpoint, :view)

# On /setup
email = Faker.Internet.email()
name = Faker.Name.first_name()
nickname = Faker.Internet.user_name()
fill_field({:name, "name"}, name)
fill_field({:name, "nickname"}, nickname)
fill_field({:name, "email"}, email)
fill_field({:name, "note"}, Faker.Lorem.sentence())
# fill_field({:name, "photo"}, Faker.Avatar.image_url)
submit_element({:name, "name"})
assert page_title() =~ "Setting Up - koype"
assert current_path() == setup_path(Koype.Web.Endpoint, :view)
end
end


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

@@ -2,65 +2,131 @@ defmodule Koype.Web.SetupControllerTest do
use Koype.Web.ConnCase
use Plug.Test
import Mock
import Koype.Factory

@actions %{
otp: %{"code" => 000_000},
name: %{"name" => Faker.Name.first_name()}
}
describe ".view/2" do
test "renders page when setup is incomplete" do
@route setup_path(Koype.Web.Endpoint, :view)

test "redirects user to their homepage when setup is complete" do
with_mock(
Koype.Setup,
[:passthrough],
complete?: fn -> true end
) do
conn = get(build_conn(), "/setup")
home_path = page_path(Koype.Web.Endpoint, :index)
conn = get(build_conn(), @route)

assert redirected_to(conn, 302) == home_path
end
end

assert html_response(conn, 200)
test "renders setup when setup is incomplete" do
with_mock(Koype.Setup, complete?: fn -> false end, state: fn -> :profile end) do
conn = get(build_conn(), @route)

assert html_response(conn, :ok)
assert called(Koype.Setup.state())
end
end

test "redirects user to their homepage" do
with_mock(Koype.Setup, [:passthrough], complete?: fn -> false end) do
conn = get(build_conn(), "/setup")
test "renders setup for auth when profile setup is complete" do
with_mock(Koype.Setup, complete?: fn -> false end, state: fn -> :auth end) do
conn = get(build_conn(), @route)

assert redirected_to(conn, 302) == "/"
assert html_response(conn, :ok) =~ "Authentication"
assert called(Koype.Setup.state())
end
end
end

describe ".handle/3" do
test "successfully sets the option" do
for {state, params} <- @actions do
route = setup_path(Koype.Web.Endpoint, :handle, state)
conn = build_conn() |> add_csrf_token |> put(route, params)
assert resp = json_response(conn, 201)
describe ".handle/2" do
test "fails to upload photo for profile" do
route = setup_path(Koype.Web.Endpoint, :handle)

with_mock(Koype.Storage.Photo, store: fn _ -> {:error, :upload_error} end) do
conn =
build_conn()
|> add_csrf_token
|> put(route, %{
name: Faker.Name.name(),
nickname: Faker.Internet.user_name(),
note: Faker.Lorem.sentence(),
photo: nil,
state: "profile"
})

assert html_response(conn, 422)
end
end

test "fails when provides invalid value for profile"
test "fails if no secret is in the session"
test "fails if code is invalid"
end
test "successfully sets up profile" do
route = setup_path(Koype.Web.Endpoint, :handle)
photo = make_mock_upload(:image)
mock_file_name = Faker.File.file_name(:image)

describe ".check/2" do
@route setup_path(Koype.Web.Endpoint, :check)
with_mock(
Koype.Storage.Photo,
[:passthrough],
store: fn _val -> {:ok, mock_file_name} end
) do
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> Plug.Test.init_test_session(%{})
|> add_csrf_token
|> put(route, %{
name: Faker.Name.name(),
nickname: Faker.Internet.user_name(),
note: Faker.Lorem.sentence(),
photo: photo,
prefer_nickname: true,
state: "profile"
})

assert html_response(conn, 200) =~ ""
assert get_flash(conn, :success) =~ "Let's continue with the setup"
assert Koype.Setup.state() == :auth
end
end

test "reports status about incomplete state" do
conn = build_conn() |> add_csrf_token |> get(@route)
test "fails if no secret is in the session" do
route = setup_path(Koype.Web.Endpoint, :handle)
code = Enum.random(100_000..999_999)

assert resp = json_response(conn, 200)
assert Enum.all?(~w(incomplete complete), fn key -> Map.has_key?(resp, key) end)
refute %{incomplete: []} = resp
Koype.Profile.set("nickname", Faker.Internet.user_name())
Koype.Profile.set("note", Faker.Lorem.sentence())

conn =
build_conn()
|> Plug.Test.init_test_session(%{})
|> add_csrf_token
|> put(route, %{
code: code,
state: "auth"
})

assert html_response(conn, 422)
assert Koype.Setup.state() == :auth
end

test "reports nothing if setup is complete" do
conn = build_conn() |> add_csrf_token |> get(@route)
test "fails if code is invalid" do
route = setup_path(Koype.Web.Endpoint, :handle)
code = Enum.random(100_000..999_999)
otp_secret = Koype.Repo.OtpSecret.generate()

Koype.Profile.set("nickname", Faker.Internet.user_name())
Koype.Profile.set("note", Faker.Lorem.sentence())

conn =
build_conn()
|> Plug.Test.init_test_session(%{otp_secret: otp_secret})
|> add_csrf_token
|> put(route, %{
code: code,
state: "auth"
})

assert resp = json_response(conn, 200)
assert Enum.all?(~w(incomplete complete), fn key -> Map.has_key?(resp, key) end)
assert %{complete: "all"} = resp
assert %{incomplete: []} = resp
assert html_response(conn, 422)
assert Koype.Setup.state() == :auth
end
end
end

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

@@ -143,7 +143,7 @@ defmodule Koype.Factory do
}

{:ok, body} = File.read(Path.absname(type_to_file[type]))
File.write!(file, body)
:ok = File.write!(file, body)

content_type =
case type do
@@ -152,8 +152,8 @@ defmodule Koype.Factory do
end

%Plug.Upload{
filename: Faker.File.file_name(type),
path: file,
filename: Faker.File.file_name(type) <> Path.extname(type_to_file[type]),
path: file <> Path.extname(type_to_file[type]),
content_type: content_type
}
end


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

@@ -8,7 +8,7 @@ defmodule Koype.Repo.OtpSecretTest do
@now Calendar.DateTime.now!("UTC")
@expired_time @now |> Calendar.DateTime.subtract!(@offset)
@future_time @now |> Calendar.DateTime.add!(@offset)
@secret :crypto.strong_rand_bytes(16) |> Base.encode32()
@secret OtpSecret.generate()

describe ".current/0" do
test "returns nil if no secrets are currently configured" do
@@ -59,12 +59,18 @@ defmodule Koype.Repo.OtpSecretTest do
test "confirms that code is valid for secret" do
code = Totpex.generate_totp(@secret)
assert insert(:otp_secret, secret: @secret)
assert OtpSecret.valid?(code)
refute OtpSecret.valid?(000_000)
assert :ok = OtpSecret.valid?(code)
assert {:error, :otp_code_mismatch} = OtpSecret.valid?(000_000)
end

test "fails if no secret is set" do
assert {:error, :no_secret} = OtpSecret.valid?(000_000)
end
end

describe ".confirm_against_secret/2" do
test "passes for a secret and a valid code" do
assert OtpSecret.confirm_against_secret(@secret, Totpex.generate_totp(@secret))
end
end
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

## Transports
transport(:websocket, Phoenix.Transports.WebSocket)
# transport :longpoll, Phoenix.Transports.LongPoll
transport :longpoll, Phoenix.Transports.LongPoll

# Socket params are passed from the client and can
# 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
end

def index(conn, _params) do
case Koype.ready_for_use?() do
{:error, :db_error} ->
conn
|> Plug.Conn.put_resp_header("content-type", "application/json")
|> Explode.bad_request()

{:error, _error} ->
conn
|> put_flash(:info, "Hey there! Let's finish setting up your site.")
|> redirect(to: "/setup")

:ok ->
conn
|> render("index.html", entries: loaded_entries())
Logger.info("Is the setup process complete? #{Koype.Setup.complete?()}")

if !Koype.Setup.complete?() do
conn
|> put_flash(:info, "Hey there! Let's finish setting up your site.")
|> redirect(to: "/setup")
else
conn
|> render("index.html", entries: loaded_entries())
end
end



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

@@ -0,0 +1,7 @@
defmodule Koype.Web.RelMeController do
use Koype.Web, :controller

def view(conn, params) do
render(conn, "view.html", params)
end
end

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

@@ -1,152 +1,133 @@
defmodule Koype.Web.SetupController do
use Koype.Web, :controller
alias Koype.Repo.OtpSecret
alias Koype.Profile
alias Koype.Repo
alias Koype.{Setup, Profile}
alias Koype.Repo.{OtpSecret, Setting}
alias Koype.Storage.Photo

@error_to_uri %{
otp: "/setup/otp",
profile: "/setup/profile"
}
defp do_handle_photo_upload(photo)
defp do_handle_photo_upload(nil), do: {:error, :no_photo_provided}

def check(conn, _params), do: json(conn, %{})
defp do_handle_photo_upload(%Plug.Upload{} = photo) do
case Photo.store({photo, :floating}) do
{:ok, url} ->
Logger.info("Uploaded #{url} as the user's avatar.")
:ok = Profile.set("photo", url)

def index(conn, _params) do
case Koype.ready_for_use?() do
{:error, error} ->
conn
|> put_flash(:info, "Your site's not yet fully set up.")
|> render("index.html", %{
next: %{
state: error,
uri: @error_to_uri[error]
}
})
{:error, error}
end
end

defp do_update_profile(params) do
Enum.all?(params, fn {key, value} -> Profile.set(key, value) end)
end

defp do_apply_preferred_display_name(params) do
result =
if Map.get(params, "prefer_nickname", false) do
Setting.set("displayed_name", "nickname")
else
Setting.set("displayed_name", "name")
end

case result do
{:ok, _setting} -> :ok
{:error, _cs} -> :error
end
end

defp do_render_next(conn) do
case Koype.Setup.state() do
:ok ->
redirect(conn, to: "/")
conn
|> put_flash(:success, "And just like that; you're done! Welcome.")
|> redirect(to: page_path(conn, :index))

next_state ->
render(conn, "view.html", state: next_state)
end
end

def view(conn, params)
defp do_create_otp_secret(code, secret)
defp do_create_otp_secret(_code, nil), do: {:error, :no_otp_secret}

def view(conn, %{"page" => page} = _params) do
case Koype.ready_for_use?() do
:ok ->
redirect(conn, to: "/")

_ ->
case page do
"profile" ->
render(conn, "setup-profile.html")

"otp" ->
secret =
case get_session(conn, :otp_secret) do
nil -> OtpSecret.generate()
existing_secret -> existing_secret
end

conn
|> put_session(:otp_secret, secret)
|> assign(:otp_secret, secret)
|> render("setup-otp.html")

_ ->
redirect(conn, to: "/setup")
end
defp do_create_otp_secret(code, secret) do
with(
:ok <- OtpSecret.confirm_against_secret(secret, code),
{:ok, _otp} <- OtpSecret.create(secret)
) do
:ok
else
{:error, :otp_code_mismatch} -> {:error, :otp_code_mismatch}
{:error, %Ecto.Changeset{} = cs} -> {:error, cs.errors}
end
end

def view(conn, _params), do: render(conn, "index.html")
defp do_attach_otp_secret(conn) do
secret =
case get_session(conn, :otp_secret) do
nil -> OtpSecret.generate()
existing_secret -> existing_secret
end

def complete(conn, params)
conn
|> assign(:otp_secret, secret)
|> put_session(:otp_secret, secret)
end

def complete(conn, %{"page" => "otp", "code" => code} = _params) do
secret = get_session(conn, :otp_secret)
defp do_humanize_auth_error(error)
defp do_humanize_auth_error(:otp_code_mismatch), do: "The code you provided didn't match the one Koype expected."

assert_secret = fn ->
if secret != nil do
:ok
else
{:error, :no_otp_secret}
end
def view(conn, params) do
if Setup.complete?() do
redirect(conn, to: page_path(conn, :index))
else
state = Map.get(params, "state", Koype.Setup.state())

conn
|> do_attach_otp_secret
|> render("view.html", state: state)
end
end

def handle(conn, params)
def handle(conn, %{"state" => ""} = params),
do: handle(conn, Map.put(params, "state", Koype.Setup.state() |> Atom.to_string()))
def handle(conn, %{"state" => "profile"} = params) do
with(
:ok <- assert_secret.(),
:ok <- OtpSecret.confirm_against_secret(secret, code),
{:ok, _otp} <- OtpSecret.create(secret)
:ok <- do_handle_photo_upload(params["photo"]),
true <- do_update_profile(Map.take(params, ~w(name nickname note email))),
:ok <- do_apply_preferred_display_name(Map.take(params, ~w(nickname name prefer_nickname)))
) do
conn
|> put_session(:otp_secret, nil)
|> put_status(:ok)
|> put_flash(
:success,
"Awesome! You've secured your website. Nice work."
"Awesome work, #{Koype.Profile.displayed_name()}! Let's continue with the setup."
)
|> redirect(to: "/setup")
|> do_render_next
else
{:error, :no_otp_secret} ->
conn
|> put_status(:bad_request)
|> put_flash(:warn, "OTP secret not found; applying a new one.")
|> put_session(:otp_secret, OtpSecret.generate())
|> render("setup-otp.html")
{:error, error} ->
conn |> put_flash(:error, error) |> put_status(422) |> render("view.html", state: :profile)

{:error, :otp_code_mismatch} ->
conn
|> put_status(:unprocessable_entity)
|> assign(:otp_secret, secret)
|> put_flash(
:error,
"The code you provided wasn't correct. Please try again."
)
|> render("setup-otp.html")

{:error, %Ecto.Changeset{} = cs} ->
false ->
# TODO: Add more specifics about the failure here.
conn
|> put_status(:internal_server_error)
|> put_flash(:warn, "Something weird happened.")
|> render("setup-otp.html", %{error: cs.errors})
|> put_flash(:error, "There was a problem updating your profile.")
|> put_status(422)
|> render("view.html", state: :profile)
end
end

def complete(
conn,
%{"page" => "profile", "nickname" => _nickname, "email" => _email} = params
) do
insert_properties = fn ->
for property <- ~w(name nickname email note) do
case Profile.set(property, params[property]) do
{:error, error} ->
Repo.rollback({:setting_invalid, property, error})

_ ->
:ok
end
end
end

case Repo.transaction(insert_properties) do
{:ok, _} ->
conn
|> put_flash(
:success,
"You've got your profile all set up. Congrats #{Koype.Profile.nickname()}!"
)
|> redirect(to: "/setup")

def handle(conn, %{"state" => "auth"} = params) do
with(:ok <- do_create_otp_secret(params["code"], get_session(conn, :otp_secret))) do
do_render_next(conn)
else
{:error, error} ->
conn
|> put_status(:unprocessable_entity)
|> put_flash(:error, "Failed to set a value for your profile.")
|> render("setup-profile.html", %{error: error})
|> do_attach_otp_secret
|> put_flash(:error, do_humanize_auth_error(error))
|> put_status(422)
|> render("view.html", state: :auth)
end
end

def complete(conn, _params) do
conn
|> put_flash(:error, "Invalid operation.")
|> redirect(to: "/setup")
end
end

+ 3
- 7
web/router.ex View File

@@ -8,7 +8,6 @@ defmodule Koype.Web.Router do
plug(:put_secure_browser_headers)
plug(:protect_from_forgery)
plug(Koype.Web.Plug.Guardian.Owner)
plug(Plug.Ribbon, [:dev, :test])
end

pipeline :client do
@@ -34,17 +33,12 @@ defmodule Koype.Web.Router do
scope "/", Koype.Web do
pipe_through([:browser, :owner, :prohibit_owner_auth])
get("/setup", SetupController, :view)
put("/setup", SetupController, :handle)

get("/auth", AuthController, :new)
post("/auth", AuthController, :submit)
end

scope "/api", Koype.Web do
pipe_through([:client, :owner, :prohibit_owner_auth])
get("/setup/check", SetupController, :check)
put("/setup/:state", SetupController, :handle)
end

scope "/", Koype.Web do
pipe_through([:browser, :owner])

@@ -68,6 +62,8 @@ defmodule Koype.Web.Router do

get("/~/settings/", SettingsController, :view)
put("/~/settings/", SettingsController, :apply)

get("/~/rel-me", RelMeController, :view)
end

scope "/indie", Koype.Web, as: :indie do


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

@@ -1,4 +1,8 @@
@import "tachyons-custom/src/tachyons.css";
:root {
--font-family-sans: "Source Sans Pro";
--font-family-serif: "Source Serif Pro";
--font-family-mono: "Source Code Pro";
}

.e-content {
p {
@@ -16,8 +20,8 @@
}

pre {
background-color: black;
color: green;
background-color: var(--black);
color: var(--green);
padding: 1.5rem 0rem;
line-height: 1;
}
@@ -26,21 +30,16 @@
html.wf-active {
h1, h2, h3, h4, h5, h6, p
blockquote, .sans-serif {
font-family: "Source Sans Pro", sans-serif;
font-family: var(--font-family-sans), sans-serif;
}

input, button, form,
article, aside,
main, footer, header,
.serif {
font-family: "Source Serif Pro", serif;
main, footer, header, .serif {
font-family: var(--font-family-serif), serif;
}

pre, code, kbd,
.mono {
font-family: "Source Mono Pro", monospace;
pre, code, kbd, .code {
font-family: var(--font-family-mono), monospace;
}
}

// @import "leaflet/dist/leaflet.css";
@import "ladda/css/ladda";

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

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

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

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

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

@@ -4,7 +4,7 @@ import swal from 'sweetalert';
import timeago from 'timeago.js';
import WebFont from 'webfontloader';

import { Socket } from 'phoenix';
// import { Socket } from 'phoenix';
import 'phoenix_html';

function buildHiddenInput(name, value) {
@@ -59,3 +59,6 @@ window.addEventListener('load', () => {
feather.replace();
Ladda.bind('button[type=submit]');
}, false);

window.Ladda = Ladda;
window.swal = swal;

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

@@ -1,47 +0,0 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";

import { extractCSRFToken, request } from "./request";

function addBaseHref(): void {
const baseElem = document.createElement("base");
const headElem = document.querySelector("head");

headElem.appendChild(baseElem);
baseElem.setAttribute("href", "https://example.koype");
}

function addMockToken(token: string = "fakeToken"): void {
const metaCsrfToken = document.createElement("meta");
const headElem = document.querySelector("head");

headElem.appendChild(metaCsrfToken);
metaCsrfToken.setAttribute("name", "_csrf_token");
metaCsrfToken.setAttribute("value", token);
}

test("extracts current CSRF token from the page", () => {
addMockToken();

const obtainedToken = extractCSRFToken();
expect(obtainedToken).toBe("fakeToken");
});

test("sends request", async () => {
addMockToken();
addBaseHref();

const route = "__test__";
const mockedResponse = "testing";
const mock = new MockAdapter(axios);

mock.onGet(route).reply(200, mockedResponse, {
"x-csrf-token": "crazy"
});

const response = await request("GET", route);
const data = response.data;

expect(data).toBe(mockedResponse);
expect(extractCSRFToken()).toBe("crazy");
});

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

@@ -17,7 +17,7 @@ function http() {

instance.interceptors.response.use(
response => {
const token = response.headers["x-csrf-token"];
const token: string = response.headers["x-csrf-token"];
setTokenInMeta(token);
return response;
},


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

@@ -0,0 +1,151 @@
import Quill from "quill";
import { slugify, snakeCase } from "voca";
const qrcode = require('qrcode-generator');

const placeHolderText = `Koype is a self-hostable IndieWeb engine for everyone.`;

function usernameify(str): string {
return snakeCase(str);
}

function preferNickname(): boolean {
const preferredElem: HTMLInputElement = document.querySelector(
"input[name=prefer_nickname]"
);
return preferredElem.checked;
}

function configureNoteEditor(): void {
const editorElem = document.querySelector("input[name=note]");
const notePreview = document.querySelector("#note");
const quillOptions: object = {
modules: {
toolbar: {
container: [["bold", "italic", "underline", "strike"]]
}
},
placeholder: "Tell me about yourself.",
readOnly: false,
theme: "bubble"
};
const editor: Quill = new Quill("#noteEditor", quillOptions);

editor.on("text-change", (delta, oldContents, source) => {
const editorText: string = editor.getText();

editorElem.setAttribute("value", editorText);

if (editorText.length > 0) {
notePreview.innerHTML = editorText;
} else {
notePreview.textContent = placeHolderText;
}
});
}

function configureNameFields(): void {
const nameElem: HTMLInputElement = document.querySelector("input[name=name]");
const nicknameElem: HTMLInputElement = document.querySelector(
"input[name=nickname]"
);
const preferredElem: HTMLHeadingElement = document.querySelector(
"#preferredName"
);

nameElem.addEventListener("keyup", e => {
if (!preferNickname()) {
preferredElem.textContent = nameElem.value;
nicknameElem.value = usernameify(nameElem.value);
}

if (nicknameElem.value === "") {
nicknameElem.value = usernameify(nameElem.value);
}

updateDid();
});

nicknameElem.addEventListener("keyup", e => {
nicknameElem.value = usernameify(nicknameElem.value);

if (preferNickname()) {
preferredElem.textContent = nicknameElem.value;
}

updateDid();
});
}

function updateDid(): void {
const didElem: HTMLSpanElement = document.querySelector("#did");
const nicknameElem: HTMLInputElement = document.querySelector(
"input[name=nickname]"
);

didElem.textContent = nicknameElem.value;
}

function configureNamePreferenceToggle(): void {
const preferredChoiceElem: HTMLInputElement = document.querySelector(
"input[name=prefer_nickname]"
);

preferredChoiceElem.addEventListener("change", () => {
if (!preferredChoiceElem.checked) {
const nameElem: HTMLInputElement = document.querySelector(
"input[name=name]"
);
const nicknameElem: HTMLInputElement = document.querySelector(
"input[name=nickname]"
);

nicknameElem.value = usernameify(nameElem.value);
}

updateDid();
});
}

function renderTotpQrCode(
imagePath: string = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>'
) {
const qrContainer: HTMLElement = document.querySelector("#qrCode");
const qrTotpUriElem: HTMLInputElement = document.querySelector("#totpUri");
const value: string = qrTotpUriElem.value;
const qr = qrcode(0, 'H');
qr.addData(value);
console.log(qr.make());
const dataUri = "url(" + qr.createDataURL(5, 4) + ")";
qrContainer.style.setProperty('background-image', dataUri);
qrContainer.style.setProperty('background-size', 'cover');
qrContainer.style.setProperty('background-repeat', 'none');
}

function configureAvatarRendering(): void {
const avatarInputElem: HTMLInputElement = document.querySelector(
"input[name=photo]"
);
const avatarRenderer: HTMLImageElement = document.querySelector(
"img#profileImage"
);

avatarInputElem.addEventListener("change", () => {
const fileReader = new FileReader();
fileReader.readAsDataURL(avatarInputElem.files[0]);
fileReader.addEventListener("load", () => {
const result = fileReader.result as string;
avatarRenderer.src = result;
});
});
}

window.addEventListener("load", function startSetupPage() {
if (document.querySelector("#regionId")) {
configureNameFields();
configureNamePreferenceToggle();
configureNoteEditor();
configureAvatarRendering();
} else if (document.querySelector("#regionAuth")) {
renderTotpQrCode();
}
});

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

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

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

@@ -1,12 +1,12 @@
<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">
<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">
<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">
<li class="lh-copy">
<%= if !is_nil(@entry[:published]) do %>
published
<time class="fw5 dt-published" title="<%= @entry[:published] %>" datetime="<%= @entry[:published] %>">
<%= get_time_ago_of_rfc3339(@entry[:published]) %>
</time>
<%= else %>
<% else %>
created
<time class="fw5 dt-published" title="<%= @model.inserted_at %>" datetime="<%= @model.inserted_at %>">
<%= time_ago_in_words(@model.inserted_at) %>
@@ -35,7 +35,7 @@
<li class="lh-copy mt1">
<span class="v-mid">by</span>
<a class="ml1 u-author h-card v-mid link fw5 navy" rel="me" href="/">
<img class="v-mid u-photo br-100 h1 w1 b--near-black" src="https://api.adorable.io/avatars/128/<%= Koype.Profile.email %>.png" />
<img class="v-mid u-photo br-100 h1 w1 b--near-black" src="<%= Koype.Profile.photo %>" />
<span class="v-mid"><%= Koype.Profile.name %></span>
</a>
</li>


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

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

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

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

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

@@ -3,14 +3,16 @@
<%= if Koype.Profile.complete? do %>
<div class="h-card pa2 items-center flex flex-column flex-row-l flex-auto flex-grow items-start">
<a href="<%= Koype.host %>" rel="author me authn" class="u-url db pa2 link lightest-blue">
<img alt="<%= Koype.Profile.displayed_name %>"
class="center ba bw2 b--lightest-blue bg--near-white br-100 w-auto h3-l u-photo db-l"
src="https://api.adorable.io/avatars/128/<%= Koype.Profile.email %>.png" />
<%= if Koype.Profile.photo do %>
<img alt="<%= Koype.Profile.displayed_name %>"
class="center ba bw1 b--lightest-blue bg--near-white br-100 w-auto h4 u-photo db"
src="<%= Koype.Profile.photo %>" />
<% end %>
<span class="p-name tc db fw7 mv2 f6"><%= Koype.Profile.name %></span>
</a>
<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>
<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>
</div>
<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">
<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">
<li class="lh-copy ttu fw7 w-100 tc">
<a class="link lightest-blue" href="<% entry_path(@conn, :stream) %>">
<i class="near-white h1 w1 pa1 v-mid" data-feather="activity"></i>
@@ -19,8 +21,8 @@
</li>
<%= for %{type: type, names: name} <- IndieWeb.Post.known_types do %>
<li class="lh-copy mv1 w-auto tc">
<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) %>">
<i class="h1 w1 ma1 v-mid" data-feather="<%= Koype.Web.EntryView.icon_for_type(type) %>"></i>
<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) %>">
<i role="none" class="h1 w1 mb1 mr1 v-mid" data-feather="<%= Koype.Web.EntryView.icon_for_type(type) %>"></i>
</a>
</li>
<% end %>
@@ -47,7 +49,7 @@
</li>
<% end %>
</ul>
<%= else %>
<% else %>
<p class="lh-copy f7 tc v-mid">
Koype
<code>v<%= Koype.version %></code>


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

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

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

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

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

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

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

@@ -0,0 +1,39 @@
<input id="totpUri" type="hidden" value="<%= totp_uri(@conn) %>" autocomplete="off">
<section id="regionAuth" class="w-100 mw8 mv3 center pa2 pb3 bt b--moon-gray flex flex-row flex-wrap justify-center items-center">
<h2 class="lh-title tracked tc f2 order-1 flex-auto flex-grow w-100">Authentication</h2>
<div class="flex-grow order-2 w-100 flex-auto ph1">
<p class="f5 lh-copy measure center tj">
Your site, your rules! But in order to play by those rules; Koype needs to confirm that you
are who you say you are. Koype handles authorization via TOTP. Scanning the QR code with a
<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
enter below.
</p>
</div>
<noscript>
<p class="flex items-center justify-center pa4 bg-lightest-blue navy">
This portion of the page requires <a href="https://js.org/">JavaScript</a> in order
to render the QR code information client side.
</p>
</noscript>
<div class="order-3 self-center pa2">
<div class="measure">
<label class="f6 b db mb2 sans" for="email">Generated Code</label>
<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" />
<small class="db mv1 f6 gray">The code generated by your application.</small>
</div>
<details class="measure mt3">
<summary class="sans">Technical Info</summary>
<label class="f6 b db mb2 sans" for="totpSecret">Secret</label>
<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) %>" />
<small class="db mv1 f6 gray">The secret to use with your OTP application.</small>
</details>
</div>
<div class="order-3">
<div id="qrCode" class="grow order-1 flex items-center justify-center h5 w5 center"></div>
<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) %>">
<i class="v-mid w1 h1" data-feather="gift"></i>
Auto Configure Your App
</a>
<small class="order-2 center db gray mb2 i w-100">Scan the code above to get your one time passcode.</small>
</div>
</section>

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

@@ -1,48 +0,0 @@
<section class="mw7 center pa2">
<h1 class="f1 lh-title serif">Securing Your Site</h1>
<p class="lh-copy f4 measure sans-serif">
In order to confirm it's you making actions on your site, we'll need you
to set up an <a class="link blue" href="https://en.wikipedia.org/wiki/Software_token">authenticator app</a>
that supports the generation of
<a class="link blue" href="https://indieweb.org/Time-based_One-time_Password_Algorithm">one time passwords</a>.
</p>
<form method="post" class="flex flex-column flex-row flex-wrap justify-around items-start">
<input type=hidden name=_csrf_token value="<%= Phoenix.Controller.get_csrf_token %>" />
<script>
window.addEventListener('load', function () {
var qrTotpUri = "<%= totp_uri(@conn) %>"
var qrCode = qrcode(0, 'H');
qrCode.addData(qrTotpUri);
qrCode.make();

var qrHtml = qrCode.createSvgTag(3, 2);
document.querySelector('#otpSecretWell').innerHTML = qrHtml;
});
</script>
<div id="otpSecretWell" class="db w-100 h-auto order-1 w-third-l tc v-mid bg-near-white grow">
</div>
<div class="order-2 flex-grow">
<div class="mv3">
<label class="db ttu fw4 lh-copy f5" for="email">Generated Code</label>
<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" />
<small class="db mv1 f6 gray">The code generated by your application.</small>
</div>
<details class="mv3">
<summary>Technical Info</summary>
<label class="db ttu fw4 lh-copy f5" for="email">Secret</label>
<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] %>" />
<small class="db mv1 f6 gray">The secret to use with your OTP application.</small>
</details>
</div>
<div class="w-100 order-3 mv3 flex flex-row-l flex-column items-center justify-around">
<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) %>">
<i class="v-mid h1 w1 mr2" data-feather="gift"></i>
<span class="v-mid fw5">Automatic Setup</span>
</a>
<button class="pointer mv2 v-mid ba w-100 w-third-l b--black pa3 bg-near-black near-white sans-serif" type="submit">
<i class="v-mid h1 w1 mr2" data-feather="unlock"></i>
<span class="v-mid fw5">Confirm Code</span>
</button>
</div>
</form>
</section>

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

@@ -1,44 +1,81 @@
<section class="mw7 w-100 pa2 center">
<h1 class="tc f1 lh-title serif">Welcome</h1>
<p class="lh-copy f3 measure sans-serif">
Hey there! So glad you've decided to begin your journey into the
<a class="link blue" href="https://indieweb.org">IndieWeb</a>.
We'll need some information about you before we can continue.
</p>
<p class="lh-copy f4 measure gray sans-serif">
Some fields below are required to be a good <a class="link blue" href="http://archive.is/Mc929" target="_blank">netizen</a>.
<br />
Learn more <a class="link blue" href="#" target="_blank">about why Koype uses this information</a>.
</p>
<form method="post">
<input type=hidden name=_csrf_token value="<%= Phoenix.Controller.get_csrf_token %>" />

<div class="mv3">
<label class="db ttu fw4 lh-copy f5" for="email">Email Address</label>
<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 %>" />
<small class="db mv1 f6 gray">The address that you'd be using to receive messaging.</small>
</div>
<div class="mv3">
<label class="db ttu fw4 lh-copy f5" for="name">Full Name</label>
<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" />
<small class="db mv1 f6 gray">The name you'd want people on the Web to refer to you as.</small>
</div>
<div class="mv3">
<label class="db ttu fw4 lh-copy f5" for="nickname">Nickname / Alias</label>
<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" />
<small class="db mv1 f6 gray">The name you'd want to use as opposed to your given/chosen name.</small>
</div>
<div class="mv3">
<label class="db ttu fw4 lh-copy f5" for="note">Short Description</label>
<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.">
</textarea>
<small class="db mv1 f6 gray">A super-brief description of yourself.</small>
</div>
<div class="w-100 mv3 flex flex-row-l flex-column items-start justify-between">
<button class="pointer v-mid ba w-100 w-third-l b--black pa3 bg-near-black near-white sans-serif" type="submit">
<i class="v-mid h1 w1 mr2" data-feather="save"></i>
<span class="v-mid">Save</span>
</button>
</div>
</form>
<section id="regionId" class="w-100 mw8 mv3 center pa2 pb3 bt b--moon-gray flex flex-row flex-wrap">
<h2 class="lh-title tracked tc f2 flex-auto flex-grow w-100">Identification</h2>
<div class="flex-grow flex-auto ph1">
<p class="f5 lh-copy measure center tj">
Koype would like to know a bit about you. This information will
help build your <a target="_new" href="https://indieweb.org/h-card" class="link navy fw7"><code>h-card</code></a>.
This controls how you're seen to other websites and services on the Web.
<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>.
</p>
<fieldset class="measure b--moon-gray b--solid center">
<legend class="center f4 ttu sans-serif pa2 ba bw1 b--moon-gray">Naming</legend>
<div class="measure mt3">
<label id="email-label" for="email" class="sans f6 b db mb2">Email Address<span class="normal gray">(optional)</span></label>
<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">
<small id="email-desc" class="sans f6 gray db mb2">An address at which you'd want your website to be referenced by.</small>
<div class="measure">
<label for="name" class="sans f6 b db mb2">Name <span class="normal gray">(optional)</span></label>
<input name="name" autocomplete="name" class="input-reset ba b--moon-gray pa2 mb2 db w-100" type="text" aria-describedby="name-desc">
<small id="name-desc" class="sans f6 gray db mb2">Your first, middle and last name.</small>
</div>
<div class="measure mt3">
<label for="nickname" class="sans f6 b db mb2">Nickname <span class="normal gray">(optional)</span></label>
<input name="nickname" autocomplete="nickname" class="input-reset ba b--moon-gray pa2 mb2 db w-100" type="text" aria-describedby="nickname-desc">
<small id="nickname-desc" class="sans f6 gray db mb2">A shorter, alternative name you'd like to go by.</small>
</div>
<div class="measure mt3">
<div class="pretty p-default p-switch">
<input checked type="checkbox" name="prefer_nickname">
<div class="state p-primary">
<label class="f5">Use my nickname when showing my name.</label>
</div>
</div>
</div>
<p class="f5 tc mt3 bg-light-blue dark-gray b--blue bw1 ba pa1 lh-copy">
<i class="w1 h1 v-mid" data-feather="info"></i>
You don't have to fill in both! Only one value is needed.
</p>
</fieldset>
<fieldset class="measure b--moon-gray b--solid mt3 center">
<legend class="center f4 ttu sans-serif pa2 ba bw1 b--moon-gray">Presentation</legend>
<div class="measure">
<label for="photo" class="f6 b db mb2 sans">Avatar <span class="normal gray">(optional)</span></label>
<input accept="image/*" name="photo" class="input-reset bn mb2 db w-auto" type="file" aria-describedby="photo-desc">
<small id="photo-desc" class="f6 gray db mb2 sans">The image that'll be used to present you.</small>
</div>
<div class="measure mt3">
<label for="note" class="sans f6 b db mb2">Note <span class="normal gray">(optional)</span></label>
<div id="noteEditor" class="w-100 bg-white pa1 mb2 b--moon-gray ba h4">
<div id="noteToolbar"></div>
</div>
<textarea name="note" class="dn border-box hover-black w-100 measure ba b--moon-gray pa2 br2"></textarea>
<small id="note-desc" class="sans f6 gray db mv2">
A short text about yourself. <strong>Rich</strong> <u>text</u> is <em>supported</em>.
Just select some text and a toolbar will appear.
</small>
</div>
</fieldset>
</div>
<div class="center self-start pa2">
<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>
<article class="mw5 bg-white br3 ph2 pv3 mv3 ba b--black-10 center">
<div class="tc">
<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">
<h4 id="preferredName" class="f4 mb2">Mimi Whitehouse</h4>
<h6 class="ma0 lh-solid gray code f6">@<span id="did">mimi</span>@<%= URI.parse(Koype.host).host %></h6>
<hr class="mw3 bb bw1 b--black-10">
</div>
<p id="note" class="lh-copy measure center f6 black-70 tj mb1">
Quite affectionate and outgoing.
She loves to get chin scratches and will
roll around on the floor waiting for you give her more of them.
</p>
<hr class="mw3 bb bw1 b--black-10">
<p class="lh-copy measure center tc f6 black-50 sans-serif flex flex-row flex-wrap justify-around">
<a href="#" class="link dim navy fw7"><i class="w1 h1 v-mid" data-feather="file-text"></i>&nbsp;About</a>
<a href="#" class="link dim navy fw7"><i class="w1 h1 v-mid" data-feather="rss"></i>&nbsp;Follow</a>
<a href="#" class="link dim navy fw7"><i class="w1 h1 v-mid" data-feather="at-sign"></i>&nbsp;Contact</a>
</p>
</article>
</div>
</section>

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

@@ -0,0 +1,25 @@
<section class="dn w-100 mw8 mv3 center pa2 pb3 justify-center items-center flex flex-column">
<h1 class="lh-title f1 tracked-tight">Setting Up</h1>
<p class="f4 lh-copy measure tj">
Hey there! So glad you've chosen Koype as your platform for
the <a target="_new" class="link navy fw7" href="https://indieweb.org/why">independent Web</a>.
This page aims to set up some important bits of information to faciliate
your site's compability with other services.
</p>
</section>
<form enctype="<%= form_enctype(assigns[:state]) %>" accept-charset="utf-8" autocomplete="on" id="setupForm" class="w-100" method="post" action="<%= setup_path(@conn, :handle) %>">
<%= render @view_module, "setup-#{assigns[:state]}.html", assigns %>
<footer class="flex flex-row flex-wrap justify-end center w-100 mw7 pv3">
<p id="errorMessage" class="w-100 w-auto-l flex-auto flex-grow dn"></p>
<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">
<span class="ladda-label">
<i class="v-mid h1 w1 mr2" data-feather="save">Save</i>
<span class="v-mid">Continue</span>
</span>
</button>
</footer>
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="_csrf_token" value="<%= Phoenix.Controller.get_csrf_token %>" />
<input type="hidden" name="state" value="<%= assigns[:state] %>" />
</form>
<script async defer src="<%= static_path(@conn, "/assets/js/setup.js") %>"></script>

+ 17
- 16
web/views/entry_view.ex