diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be872e132..8b0285253 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,50 @@ defaults:
shell: bash
jobs:
+ static-analysis:
+ runs-on: ubuntu-18.04
+ env:
+ MIX_ENV: dev
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: erlef/setup-beam@v1
+ with:
+ otp-version: '24.1'
+ elixir-version: '1.12.3'
+ - uses: actions/cache@v2
+ with:
+ path: |
+ deps
+ _build
+ key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-mix-
+ - name: Install Dependencies
+ run: mix deps.get --only dev
+ # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
+ # Cache key based on Elixir & Erlang version (also usefull when running in matrix)
+ - name: Restore PLT cache
+ uses: actions/cache@v2
+ id: plt_cache
+ with:
+ key: |
+ ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
+ restore-keys: |
+ ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
+ path: |
+ priv/plts
+ # Create PLTs if no cache was found
+ - name: Create PLTs
+ if: steps.plt_cache.outputs.cache-hit != 'true'
+ run: mix dialyzer --plt
+ - name: Run format check
+ run: mix format --check-formatted
+ - name: Run linter
+ run: mix credo --strict
+ - name: Run dialyzer
+ run: mix dialyzer --format dialyxir
+
unit-test:
runs-on: ubuntu-18.04
env:
diff --git a/.gitignore b/.gitignore
index 8891002e4..51d20e54e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,9 @@ npm-debug.log
# this depending on your deployment strategy.
/priv/static/
+# Dialyxir output
+/priv/plts/
+
# ElixirLS generates an .elixir_ls folder for user settings
.elixir_ls
@@ -51,7 +54,6 @@ npm-debug.log
/*.deb
/*.rpm
-pkg/debian/opt
# Test screenshots
apps/fz_http/screenshots
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9eb9a79ec..32e6ea832 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -13,6 +13,12 @@ repos:
language: system
pass_filenames: false
files: \.exs*$
+ - id: mix-analysis
+ name: 'elixir: mix dialyzer'
+ entry: mix dialyzer --format dialyxir
+ language: system
+ pass_filenames: false
+ files: \.exs*$
- id: mix-compile
name: 'elixir: mix compile'
entry: mix compile --force --warnings-as-errors
diff --git a/.tool-versions b/.tool-versions
index 72e5aebae..217f2f179 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,4 +1,5 @@
-# These should match the versions used in the build
+# These are used for the dev environment.
+# This should match the versions used in the built product.
nodejs 14.18.1
elixir 1.12.3-otp-24
erlang 24.1.4
diff --git a/apps/fz_common/lib/fz_integer.ex b/apps/fz_common/lib/fz_integer.ex
new file mode 100644
index 000000000..cb90015df
--- /dev/null
+++ b/apps/fz_common/lib/fz_integer.ex
@@ -0,0 +1,9 @@
+defmodule FzCommon.FzInteger do
+ @moduledoc """
+ Utility functions for working with Integers.
+ """
+
+ def clamp(num, min, _max) when is_integer(num) and num < min, do: min
+ def clamp(num, _min, max) when is_integer(num) and num > max, do: max
+ def clamp(num, _min, _max) when is_integer(num), do: num
+end
diff --git a/apps/fz_common/lib/fz_string.ex b/apps/fz_common/lib/fz_string.ex
new file mode 100644
index 000000000..73b91b712
--- /dev/null
+++ b/apps/fz_common/lib/fz_string.ex
@@ -0,0 +1,21 @@
+defmodule FzCommon.FzString do
+ @moduledoc """
+ Utility functions for working with Strings.
+ """
+
+ def to_boolean(str) when is_binary(str) do
+ as_bool(String.downcase(str))
+ end
+
+ defp as_bool("true") do
+ true
+ end
+
+ defp as_bool("false") do
+ false
+ end
+
+ defp as_bool(unknown) do
+ raise "Unknown boolean: string #{unknown} not one of ['true', 'false']."
+ end
+end
diff --git a/apps/fz_common/test/fz_crypto_test.exs b/apps/fz_common/test/fz_crypto_test.exs
index f8ac19a3a..7e7943a18 100644
--- a/apps/fz_common/test/fz_crypto_test.exs
+++ b/apps/fz_common/test/fz_crypto_test.exs
@@ -3,7 +3,7 @@ defmodule FzCommon.FzCryptoTest do
alias FzCommon.FzCrypto
- describe "rand_string" do
+ describe "rand_string/1" do
test "it returns a string of default length" do
assert 16 == String.length(FzCrypto.rand_string())
end
@@ -14,4 +14,27 @@ defmodule FzCommon.FzCryptoTest do
end
end
end
+
+ describe "rand_token/1" do
+ test "returns a token of default length" do
+ # 8 bytes is 12 chars in Base64
+ assert 12 == String.length(FzCrypto.rand_token())
+ end
+
+ test "returns a token of length 4 when bytes is 1" do
+ assert 4 == String.length(FzCrypto.rand_token(1))
+ end
+
+ test "returns a token of length 4 when bytes is 3" do
+ assert 4 == String.length(FzCrypto.rand_token(3))
+ end
+
+ test "returns a token of length 40_000 when bytes is 32_768" do
+ assert 44 == String.length(FzCrypto.rand_token(32))
+ end
+
+ test "returns a token of length 44 when bytes is 32" do
+ assert 43_692 == String.length(FzCrypto.rand_token(32_768))
+ end
+ end
end
diff --git a/apps/fz_common/test/fz_integer_text.exs b/apps/fz_common/test/fz_integer_text.exs
new file mode 100644
index 000000000..fd09cde77
--- /dev/null
+++ b/apps/fz_common/test/fz_integer_text.exs
@@ -0,0 +1,31 @@
+defmodule FzCommon.FzIntegerTest do
+ use ExUnit.Case, async: true
+
+ alias FzCommon.FzInteger
+
+ describe "clamp/3" do
+ test "clamps to min" do
+ min = 1
+ max = 5
+ num = 0
+
+ assert 1 == FzInteger.clamp(num, min, max)
+ end
+
+ test "clamps to max" do
+ min = 1
+ max = 5
+ num = 7
+
+ assert 5 == FzInteger.clamp(num, min, max)
+ end
+
+ test "returns num if in range" do
+ min = 1
+ max = 5
+ num = 3
+
+ assert 3 == FzInteger.clamp(num, min, max)
+ end
+ end
+end
diff --git a/apps/fz_common/test/fz_map_test.exs b/apps/fz_common/test/fz_map_test.exs
index ac7a7868e..911809966 100644
--- a/apps/fz_common/test/fz_map_test.exs
+++ b/apps/fz_common/test/fz_map_test.exs
@@ -18,4 +18,18 @@ defmodule FzCommon.FzMapTest do
assert FzMap.compact(@data, "") == %{foo: "bar"}
end
end
+
+ describe "stringify_keys/1" do
+ @data %{foo: "bar", bar: "", map: %{foo: "bar"}}
+
+ test "stringifies the keys" do
+ assert FzMap.stringify_keys(@data) == %{
+ "foo" => "bar",
+ "bar" => "",
+ "map" => %{
+ foo: "bar"
+ }
+ }
+ end
+ end
end
diff --git a/apps/fz_common/test/fz_string_test.exs b/apps/fz_common/test/fz_string_test.exs
new file mode 100644
index 000000000..1ffed172b
--- /dev/null
+++ b/apps/fz_common/test/fz_string_test.exs
@@ -0,0 +1,23 @@
+defmodule FzCommon.FzStringTest do
+ use ExUnit.Case, async: true
+
+ alias FzCommon.FzString
+
+ describe "to_boolean/1" do
+ test "converts to true" do
+ assert true == FzString.to_boolean("True")
+ end
+
+ test "converts to false" do
+ assert false == FzString.to_boolean("False")
+ end
+
+ test "raises exception on unknowns" do
+ message = "Unknown boolean: string foobar not one of ['true', 'false']."
+
+ assert_raise RuntimeError, message, fn ->
+ FzString.to_boolean("foobar")
+ end
+ end
+ end
+end
diff --git a/apps/fz_http/.gitignore b/apps/fz_http/.gitignore
index 9f6a27061..c7032b99f 100644
--- a/apps/fz_http/.gitignore
+++ b/apps/fz_http/.gitignore
@@ -30,6 +30,8 @@ npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
+/assets/lib/node_modules/
+/assets/bin/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
diff --git a/apps/fz_http/assets/css/app.scss b/apps/fz_http/assets/css/app.scss
index 3336ef710..0727fc16b 100644
--- a/apps/fz_http/assets/css/app.scss
+++ b/apps/fz_http/assets/css/app.scss
@@ -6,8 +6,16 @@
@import "./email.scss";
@import "./tables.scss";
+/* Font Awesome */
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "~@fortawesome/fontawesome-free/scss/solid.scss";
@import "~@fortawesome/fontawesome-free/scss/regular.scss";
@import "~@fortawesome/fontawesome-free/scss/brands.scss";
+
+/* Material Design Icons */
+$mdi-font-path: "~@mdi/font/fonts";
+@import "~@mdi/font/scss/materialdesignicons.scss";
+
+/* Bulma Tooltip */
+@import "~@creativebulma/bulma-tooltip/src/sass/index.sass";
diff --git a/apps/fz_http/assets/js/app.js b/apps/fz_http/assets/js/app.js
index 22331c444..38efbeaad 100644
--- a/apps/fz_http/assets/js/app.js
+++ b/apps/fz_http/assets/js/app.js
@@ -3,6 +3,8 @@
// its own CSS file.
import css from "../css/app.scss"
+import "@fontsource/fira-sans"
+
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
diff --git a/apps/fz_http/assets/package-lock.json b/apps/fz_http/assets/package-lock.json
index bd53d11f6..9c121a66b 100644
--- a/apps/fz_http/assets/package-lock.json
+++ b/apps/fz_http/assets/package-lock.json
@@ -16,12 +16,15 @@
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
"phoenix_live_view": "file:../../../deps/phoenix_live_view",
- "qrcode": "^1.4.4"
+ "qrcode": "^1.3.3"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
+ "@creativebulma/bulma-tooltip": "^1.2.0",
+ "@fontsource/fira-sans": "^4.5.0",
"@fortawesome/fontawesome-free": "^5.15.3",
+ "@mdi/font": "^6.5.95",
"admin-one-bulma-dashboard": "file:local_modules/admin-one-bulma-dashboard",
"autoprefixer": "^9.8.8",
"babel-loader": "^8.2.3",
@@ -1625,6 +1628,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@creativebulma/bulma-tooltip": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@creativebulma/bulma-tooltip/-/bulma-tooltip-1.2.0.tgz",
+ "integrity": "sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==",
+ "dev": true
+ },
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz",
@@ -1634,6 +1643,12 @@
"node": ">=10.0.0"
}
},
+ "node_modules/@fontsource/fira-sans": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-4.5.0.tgz",
+ "integrity": "sha512-qFYIYZgerQ0iC+j6HjhUuKDOYOG3UFV8QIARdkhvZsNDpA6PTHiY8zGr5e3BjwzFFJBJOrnisxSvtbrUb82KXw==",
+ "dev": true
+ },
"node_modules/@fortawesome/fontawesome-free": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
@@ -1644,6 +1659,12 @@
"node": ">=6"
}
},
+ "node_modules/@mdi/font": {
+ "version": "6.5.95",
+ "resolved": "https://registry.npmjs.org/@mdi/font/-/font-6.5.95.tgz",
+ "integrity": "sha512-ES5rj6J39FUkHe/b3C9SJs8bqIungYhuU7rBINTBaHOv/Ce4RCb3Gw08CZVl32W33UEkgRkzyWaIedV4at+QHg==",
+ "dev": true
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2007,6 +2028,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true,
"engines": {
"node": ">=6"
}
@@ -2015,6 +2037,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -2371,25 +2394,6 @@
"ieee754": "^1.2.1"
}
},
- "node_modules/buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "dependencies": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
- }
- },
- "node_modules/buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
- },
- "node_modules/buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
- },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2517,6 +2521,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/can-promise": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/can-promise/-/can-promise-0.0.1.tgz",
+ "integrity": "sha512-gzVrHyyrvgt0YpDm7pn04MQt8gjh0ZAhN4ZDyCRtGl6YnuuK6b4aiUTD7G52r9l4YNmxfTtEscb92vxtAlL6XQ==",
+ "dependencies": {
+ "window-or-global": "^1.0.1"
+ }
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -2602,6 +2614,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
"dependencies": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
@@ -2626,7 +2639,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2635,6 +2647,7 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@@ -2642,7 +2655,8 @@
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
},
"node_modules/colord": {
"version": "2.9.1",
@@ -3302,7 +3316,8 @@
"node_modules/emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
},
"node_modules/emojis-list": {
"version": "3.0.0",
@@ -3313,6 +3328,14 @@
"node": ">= 4"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
@@ -3786,6 +3809,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
@@ -4193,6 +4217,14 @@
"node": ">= 0.10"
}
},
+ "node_modules/invert-kv": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/is-absolute-url": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
@@ -4332,8 +4364,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"node_modules/isobject": {
"version": "3.0.1",
@@ -4509,6 +4540,17 @@
"node": ">= 8"
}
},
+ "node_modules/lcid": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+ "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+ "dependencies": {
+ "invert-kv": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/lilconfig": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz",
@@ -4622,6 +4664,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/map-age-cleaner": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+ "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+ "dependencies": {
+ "p-defer": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/map-obj": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
@@ -4640,6 +4693,19 @@
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true
},
+ "node_modules/mem": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+ "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
+ "dependencies": {
+ "map-age-cleaner": "^0.1.1",
+ "mimic-fn": "^2.0.0",
+ "p-is-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/meow": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@@ -4728,7 +4794,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -4901,6 +4966,11 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
+ },
"node_modules/node-gyp": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz",
@@ -5152,7 +5222,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5225,6 +5294,151 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/os-locale": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+ "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+ "dependencies": {
+ "execa": "^1.0.0",
+ "lcid": "^2.0.0",
+ "mem": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-locale/node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/os-locale/node_modules/execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "dependencies": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-locale/node_modules/get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-locale/node_modules/is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale/node_modules/npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "dependencies": {
+ "path-key": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/os-locale/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/p-defer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -5987,6 +6201,15 @@
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
"dev": true
},
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -5997,17 +6220,15 @@
}
},
"node_modules/qrcode": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
- "integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.3.3.tgz",
+ "integrity": "sha512-SH7V13AcJusH3GT8bMNOGz4w0L+LjcpNOU/NiOgtBhT/5DoWeZE6D5ntMJnJ84AMkoaM4kjJJoHoh9g++8lWFg==",
"dependencies": {
- "buffer": "^5.4.3",
- "buffer-alloc": "^1.2.0",
- "buffer-from": "^1.1.1",
+ "can-promise": "0.0.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
- "yargs": "^13.2.4"
+ "yargs": "^12.0.5"
},
"bin": {
"qrcode": "bin/qrcode"
@@ -6016,27 +6237,194 @@
"node": ">=4"
}
},
- "node_modules/qrcode/node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
+ "node_modules/qrcode/node_modules/ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/qrcode/node_modules/cliui": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+ "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
"dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
+ "string-width": "^2.1.1",
+ "strip-ansi": "^4.0.0",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode/node_modules/get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
+ },
+ "node_modules/qrcode/node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode/node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/qrcode/node_modules/require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
+ },
+ "node_modules/qrcode/node_modules/string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/qrcode/node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "dependencies": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/yargs": {
+ "version": "12.0.5",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+ "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+ "dependencies": {
+ "cliui": "^4.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^3.0.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1 || ^4.0.0",
+ "yargs-parser": "^11.1.1"
+ }
+ },
+ "node_modules/qrcode/node_modules/yargs-parser": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+ "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
}
},
"node_modules/qs": {
@@ -6359,7 +6747,8 @@
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
},
"node_modules/resolve": {
"version": "1.20.0",
@@ -6627,8 +7016,7 @@
"node_modules/signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
- "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==",
- "dev": true
+ "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"node_modules/slash": {
"version": "3.0.0",
@@ -6773,6 +7161,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
"dependencies": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
@@ -6786,6 +7175,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
"dependencies": {
"ansi-regex": "^4.1.0"
},
@@ -6793,6 +7183,14 @@
"node": ">=6"
}
},
+ "node_modules/strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -7455,10 +7853,16 @@
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
"dev": true
},
+ "node_modules/window-or-global": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/window-or-global/-/window-or-global-1.0.1.tgz",
+ "integrity": "sha1-2+RboqKRqrxW1iz2bEW3+jIpRt4="
+ },
"node_modules/wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
@@ -7497,6 +7901,7 @@
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
"dependencies": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
@@ -7514,6 +7919,7 @@
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
@@ -7523,6 +7929,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
"dependencies": {
"locate-path": "^3.0.0"
},
@@ -7534,6 +7941,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
"dependencies": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
@@ -7546,6 +7954,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
"dependencies": {
"p-try": "^2.0.0"
},
@@ -7560,6 +7969,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
"dependencies": {
"p-limit": "^2.0.0"
},
@@ -7571,6 +7981,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true,
"engines": {
"node": ">=4"
}
@@ -8672,18 +9083,36 @@
"to-fast-properties": "^2.0.0"
}
},
+ "@creativebulma/bulma-tooltip": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@creativebulma/bulma-tooltip/-/bulma-tooltip-1.2.0.tgz",
+ "integrity": "sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==",
+ "dev": true
+ },
"@discoveryjs/json-ext": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz",
"integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==",
"dev": true
},
+ "@fontsource/fira-sans": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-4.5.0.tgz",
+ "integrity": "sha512-qFYIYZgerQ0iC+j6HjhUuKDOYOG3UFV8QIARdkhvZsNDpA6PTHiY8zGr5e3BjwzFFJBJOrnisxSvtbrUb82KXw==",
+ "dev": true
+ },
"@fortawesome/fontawesome-free": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==",
"dev": true
},
+ "@mdi/font": {
+ "version": "6.5.95",
+ "resolved": "https://registry.npmjs.org/@mdi/font/-/font-6.5.95.tgz",
+ "integrity": "sha512-ES5rj6J39FUkHe/b3C9SJs8bqIungYhuU7rBINTBaHOv/Ce4RCb3Gw08CZVl32W33UEkgRkzyWaIedV4at+QHg==",
+ "dev": true
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -9012,12 +9441,14 @@
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -9277,25 +9708,6 @@
"ieee754": "^1.2.1"
}
},
- "buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "requires": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
- }
- },
- "buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
- },
- "buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
- },
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -9402,6 +9814,14 @@
"quick-lru": "^4.0.1"
}
},
+ "can-promise": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/can-promise/-/can-promise-0.0.1.tgz",
+ "integrity": "sha512-gzVrHyyrvgt0YpDm7pn04MQt8gjh0ZAhN4ZDyCRtGl6YnuuK6b4aiUTD7G52r9l4YNmxfTtEscb92vxtAlL6XQ==",
+ "requires": {
+ "window-or-global": "^1.0.1"
+ }
+ },
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -9469,6 +9889,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
@@ -9489,13 +9910,13 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
"requires": {
"color-name": "1.1.3"
}
@@ -9503,7 +9924,8 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
},
"colord": {
"version": "2.9.1",
@@ -9981,7 +10403,8 @@
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
},
"emojis-list": {
"version": "3.0.0",
@@ -9989,6 +10412,14 @@
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
},
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
"enhanced-resolve": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
@@ -10343,7 +10774,8 @@
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
},
"get-intrinsic": {
"version": "1.1.1",
@@ -10632,6 +11064,11 @@
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
"dev": true
},
+ "invert-kv": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA=="
+ },
"is-absolute-url": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
@@ -10735,8 +11172,7 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isobject": {
"version": "3.0.1",
@@ -10875,6 +11311,14 @@
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
"dev": true
},
+ "lcid": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+ "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+ "requires": {
+ "invert-kv": "^2.0.0"
+ }
+ },
"lilconfig": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz",
@@ -10966,6 +11410,14 @@
"semver": "^6.0.0"
}
},
+ "map-age-cleaner": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+ "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+ "requires": {
+ "p-defer": "^1.0.0"
+ }
+ },
"map-obj": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
@@ -10978,6 +11430,16 @@
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true
},
+ "mem": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+ "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
+ "requires": {
+ "map-age-cleaner": "^0.1.1",
+ "mimic-fn": "^2.0.0",
+ "p-is-promise": "^2.0.0"
+ }
+ },
"meow": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@@ -11046,8 +11508,7 @@
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"min-indent": {
"version": "1.0.1",
@@ -11169,6 +11630,11 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
+ },
"node-gyp": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz",
@@ -11358,8 +11824,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
- "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
"version": "0.9.0",
@@ -11408,6 +11873,111 @@
"mimic-fn": "^2.1.0"
}
},
+ "os-locale": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+ "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+ "requires": {
+ "execa": "^1.0.0",
+ "lcid": "^2.0.0",
+ "mem": "^4.0.0"
+ },
+ "dependencies": {
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "requires": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
+ "p-defer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww="
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
+ },
+ "p-is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg=="
+ },
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -11918,6 +12488,15 @@
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
"dev": true
},
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -11925,26 +12504,165 @@
"dev": true
},
"qrcode": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
- "integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.3.3.tgz",
+ "integrity": "sha512-SH7V13AcJusH3GT8bMNOGz4w0L+LjcpNOU/NiOgtBhT/5DoWeZE6D5ntMJnJ84AMkoaM4kjJJoHoh9g++8lWFg==",
"requires": {
- "buffer": "^5.4.3",
- "buffer-alloc": "^1.2.0",
- "buffer-from": "^1.1.1",
+ "can-promise": "0.0.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
- "yargs": "^13.2.4"
+ "yargs": "^12.0.5"
},
"dependencies": {
- "buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+ },
+ "cliui": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+ "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
"requires": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
+ "string-width": "^2.1.1",
+ "strip-ansi": "^4.0.0",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+ },
+ "require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
+ }
+ },
+ "yargs": {
+ "version": "12.0.5",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+ "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+ "requires": {
+ "cliui": "^4.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^3.0.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1 || ^4.0.0",
+ "yargs-parser": "^11.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+ "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
}
}
}
@@ -12205,7 +12923,8 @@
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
},
"resolve": {
"version": "1.20.0",
@@ -12386,8 +13105,7 @@
"signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
- "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==",
- "dev": true
+ "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"slash": {
"version": "3.0.0",
@@ -12514,6 +13232,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
@@ -12524,10 +13243,16 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
+ },
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -13004,10 +13729,16 @@
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
"dev": true
},
+ "window-or-global": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/window-or-global/-/window-or-global-1.0.1.tgz",
+ "integrity": "sha1-2+RboqKRqrxW1iz2bEW3+jIpRt4="
+ },
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
@@ -13040,6 +13771,7 @@
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
@@ -13057,6 +13789,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
"requires": {
"locate-path": "^3.0.0"
}
@@ -13065,6 +13798,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
@@ -13074,6 +13808,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
"requires": {
"p-try": "^2.0.0"
}
@@ -13082,6 +13817,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
"requires": {
"p-limit": "^2.0.0"
}
@@ -13089,7 +13825,8 @@
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
}
}
},
@@ -13097,6 +13834,7 @@
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
diff --git a/apps/fz_http/assets/package.json b/apps/fz_http/assets/package.json
index dac1bfe52..a3ed00b47 100644
--- a/apps/fz_http/assets/package.json
+++ b/apps/fz_http/assets/package.json
@@ -21,12 +21,15 @@
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
"phoenix_live_view": "file:../../../deps/phoenix_live_view",
- "qrcode": "^1.4.4"
+ "qrcode": "^1.3.3"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
+ "@creativebulma/bulma-tooltip": "^1.2.0",
+ "@fontsource/fira-sans": "^4.5.0",
"@fortawesome/fontawesome-free": "^5.15.3",
+ "@mdi/font": "^6.5.95",
"admin-one-bulma-dashboard": "file:local_modules/admin-one-bulma-dashboard",
"autoprefixer": "^9.8.8",
"babel-loader": "^8.2.3",
diff --git a/apps/fz_http/lib/fz_http/application.ex b/apps/fz_http/lib/fz_http/application.ex
index a2048994b..9bcb1aa0c 100644
--- a/apps/fz_http/lib/fz_http/application.ex
+++ b/apps/fz_http/lib/fz_http/application.ex
@@ -6,28 +6,10 @@ defmodule FzHttp.Application do
use Application
def start(_type, _args) do
- children =
- case Application.get_env(:fz_http, :minimal) do
- true ->
- [
- FzHttp.Repo,
- FzHttp.Vault
- ]
-
- _ ->
- [
- FzHttp.Server,
- FzHttp.Repo,
- FzHttp.Vault,
- {Phoenix.PubSub, name: FzHttp.PubSub},
- FzHttpWeb.Endpoint
- ]
- end
-
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: FzHttp.Supervisor]
- Supervisor.start_link(children, opts)
+ Supervisor.start_link(children(), opts)
end
# Tell Phoenix to update the endpoint configuration
@@ -36,4 +18,27 @@ defmodule FzHttp.Application do
FzHttpWeb.Endpoint.config_change(changed, removed)
:ok
end
+
+ defp children, do: children(Application.fetch_env!(:fz_http, :supervision_tree_mode))
+
+ defp children(:full) do
+ [
+ FzHttp.Server,
+ FzHttp.Repo,
+ FzHttp.Vault,
+ FzHttpWeb.Endpoint,
+ {Phoenix.PubSub, name: FzHttp.PubSub},
+ FzHttp.ConnectivityCheckService
+ ]
+ end
+
+ defp children(:test) do
+ [
+ FzHttp.Server,
+ FzHttp.Repo,
+ FzHttp.Vault,
+ FzHttpWeb.Endpoint,
+ {Phoenix.PubSub, name: FzHttp.PubSub}
+ ]
+ end
end
diff --git a/apps/fz_http/lib/fz_http/connectivity_check_service.ex b/apps/fz_http/lib/fz_http/connectivity_check_service.ex
new file mode 100644
index 000000000..bd28d9e09
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/connectivity_check_service.ex
@@ -0,0 +1,78 @@
+defmodule FzHttp.ConnectivityCheckService do
+ @moduledoc """
+ A simple GenServer to periodically check for WAN connectivity by issuing
+ POSTs to https://ping[-dev].firez.one/{version}.
+ """
+ use GenServer
+
+ require Logger
+
+ alias FzHttp.ConnectivityChecks
+
+ def start_link(_) do
+ http_client().start()
+ GenServer.start_link(__MODULE__, %{})
+ end
+
+ @impl GenServer
+ def init(state) do
+ if enabled?() do
+ send(self(), :perform)
+ :timer.send_interval(interval(), :perform)
+ end
+
+ {:ok, state}
+ end
+
+ @impl GenServer
+ def handle_info(:perform, _state) do
+ # XXX: Consider passing state here to implement exponential backoff in the
+ # case of errors.
+ {:noreply, post_request()}
+ end
+
+ def post_request, do: post_request(url())
+
+ def post_request(request_url) do
+ body = ""
+
+ case http_client().post(request_url, body) do
+ {:ok, response} ->
+ ConnectivityChecks.create_connectivity_check(%{
+ response_body: response.body,
+ response_code: response.status_code,
+ response_headers: Map.new(response.headers),
+ url: request_url
+ })
+
+ response
+
+ {:error, error} ->
+ Logger.error("""
+ An unexpected error occurred while performing a Firezone connectivity check to #{request_url}. Reason: #{error.reason}
+ """)
+
+ error
+ end
+ end
+
+ defp url do
+ Application.fetch_env!(:fz_http, :connectivity_checks_url) <> version()
+ end
+
+ defp http_client do
+ Application.fetch_env!(:fz_http, :http_client)
+ end
+
+ defp version do
+ Application.spec(:fz_http, :vsn) |> to_string()
+ end
+
+ defp interval do
+ Application.fetch_env!(:fz_http, :connectivity_checks_interval) * 1_000
+ end
+
+ defp enabled? do
+ Application.fetch_env!(:fz_http, :connectivity_checks_enabled)
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/connectivity_checks.ex b/apps/fz_http/lib/fz_http/connectivity_checks.ex
new file mode 100644
index 000000000..db83fc727
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/connectivity_checks.ex
@@ -0,0 +1,138 @@
+defmodule FzHttp.ConnectivityChecks do
+ @moduledoc """
+ The ConnectivityChecks context.
+ """
+
+ import Ecto.Query, warn: false
+ alias FzHttp.Repo
+
+ alias FzHttp.ConnectivityChecks.ConnectivityCheck
+
+ @doc """
+ Returns the list of connectivity_checks.
+
+ ## Examples
+
+ iex> list_connectivity_checks()
+ [%ConnectivityCheck{}, ...]
+
+ """
+ def list_connectivity_checks do
+ Repo.all(ConnectivityCheck)
+ end
+
+ def list_connectivity_checks(limit: limit) when is_integer(limit) do
+ Repo.all(
+ from(
+ c in ConnectivityCheck,
+ limit: ^limit,
+ order_by: [desc: :inserted_at]
+ )
+ )
+ end
+
+ @doc """
+ Gets a single connectivity_check.
+
+ Raises `Ecto.NoResultsError` if the ConnectivityCheck does not exist.
+
+ ## Examples
+
+ iex> get_connectivity_check!(123)
+ %ConnectivityCheck{}
+
+ iex> get_connectivity_check!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_connectivity_check!(id), do: Repo.get!(ConnectivityCheck, id)
+
+ @doc """
+ Creates a connectivity_check.
+
+ ## Examples
+
+ iex> create_connectivity_check(%{field: value})
+ {:ok, %ConnectivityCheck{}}
+
+ iex> create_connectivity_check(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_connectivity_check(attrs \\ %{}) do
+ %ConnectivityCheck{}
+ |> ConnectivityCheck.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a connectivity_check.
+
+ ## Examples
+
+ iex> update_connectivity_check(connectivity_check, %{field: new_value})
+ {:ok, %ConnectivityCheck{}}
+
+ iex> update_connectivity_check(connectivity_check, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_connectivity_check(%ConnectivityCheck{} = connectivity_check, attrs) do
+ connectivity_check
+ |> ConnectivityCheck.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a connectivity_check.
+
+ ## Examples
+
+ iex> delete_connectivity_check(connectivity_check)
+ {:ok, %ConnectivityCheck{}}
+
+ iex> delete_connectivity_check(connectivity_check)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_connectivity_check(%ConnectivityCheck{} = connectivity_check) do
+ Repo.delete(connectivity_check)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking connectivity_check changes.
+
+ ## Examples
+
+ iex> change_connectivity_check(connectivity_check)
+ %Ecto.Changeset{data: %ConnectivityCheck{}}
+
+ """
+ def change_connectivity_check(%ConnectivityCheck{} = connectivity_check, attrs \\ %{}) do
+ ConnectivityCheck.changeset(connectivity_check, attrs)
+ end
+
+ @doc """
+ Returns the latest connectivity_check.
+ """
+ def latest_connectivity_check do
+ Repo.one(
+ from(
+ c in ConnectivityCheck,
+ limit: 1,
+ order_by: [desc: :inserted_at]
+ )
+ )
+ end
+
+ @doc """
+ Returns the latest connectivity_check's response_body which should contain the resolved public
+ IP.
+ """
+ def endpoint do
+ case latest_connectivity_check() do
+ nil -> nil
+ connectivity_check -> connectivity_check.response_body
+ end
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex b/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex
new file mode 100644
index 000000000..7571d8984
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex
@@ -0,0 +1,27 @@
+defmodule FzHttp.ConnectivityChecks.ConnectivityCheck do
+ @moduledoc """
+ Manages the connectivity_checks table
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ @url_regex ~r<\Ahttps://ping(?:-dev)?\.firez\.one/\d+\.\d+\.\d+(?:\+git\.\d+\.[0-9a-fA-F]{7,})?\z>
+
+ schema "connectivity_checks" do
+ field :response_body, :string
+ field :response_code, :integer
+ field :response_headers, :map
+ field :url, :string
+
+ timestamps(type: :utc_datetime_usec)
+ end
+
+ @doc false
+ def changeset(connectivity_check, attrs) do
+ connectivity_check
+ |> cast(attrs, [:url, :response_body, :response_code, :response_headers])
+ |> validate_required([:url, :response_code])
+ |> validate_format(:url, @url_regex)
+ |> validate_number(:response_code, greater_than_or_equal_to: 100, less_than: 600)
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/devices.ex b/apps/fz_http/lib/fz_http/devices.ex
index 6588c2a77..2a0a130d0 100644
--- a/apps/fz_http/lib/fz_http/devices.ex
+++ b/apps/fz_http/lib/fz_http/devices.ex
@@ -5,7 +5,7 @@ defmodule FzHttp.Devices do
import Ecto.Query, warn: false
alias FzCommon.NameGenerator
- alias FzHttp.{Devices.Device, Repo, Users.User}
+ alias FzHttp.{ConnectivityChecks, Devices.Device, Repo, Settings, Users.User}
@ipv4_prefix "10.3.2."
@ipv6_prefix "fd00:3:2::"
@@ -38,8 +38,8 @@ defmodule FzHttp.Devices do
Repo.delete(device)
end
- def change_device(%Device{} = device) do
- Device.update_changeset(device, %{})
+ def change_device(%Device{} = device, attrs \\ %{}) do
+ Device.update_changeset(device, attrs)
end
def rand_name do
@@ -62,4 +62,34 @@ defmodule FzHttp.Devices do
}
end
end
+
+ def allowed_ips(device) do
+ if device.use_default_allowed_ips do
+ Settings.default_device_allowed_ips()
+ else
+ device.allowed_ips
+ end
+ end
+
+ def dns_servers(device) do
+ if device.use_default_dns_servers do
+ Settings.default_device_dns_servers()
+ else
+ device.dns_servers
+ end
+ end
+
+ def endpoint(device) do
+ if device.use_default_endpoint do
+ Settings.default_device_endpoint() || ConnectivityChecks.endpoint()
+ else
+ device.endpoint
+ end
+ end
+
+ def defaults(changeset) do
+ ~w(use_default_allowed_ips use_default_dns_servers use_default_endpoint)a
+ |> Enum.map(fn field -> {field, Device.field(changeset, field)} end)
+ |> Map.new()
+ end
end
diff --git a/apps/fz_http/lib/fz_http/devices/device.ex b/apps/fz_http/lib/fz_http/devices/device.ex
index c0737f74d..a95c85435 100644
--- a/apps/fz_http/lib/fz_http/devices/device.ex
+++ b/apps/fz_http/lib/fz_http/devices/device.ex
@@ -6,10 +6,13 @@ defmodule FzHttp.Devices.Device do
use Ecto.Schema
import Ecto.Changeset
- import FzCommon.FzNet,
+ import FzHttp.SharedValidators,
only: [
- valid_ip?: 1,
- valid_cidr?: 1
+ validate_ip: 2,
+ validate_omitted: 2,
+ validate_list_of_ips: 2,
+ validate_no_duplicates: 2,
+ validate_list_of_ips_or_cidrs: 2
]
alias FzHttp.Users.User
@@ -17,8 +20,12 @@ defmodule FzHttp.Devices.Device do
schema "devices" do
field :name, :string
field :public_key, :string
- field :allowed_ips, :string, read_after_writes: true
- field :dns_servers, :string, read_after_writes: true
+ field :use_default_allowed_ips, :boolean, read_after_writes: true, default: true
+ field :use_default_dns_servers, :boolean, read_after_writes: true, default: true
+ field :use_default_endpoint, :boolean, read_after_writes: true, default: true
+ field :endpoint, :string
+ field :allowed_ips, :string
+ field :dns_servers, :string
field :private_key, FzHttp.Encrypted.Binary
field :server_public_key, :string
field :remote_ip, EctoNetwork.INET
@@ -32,25 +39,30 @@ defmodule FzHttp.Devices.Device do
def create_changeset(device, attrs) do
device
- |> cast(attrs, [
- :allowed_ips,
- :dns_servers,
- :remote_ip,
- :address,
- :server_public_key,
- :private_key,
- :user_id,
- :name,
- :public_key
- ])
+ |> shared_cast(attrs)
|> shared_changeset()
end
def update_changeset(device, attrs) do
+ device
+ |> shared_cast(attrs)
+ |> shared_changeset()
+ |> validate_required(:address)
+ end
+
+ def field(changeset, field) do
+ get_field(changeset, field)
+ end
+
+ defp shared_cast(device, attrs) do
device
|> cast(attrs, [
+ :use_default_allowed_ips,
+ :use_default_dns_servers,
+ :use_default_endpoint,
:allowed_ips,
:dns_servers,
+ :endpoint,
:remote_ip,
:address,
:server_public_key,
@@ -59,8 +71,6 @@ defmodule FzHttp.Devices.Device do
:name,
:public_key
])
- |> shared_changeset()
- |> validate_required(:address)
end
defp shared_changeset(changeset) do
@@ -72,9 +82,12 @@ defmodule FzHttp.Devices.Device do
:server_public_key,
:private_key
])
+ |> validate_required_unless_default([:allowed_ips, :dns_servers, :endpoint])
+ |> validate_omitted_if_default([:allowed_ips, :dns_servers, :endpoint])
|> validate_list_of_ips_or_cidrs(:allowed_ips)
|> validate_list_of_ips(:dns_servers)
|> validate_no_duplicates(:dns_servers)
+ |> validate_ip(:endpoint)
|> unique_constraint(:address)
|> validate_number(:address, greater_than_or_equal_to: 2, less_than_or_equal_to: 254)
|> unique_constraint(:public_key)
@@ -82,66 +95,25 @@ defmodule FzHttp.Devices.Device do
|> unique_constraint([:user_id, :name])
end
- defp validate_no_duplicates(changeset, field) when is_atom(field) do
- validate_change(changeset, field, fn _current_field, value ->
- try do
- trimmed = Enum.map(String.split(value, ","), fn el -> String.trim(el) end)
- dupes = Enum.uniq(trimmed -- Enum.uniq(trimmed))
+ defp validate_omitted_if_default(changeset, fields) when is_list(fields) do
+ fields_to_validate =
+ defaulted_fields(changeset, fields)
+ |> Enum.map(fn field ->
+ String.trim(Atom.to_string(field), "use_default_") |> String.to_atom()
+ end)
- if length(dupes) > 0 do
- throw(dupes)
- end
-
- []
- catch
- dupes ->
- [
- {field,
- "is invalid: duplicate DNS servers are not allowed: #{Enum.join(dupes, ", ")}"}
- ]
- end
- end)
+ validate_omitted(changeset, fields_to_validate)
end
- defp validate_list_of_ips(changeset, field) when is_atom(field) do
- validate_change(changeset, field, fn _current_field, value ->
- try do
- for ip <- String.split(value, ",") do
- unless valid_ip?(String.trim(ip)) do
- throw(ip)
- end
- end
-
- []
- catch
- ip ->
- [{field, "is invalid: #{String.trim(ip)} is not a valid IPv4 / IPv6 address"}]
- end
- end)
+ defp validate_required_unless_default(changeset, fields) when is_list(fields) do
+ fields_as_atoms = Enum.map(fields, fn field -> String.to_atom("use_default_#{field}") end)
+ fields_to_validate = fields_as_atoms -- defaulted_fields(changeset, fields)
+ validate_required(changeset, fields_to_validate)
end
- defp validate_list_of_ips_or_cidrs(changeset, field) when is_atom(field) do
- validate_change(changeset, field, fn _current_field, value ->
- try do
- for ip_or_cidr <- String.split(value, ",") do
- trimmed_ip_or_cidr = String.trim(ip_or_cidr)
-
- unless valid_ip?(trimmed_ip_or_cidr) or valid_cidr?(trimmed_ip_or_cidr) do
- throw(ip_or_cidr)
- end
- end
-
- []
- catch
- ip_or_cidr ->
- [
- {field,
- """
- is invalid: #{String.trim(ip_or_cidr)} is not a valid IPv4 / IPv6 address or \
- CIDR range\
- """}
- ]
- end
- end)
+ defp defaulted_fields(changeset, fields) do
+ fields
+ |> Enum.map(fn field -> String.to_atom("use_default_#{field}") end)
+ |> Enum.filter(fn field -> get_field(changeset, field) end)
end
end
diff --git a/apps/fz_http/lib/fz_http/macros.ex b/apps/fz_http/lib/fz_http/macros.ex
new file mode 100644
index 000000000..1e7048e4b
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/macros.ex
@@ -0,0 +1,17 @@
+defmodule FzHttp.Macros do
+ @moduledoc """
+ Metaprogramming macros
+ """
+
+ defmacro def_settings(keys) do
+ quote bind_quoted: [keys: keys] do
+ Enum.each(keys, fn key ->
+ fun_name = key |> String.replace(".", "_") |> String.to_atom() |> Macro.var(__MODULE__)
+
+ def unquote(fun_name) do
+ get_setting!(key: unquote(key)).value
+ end
+ end)
+ end
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/mock_http_client.ex b/apps/fz_http/lib/fz_http/mock_http_client.ex
new file mode 100644
index 000000000..8fb4097b9
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/mock_http_client.ex
@@ -0,0 +1,32 @@
+defmodule FzHttp.MockHttpClient do
+ @moduledoc """
+ Mocks http requests in place of HTTPoison
+ """
+
+ @success_response {
+ :ok,
+ %{
+ headers: [
+ {"content-length", 9},
+ {"date", "Tue, 07 Dec 2021 19:57:02 GMT"}
+ ],
+ status_code: 200,
+ body: "127.0.0.1"
+ }
+ }
+ @error_sentinel "invalid-url"
+ @error_response {:error, %{reason: :nxdomain}}
+
+ def start, do: nil
+
+ @doc """
+ Simulates a POST. Include @error_sentinel in the request URL to simulate an error.
+ """
+ def post(url, _body) do
+ if String.contains?(url, @error_sentinel) do
+ @error_response
+ else
+ @success_response
+ end
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/settings.ex b/apps/fz_http/lib/fz_http/settings.ex
new file mode 100644
index 000000000..f754f2177
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/settings.ex
@@ -0,0 +1,94 @@
+defmodule FzHttp.Settings do
+ @moduledoc """
+ The Settings context.
+ """
+
+ import FzHttp.Macros
+ import Ecto.Query, warn: false
+ alias FzHttp.Repo
+
+ alias FzHttp.Settings.Setting
+
+ def_settings(~w(
+ default.device.allowed_ips
+ default.device.dns_servers
+ default.device.endpoint
+ ))
+
+ @doc """
+ Returns the list of settings.
+
+ ## Examples
+
+ iex> list_settings()
+ [%Setting{}, ...]
+
+ """
+ def list_settings do
+ Repo.all(Setting)
+ end
+
+ @doc """
+ Gets a single setting by its ID.
+
+ Raises `Ecto.NoResultsError` if the Setting does not exist.
+
+ ## Examples
+
+ iex> get_setting!(123)
+ %Setting{}
+
+ iex> get_setting!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_setting!(key: key) do
+ Repo.one!(from s in Setting, where: s.key == ^key)
+ end
+
+ def get_setting!(id), do: Repo.get!(Setting, id)
+
+ @doc """
+ Updates a setting.
+
+ ## Examples
+
+ iex> update_setting(setting, %{field: new_value})
+ {:ok, %Setting{}}
+
+ iex> update_setting(setting, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_setting(%Setting{} = setting, attrs) do
+ setting
+ |> Setting.changeset(attrs)
+ |> Repo.update()
+ end
+
+ def update_setting(key, value) when is_binary(key) do
+ get_setting!(key: key)
+ |> update_setting(%{value: value})
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking setting changes.
+
+ ## Examples
+
+ iex> change_setting(setting)
+ %Ecto.Changeset{data: %Setting{}}
+
+ """
+ def change_setting(%Setting{} = setting, attrs \\ %{}) do
+ Setting.changeset(setting, attrs)
+ end
+
+ @doc """
+ Returns a list of all the settings beginning with the specified key prefix.
+ """
+ def to_list(prefix \\ "") do
+ starts_with = prefix <> "%"
+ Repo.all(from s in Setting, where: ilike(s.key, ^starts_with))
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/settings/setting.ex b/apps/fz_http/lib/fz_http/settings/setting.ex
new file mode 100644
index 000000000..ae393d8fc
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/settings/setting.ex
@@ -0,0 +1,69 @@
+defmodule FzHttp.Settings.Setting do
+ @moduledoc """
+ Represents Firezone runtime configuration settings.
+
+ Each record in the table has a unique key corresponding to a configuration setting.
+
+ Settings values can be changed at application runtime on the fly.
+ Settings cannot be created or destroyed by the running application.
+
+ Settings are created / destroyed in migrations.
+ """
+
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ import FzHttp.SharedValidators,
+ only: [
+ validate_list_of_ips: 2,
+ validate_list_of_ips_or_cidrs: 2,
+ validate_no_duplicates: 2
+ ]
+
+ schema "settings" do
+ field :key, :string
+ field :value, :string
+
+ timestamps(type: :utc_datetime_usec)
+ end
+
+ @doc false
+ def changeset(setting, attrs) do
+ setting
+ |> cast(attrs, [:key, :value])
+ |> validate_required([:key])
+ |> validate_setting()
+ end
+
+ defp validate_setting(%{data: %{key: key}, changes: %{value: _value}} = changeset) do
+ changeset
+ |> validate_kv_pair(key)
+ end
+
+ defp validate_setting(changeset), do: changeset
+
+ defp validate_kv_pair(changeset, "default.device.dns_servers") do
+ changeset
+ |> validate_list_of_ips(:value)
+ |> validate_no_duplicates(:value)
+ end
+
+ defp validate_kv_pair(changeset, "default.device.allowed_ips") do
+ changeset
+ |> validate_required(:value)
+ |> validate_list_of_ips_or_cidrs(:value)
+ |> validate_no_duplicates(:value)
+ end
+
+ defp validate_kv_pair(changeset, "default.device.endpoint") do
+ changeset
+ |> validate_list_of_ips_or_cidrs(:value)
+ |> validate_no_duplicates(:value)
+ end
+
+ defp validate_kv_pair(changeset, unknown_key) do
+ validate_change(changeset, :key, fn _current_field, _value ->
+ [{:key, "is invalid: #{unknown_key} is not a valid setting"}]
+ end)
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/shared_validators.ex b/apps/fz_http/lib/fz_http/shared_validators.ex
new file mode 100644
index 000000000..c641acb9d
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/shared_validators.ex
@@ -0,0 +1,109 @@
+defmodule FzHttp.SharedValidators do
+ @moduledoc """
+ Shared validators to use between schemas.
+ """
+
+ import Ecto.Changeset
+
+ import FzCommon.FzNet,
+ only: [
+ valid_ip?: 1,
+ valid_cidr?: 1
+ ]
+
+ def validate_no_duplicates(changeset, field) when is_atom(field) do
+ validate_change(changeset, field, fn _current_field, value ->
+ try do
+ trimmed = Enum.map(String.split(value, ","), fn el -> String.trim(el) end)
+ dupes = Enum.uniq(trimmed -- Enum.uniq(trimmed))
+
+ if length(dupes) > 0 do
+ throw(dupes)
+ end
+
+ []
+ catch
+ dupes ->
+ [
+ {field,
+ "is invalid: duplicate DNS servers are not allowed: #{Enum.join(dupes, ", ")}"}
+ ]
+ end
+ end)
+ end
+
+ def validate_ip(changeset, field) when is_atom(field) do
+ validate_change(changeset, field, fn _current_field, value ->
+ try do
+ for ip <- String.split(value, ",") do
+ unless valid_ip?(String.trim(ip)) do
+ throw(ip)
+ end
+ end
+
+ []
+ catch
+ ip ->
+ [{field, "is invalid: #{String.trim(ip)} is not a valid IPv4 / IPv6 address"}]
+ end
+ end)
+ end
+
+ def validate_list_of_ips(changeset, field) when is_atom(field) do
+ validate_change(changeset, field, fn _current_field, value ->
+ try do
+ for ip <- String.split(value, ",") do
+ unless valid_ip?(String.trim(ip)) do
+ throw(ip)
+ end
+ end
+
+ []
+ catch
+ ip ->
+ [{field, "is invalid: #{String.trim(ip)} is not a valid IPv4 / IPv6 address"}]
+ end
+ end)
+ end
+
+ def validate_list_of_ips_or_cidrs(changeset, field) when is_atom(field) do
+ validate_change(changeset, field, fn _current_field, value ->
+ try do
+ for ip_or_cidr <- String.split(value, ",") do
+ trimmed_ip_or_cidr = String.trim(ip_or_cidr)
+
+ unless valid_ip?(trimmed_ip_or_cidr) or valid_cidr?(trimmed_ip_or_cidr) do
+ throw(ip_or_cidr)
+ end
+ end
+
+ []
+ catch
+ ip_or_cidr ->
+ [
+ {field,
+ """
+ is invalid: #{String.trim(ip_or_cidr)} is not a valid IPv4 / IPv6 address or \
+ CIDR range\
+ """}
+ ]
+ end
+ end)
+ end
+
+ def validate_omitted(changeset, fields) when is_list(fields) do
+ Enum.reduce(fields, changeset, fn field, accumulated_changeset ->
+ validate_omitted(accumulated_changeset, field)
+ end)
+ end
+
+ def validate_omitted(changeset, field) when is_atom(field) do
+ validate_change(changeset, field, fn _current_field, value ->
+ if is_nil(value) do
+ []
+ else
+ [{field, "must not be present"}]
+ end
+ end)
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/users/user.ex b/apps/fz_http/lib/fz_http/users/user.ex
index 8ada19226..fe0808d84 100644
--- a/apps/fz_http/lib/fz_http/users/user.ex
+++ b/apps/fz_http/lib/fz_http/users/user.ex
@@ -10,7 +10,6 @@ defmodule FzHttp.Users.User do
import Ecto.Changeset
import FzHttp.Users.PasswordHelpers
- alias FzCommon.FzMap
alias FzHttp.Devices.Device
schema "users" do
@@ -62,7 +61,7 @@ defmodule FzHttp.Users.User do
|> validate_required([:sign_in_token, :sign_in_token_created_at])
end
- # Password updated with user logged in
+ # If password isn't being changed, remove it from list of attributes to validate
def update_changeset(
user,
%{
@@ -71,16 +70,24 @@ defmodule FzHttp.Users.User do
"current_password" => nil
} = attrs
) do
- update_changeset(user, FzMap.compact(attrs))
+ update_changeset(
+ user,
+ Map.drop(attrs, ["password", "password_confirmation", "current_password"])
+ )
end
+ # If password isn't being changed, remove it from list of attributes to validate
def update_changeset(
user,
%{"password" => "", "password_confirmation" => "", "current_password" => ""} = attrs
) do
- update_changeset(user, FzMap.compact(attrs, ""))
+ update_changeset(
+ user,
+ Map.drop(attrs, ["password", "password_confirmation", "current_password"])
+ )
end
+ # Password and other fields are being changed
def update_changeset(
user,
%{
@@ -109,7 +116,7 @@ defmodule FzHttp.Users.User do
"password_confirmation" => ""
} = attrs
) do
- update_changeset(user, FzMap.compact(attrs, ""))
+ update_changeset(user, Map.drop(attrs, ["password", "password_confirmation"]))
end
# Password updated from token or admin
diff --git a/apps/fz_http/lib/fz_http_web/live/account_live/show_live.ex b/apps/fz_http/lib/fz_http_web/live/account_live/show_live.ex
index dba6839da..3673f43c7 100644
--- a/apps/fz_http/lib/fz_http_web/live/account_live/show_live.ex
+++ b/apps/fz_http/lib/fz_http_web/live/account_live/show_live.ex
@@ -6,7 +6,7 @@ defmodule FzHttpWeb.AccountLive.Show do
alias FzHttp.Users
- @impl true
+ @impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
@@ -14,7 +14,7 @@ defmodule FzHttpWeb.AccountLive.Show do
|> assign(:page_title, "Account")}
end
- @impl true
+ @impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
diff --git a/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index.html.heex b/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index.html.heex
new file mode 100644
index 000000000..14a93d4db
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index.html.heex
@@ -0,0 +1,41 @@
+<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
+
+
+ <%= render FzHttpWeb.SharedView, "flash.html", assigns %>
+
+
+
+ Firezone periodically checks for WAN connectivity to the Internet and logs
+ the result here. This is used to determine the public IP address of this
+ server for populating the default endpoint field in device configurations.
+
+
+
+
+ Checked At
+ Resolved IP
+ Status
+
+
+
+ <%= for connectivity_check <- @connectivity_checks do %>
+
+
+ …
+
+ <%= connectivity_check.response_body %>
+
+
+
+
+
+
+ <% end %>
+
+
+
+
diff --git a/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex b/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex
new file mode 100644
index 000000000..e7edfe244
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex
@@ -0,0 +1,20 @@
+defmodule FzHttpWeb.ConnectivityCheckLive.Index do
+ @moduledoc """
+ Manages the connectivity_checks view.
+ """
+ use FzHttpWeb, :live_view
+
+ alias FzHttp.ConnectivityChecks
+
+ @impl Phoenix.LiveView
+ def mount(params, session, socket) do
+ {:ok,
+ socket
+ |> assign_defaults(params, session, &load_data/2)
+ |> assign(:page_title, "Connectivity Checks")}
+ end
+
+ defp load_data(_params, socket) do
+ assign(socket, :connectivity_checks, ConnectivityChecks.list_connectivity_checks(limit: 20))
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex
index e958bab8a..fa5163087 100644
--- a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex
+++ b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex
@@ -4,17 +4,32 @@ defmodule FzHttpWeb.DeviceLive.FormComponent do
"""
use FzHttpWeb, :live_component
- alias FzHttp.Devices
+ alias FzHttp.{ConnectivityChecks, Devices, Settings}
def update(assigns, socket) do
- changeset = Devices.change_device(assigns.device)
+ device = assigns.device
+ changeset = Devices.change_device(device)
+ default_device_endpoint = Settings.default_device_endpoint() || ConnectivityChecks.endpoint()
{:ok,
socket
|> assign(assigns)
+ |> assign(Devices.defaults(changeset))
+ |> assign(:default_device_allowed_ips, Settings.default_device_allowed_ips())
+ |> assign(:default_device_dns_servers, Settings.default_device_dns_servers())
+ |> assign(:default_device_endpoint, default_device_endpoint)
|> assign(:changeset, changeset)}
end
+ def handle_event("change", %{"device" => device_params}, socket) do
+ changeset = Devices.change_device(socket.assigns.device, device_params)
+
+ {:noreply,
+ socket
+ |> assign(:changeset, changeset)
+ |> assign(Devices.defaults(changeset))}
+ end
+
def handle_event("save", %{"device" => device_params}, socket) do
device = socket.assigns.device
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex
index a4f5288c0..3c4477a0e 100644
--- a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex
+++ b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex
@@ -1,8 +1,7 @@
- <.form let={f} for={@changeset} id="edit-device" phx-target={@myself} phx-submit="save">
+ <.form let={f} for={@changeset} id="edit-device" phx-change="change" phx-target={@myself} phx-submit="save">
<%= label f, :name, class: "label" %>
-
<%= text_input f, :name, class: "input" %>
@@ -12,10 +11,26 @@
- <%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
-
+ <%= label f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label" %>
- <%= text_input f, :allowed_ips, class: "input" %>
+
+ <%= radio_button f, :use_default_allowed_ips, true %>
+ Yes
+
+
+ <%= radio_button f, :use_default_allowed_ips, false %>
+ No
+
+
+
+ Default: <%= @default_device_allowed_ips %>
+
+
+
+
+ <%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
+
+ <%= text_input f, :allowed_ips, class: "input", disabled: @use_default_allowed_ips %>
<%= error_tag f, :allowed_ips %>
@@ -23,16 +38,60 @@
- <%= label f, :dns_servers, "DNS Servers", class: "label" %>
-
+ <%= label f, :use_default_dns_servers, "Use Default DNS Servers", class: "label" %>
- <%= text_input f, :dns_servers, class: "input" %>
+
+ <%= radio_button f, :use_default_dns_servers, true %>
+ Yes
+
+
+ <%= radio_button f, :use_default_dns_servers, false %>
+ No
+
+
+
+ Default: <%= @default_device_dns_servers %>
+
+
+
+
+ <%= label f, :dns_servers, "DNS Servers", class: "label" %>
+
+ <%= text_input f, :dns_servers, class: "input", disabled: @use_default_dns_servers %>
<%= error_tag f, :dns_servers %>
+
+ <%= label f, :use_default_endpoint, "Use Default Endpoint", class: "label" %>
+
+
+ <%= radio_button f, :use_default_endpoint, true %>
+ Yes
+
+
+ <%= radio_button f, :use_default_endpoint, false %>
+ No
+
+
+
+ Default: <%= @default_device_endpoint %>
+
+
+
+
+ <%= label f, :endpoint, "Server Endpoint", class: "label" %>
+
The IP of the server this device should connect to.
+
+ <%= text_input f, :endpoint, class: "input", disabled: @use_default_endpoint %>
+
+
+ <%= error_tag f, :endpoint %>
+
+
+
<%= label f, :address, "Interface Address (last octet only)", class: "label" %>
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex
index 157d8409b..93a44505d 100644
--- a/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex
+++ b/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex
@@ -34,10 +34,13 @@
Allowed IPs
-
<%= @device.allowed_ips %>
+
<%= @allowed_ips %>
DNS Servers
-
<%= @device.dns_servers %>
+
<%= @dns_servers || "None" %>
+
+
Endpoint
+
<%= @endpoint %>
Public key
<%= @device.public_key %>
@@ -88,12 +91,12 @@
[Interface]
PrivateKey = <%= @device.private_key %>
Address = <%= FzHttp.Devices.ipv4_address(@device) %>/32, <%= FzHttp.Devices.ipv6_address(@device) %>/128
-DNS = <%= @device.dns_servers %>
+<%= @dns_servers %>
[Peer]
PublicKey = <%= @device.server_public_key %>
-AllowedIPs = <%= @device.allowed_ips %>
-Endpoint = <%= @wireguard_endpoint %>:<%= @wireguard_port %>
+AllowedIPs = <%= @allowed_ips %>
+Endpoint = <%= @endpoint %>:<%= @wireguard_port %>
Or scan the QR code with your mobile phone:
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex
index 021fb9d96..2f7ac4ef4 100644
--- a/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex
+++ b/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex
@@ -51,11 +51,34 @@ defmodule FzHttpWeb.DeviceLive.Show do
device: device,
user: Users.get_user!(device.user_id),
page_title: device.name,
- wireguard_endpoint: Application.fetch_env!(:fz_vpn, :wireguard_endpoint),
+ allowed_ips: Devices.allowed_ips(device),
+ dns_servers: dns_servers(device),
+ endpoint: Devices.endpoint(device),
wireguard_port: Application.fetch_env!(:fz_vpn, :wireguard_port)
)
else
not_authorized(socket)
end
end
+
+ defp dns_servers(device) when is_struct(device) do
+ dns_servers = Devices.dns_servers(device)
+
+ if dns_servers_empty?(dns_servers) do
+ ""
+ else
+ "DNS = #{dns_servers}"
+ end
+ end
+
+ defp dns_servers_empty?(nil), do: true
+
+ defp dns_servers_empty?(dns_servers) when is_binary(dns_servers) do
+ len =
+ dns_servers
+ |> String.trim()
+ |> String.length()
+
+ len == 0
+ end
end
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex
new file mode 100644
index 000000000..07bf027ed
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex
@@ -0,0 +1,49 @@
+<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
+
+
+ <%= render FzHttpWeb.SharedView, "flash.html", assigns %>
+
+
+
+
+
+
+
+ <%= live_component(
+ @socket,
+ FzHttpWeb.SettingLive.FormComponent,
+ label_text: "Allowed IPs",
+ placeholder: nil,
+ changeset: @changesets["default.device.allowed_ips"],
+ help_text: @help_texts.allowed_ips,
+ id: :allowed_ips_form_component) %>
+
+
+
+ <%= live_component(
+ @socket,
+ FzHttpWeb.SettingLive.FormComponent,
+ label_text: "DNS Servers",
+ placeholder: nil,
+ changeset: @changesets["default.device.dns_servers"],
+ help_text: @help_texts.dns_servers,
+ id: :dns_servers_form_component) %>
+
+
+
+ <%= live_component(
+ @socket,
+ FzHttpWeb.SettingLive.FormComponent,
+ label_text: "Endpoint",
+ placeholder: @endpoint_placeholder,
+ changeset: @changesets["default.device.endpoint"],
+ help_text: @help_texts.endpoint,
+ id: :endpoint_form_component) %>
+
+
+
+
+
+
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex
new file mode 100644
index 000000000..60e63919f
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex
@@ -0,0 +1,46 @@
+defmodule FzHttpWeb.SettingLive.Default do
+ @moduledoc """
+ Manages the defaults view.
+ """
+ use FzHttpWeb, :live_view
+
+ alias FzHttp.{ConnectivityChecks, Settings}
+
+ @help_texts %{
+ allowed_ips: """
+ Configures the default AllowedIPs setting for devices.
+ AllowedIPs determines which destination IPs get routed through
+ Firezone. Specify a comma-separated list of IPs or CIDRs here to achieve split tunneling, or use
+ 0.0.0.0/0, ::/0 to route all device traffic through this Firezone server.
+ """,
+ dns_servers: """
+ Comma-separated list of DNS servers to use for devices.
+ Leaving this blank will omit the DNS section in
+ generated device configs.
+ """,
+ endpoint: """
+ IPv4 or IPv6 address that devices will be configured to connect
+ to. Defaults to this server's public IP if not set.
+ """
+ }
+
+ @impl Phoenix.LiveView
+ def mount(params, session, socket) do
+ {:ok,
+ socket
+ |> assign_defaults(params, session)
+ |> assign(:help_texts, @help_texts)
+ |> assign(:changesets, load_changesets())
+ |> assign(:endpoint_placeholder, endpoint_placeholder())
+ |> assign(:page_title, "Default Settings")}
+ end
+
+ defp endpoint_placeholder do
+ ConnectivityChecks.endpoint()
+ end
+
+ defp load_changesets do
+ Settings.to_list("default.")
+ |> Map.new(fn setting -> {setting.key, Settings.change_setting(setting)} end)
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/form_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/form_component.ex
new file mode 100644
index 000000000..2994ab7af
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/setting_live/form_component.ex
@@ -0,0 +1,62 @@
+defmodule FzHttpWeb.SettingLive.FormComponent do
+ @moduledoc """
+ Handles updating setting values, one at a time
+ """
+ use FzHttpWeb, :live_component
+
+ alias FzHttp.Settings
+
+ @impl Phoenix.LiveComponent
+ def update(assigns, socket) do
+ {:ok,
+ socket
+ |> assign(:input_class, "input")
+ |> assign(:form_changed, false)
+ |> assign(:input_icon, "")
+ |> assign(assigns)}
+ end
+
+ @impl Phoenix.LiveComponent
+ def handle_event("save", %{"setting" => %{"value" => value}}, socket) do
+ key = socket.assigns.changeset.data.key
+
+ case Settings.update_setting(key, value) do
+ {:ok, setting} ->
+ {:noreply,
+ socket
+ |> assign(:input_class, input_class(false))
+ |> assign(:input_icon, input_icon(false))
+ |> assign(:changeset, Settings.change_setting(setting, %{}))}
+
+ {:error, changeset} ->
+ {:noreply,
+ socket
+ |> assign(:input_class, input_class(true))
+ |> assign(:input_icon, input_icon(true))
+ |> assign(:changeset, changeset)}
+ end
+ end
+
+ @impl Phoenix.LiveComponent
+ def handle_event("change", _params, socket) do
+ {:noreply,
+ socket
+ |> assign(:form_changed, true)}
+ end
+
+ defp input_icon(false) do
+ "mdi mdi-check-circle"
+ end
+
+ defp input_icon(true) do
+ "mdi mdi-alert-circle"
+ end
+
+ defp input_class(false) do
+ "input is-success"
+ end
+
+ defp input_class(true) do
+ "input is-danger"
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/form_component.html.heex
new file mode 100644
index 000000000..bf09b6d72
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/setting_live/form_component.html.heex
@@ -0,0 +1,26 @@
+
+ <.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-change="change" phx-submit="save">
+
+ <%= label f, :value, @label_text, class: "label" %>
+
+
+ <%= text_input f, :value, placeholder: @placeholder, class: @input_class %>
+
+
+
+
+ <%= error_tag f, :value %>
+
+
+ <%= raw @help_text %>
+
+
+ <%= if @form_changed do %>
+
+ <%= submit "Save", class: "button is-primary" %>
+
+ <% end %>
+
+
+
+
diff --git a/apps/fz_http/lib/fz_http_web/live_helpers.ex b/apps/fz_http/lib/fz_http_web/live_helpers.ex
index d3fd10f0f..d6cdad2ca 100644
--- a/apps/fz_http/lib/fz_http_web/live_helpers.ex
+++ b/apps/fz_http/lib/fz_http_web/live_helpers.ex
@@ -1,6 +1,8 @@
defmodule FzHttpWeb.LiveHelpers do
@moduledoc """
Helpers available to all LiveViews.
+ XXX: Consider splitting these up using one of the techniques at
+ https://bernheisel.com/blog/phoenix-liveview-and-views
"""
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
@@ -43,4 +45,33 @@ defmodule FzHttpWeb.LiveHelpers do
modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
live_component(FzHttpWeb.ModalComponent, modal_opts)
end
+
+ def connectivity_check_span_class(response_code) do
+ if http_success?(status_digit(response_code)) do
+ "icon has-text-success"
+ else
+ "icon has-text-danger"
+ end
+ end
+
+ def connectivity_check_icon_class(response_code) do
+ if http_success?(status_digit(response_code)) do
+ "mdi mdi-check-circle"
+ else
+ "mdi mdi-alert-circle"
+ end
+ end
+
+ defp status_digit(response_code) when is_integer(response_code) do
+ [status_digit | _tail] = Integer.digits(response_code)
+ status_digit
+ end
+
+ defp http_success?(2) do
+ true
+ end
+
+ defp http_success?(_) do
+ false
+ end
end
diff --git a/apps/fz_http/lib/fz_http_web/router.ex b/apps/fz_http/lib/fz_http_web/router.ex
index 3023dd02f..33b9c7fa5 100644
--- a/apps/fz_http/lib/fz_http_web/router.ex
+++ b/apps/fz_http/lib/fz_http_web/router.ex
@@ -24,9 +24,6 @@ defmodule FzHttpWeb.Router do
get "/", DeviceController, :index
resources "/session", SessionController, only: [:new, :create, :delete], singleton: true
- live "/account", AccountLive.Show, :show
- live "/account/edit", AccountLive.Show, :edit
-
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
@@ -38,6 +35,13 @@ defmodule FzHttpWeb.Router do
live "/devices/:id", DeviceLive.Show, :show
live "/devices/:id/edit", DeviceLive.Show, :edit
+ live "/settings/default", SettingLive.Default, :default
+
+ live "/account", AccountLive.Show, :show
+ live "/account/edit", AccountLive.Show, :edit
+
+ live "/connectivity_checks", ConnectivityCheckLive.Index, :index
+
get "/sign_in/:token", SessionController, :create
delete "/user", UserController, :delete
end
diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex
index 31fff31f3..d8a7002c6 100644
--- a/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex
+++ b/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex
@@ -17,10 +17,6 @@
-
-
-
-
@@ -44,8 +40,8 @@
<%= link(to: Routes.account_show_path(@conn, :show), class: "navbar-item") do %>
-
- Settings
+
+ Account Settings
<% end %>
<%= link(to: Routes.session_path(@conn, :delete), method: :delete, class: "navbar-item") do %>
@@ -78,7 +74,7 @@
<%= link(to: Routes.device_index_path(@conn, :index), class: nav_class(@conn.request_path, "devices")) do %>
-
+
<% end %>
@@ -91,6 +87,12 @@
+
+
@@ -109,7 +120,7 @@
<%= link(to: "mailto:" <> feedback_recipient()) do %>
- Leave us feedback!
+ Click here to leave feedback
<% end %>
@@ -129,7 +140,5 @@
-
-