diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c0b81c40f..11d54bd4b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -351,14 +351,14 @@ jobs:
run: mix deps.compile --skip-umbrella-children
- name: Compile Application
run: mix compile
- - name: Install Node Dependencies
+ - name: Install Front-End Dependencies
run: |
- cd apps/web/assets
- yarn install --frozen-lockfile
+ cd apps/web
+ mix assets.setup
- name: Build Assets
run: |
- cd apps/web/assets
- yarn deploy
+ cd apps/web
+ mix assets.build
- name: Setup Database
run: |
mix ecto.create
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 91e49b86e..e3028b842 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -47,7 +47,6 @@ RUN mix do deps.get, deps.compile, compile
# Copy more granular, dependency management files first to prevent
# busting the Docker build cache unnecessarily
COPY apps/web/assets/package.json /var/app/apps/web/assets/package.json
-COPY apps/web/assets/local_modules /var/app/apps/web/assets/local_modules
COPY apps/web/assets/yarn.lock /var/app/apps/web/assets/yarn.lock
RUN cd apps/web/assets && yarn install
diff --git a/apps/domain/mix.exs b/apps/domain/mix.exs
index 10582f011..f44814c0b 100644
--- a/apps/domain/mix.exs
+++ b/apps/domain/mix.exs
@@ -11,7 +11,6 @@ defmodule Domain.MixProject do
lockfile: "../../mix.lock",
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
- compilers: Mix.compilers(),
start_permanent: Mix.env() == :prod,
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
diff --git a/apps/web/.formatter.exs b/apps/web/.formatter.exs
index f174e5dfd..ce4ddab66 100644
--- a/apps/web/.formatter.exs
+++ b/apps/web/.formatter.exs
@@ -1,17 +1,9 @@
[
+ import_deps: [:phoenix, :phoenix_live_view],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"],
locals_without_parens: [
assert_authenticated: 2,
assert_unauthenticated: 1
- ],
- import_deps: [
- :phoenix,
- :phoenix_live_view
- ],
- inputs: [
- "*.{heex,ex,exs}",
- "{lib,test,priv}/**/*.{heex,ex,exs}"
- ],
- plugins: [
- Phoenix.LiveView.HTMLFormatter
]
]
diff --git a/apps/web/.gitignore b/apps/web/.gitignore
index a02cf5bae..63ffbec2f 100644
--- a/apps/web/.gitignore
+++ b/apps/web/.gitignore
@@ -1,6 +1,3 @@
-# macOS cruft
-.DS_Store
-
# The directory Mix will write compiled artifacts to.
/_build/
@@ -22,18 +19,19 @@ erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
+# Temporary files, for example, from tests.
+/tmp/
+
# Ignore package tarball (built via "mix hex.build").
-cloudfire-*.tar
+web-*.tar
-# If NPM crashes, it generates a log, let's ignore it too.
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
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
-# this depending on your deployment strategy.
-/priv/static/dist/
diff --git a/apps/web/README.md b/apps/web/README.md
index e9b4ae911..d7d805593 100644
--- a/apps/web/README.md
+++ b/apps/web/README.md
@@ -1,3 +1,18 @@
-# Domain
+# Web
-Phoenix app for managing Firezone.
+To start your Phoenix server:
+
+ * Run `mix setup` to install and setup dependencies
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+ * Official website: https://www.phoenixframework.org/
+ * Guides: https://hexdocs.pm/phoenix/overview.html
+ * Docs: https://hexdocs.pm/phoenix
+ * Forum: https://elixirforum.com/c/phoenix-forum
+ * Source: https://github.com/phoenixframework/phoenix
diff --git a/apps/web/assets/.babelrc b/apps/web/assets/.babelrc
deleted file mode 100644
index ce33b24d4..000000000
--- a/apps/web/assets/.babelrc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "presets": [
- "@babel/preset-env"
- ]
-}
diff --git a/apps/web/assets/config.dev.js b/apps/web/assets/config.dev.js
deleted file mode 100644
index 651b60025..000000000
--- a/apps/web/assets/config.dev.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const { config: prodConfig } = require('./config.prod')
-
-module.exports.config = {
- ...prodConfig,
- minify: false,
- sourcemap: true,
- watch: true,
-}
diff --git a/apps/web/assets/config.prod.js b/apps/web/assets/config.prod.js
deleted file mode 100644
index 02daccde1..000000000
--- a/apps/web/assets/config.prod.js
+++ /dev/null
@@ -1,37 +0,0 @@
-const path = require('path')
-const { sassPlugin } = require('esbuild-sass-plugin')
-
-const tildePlugin = {
- name: 'tilde',
- setup(build) {
- build.onResolve({ filter: /^~/ }, args => ({
- path: path.join(
- args.resolveDir,
- '..',
- args.path
- .replace(/^~/, 'node_modules/')
- .replace(/\?.*$|#\w+$/, '')
- ),
- }))
- },
-}
-
-module.exports.config = {
- entryPoints: ['js/admin.js', 'js/root.js', 'js/unprivileged.js'],
- bundle: true,
- outdir: '../priv/static/dist',
- publicPath: '/dist/',
- minify: true,
- plugins: [
- tildePlugin,
- sassPlugin({
- loadPaths: ['.'],
- })],
- loader: {
- '.eot': 'file',
- '.svg': 'file',
- '.ttf': 'file',
- '.woff': 'file',
- '.woff2': 'file',
- }
-}
diff --git a/apps/web/assets/css/app.css b/apps/web/assets/css/app.css
new file mode 100644
index 000000000..378c8f905
--- /dev/null
+++ b/apps/web/assets/css/app.css
@@ -0,0 +1,5 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
+/* This file is for your main application CSS */
diff --git a/apps/web/assets/css/app.scss b/apps/web/assets/css/app.scss
deleted file mode 100644
index 0727fc16b..000000000
--- a/apps/web/assets/css/app.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-/* This file is for your main application css. */
-
-@charset "utf-8";
-@import "~admin-one-bulma-dashboard/src/scss/main.scss";
-@import "./main.scss";
-@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/web/assets/css/email.scss b/apps/web/assets/css/email.scss
deleted file mode 100644
index ee6e3ee9d..000000000
--- a/apps/web/assets/css/email.scss
+++ /dev/null
@@ -1 +0,0 @@
-/* Email Styles */
diff --git a/apps/web/assets/css/main.scss b/apps/web/assets/css/main.scss
deleted file mode 100644
index 3012b16a1..000000000
--- a/apps/web/assets/css/main.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-/* Main Styles */
-
-nav.navbar {
- border-bottom: 1px solid black;
-}
-
-.code {
- @extend .is-family-monospace;
-}
-
-pre.multiline {
- white-space: pre-wrap; /* Since CSS 2.1 */
- white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
- white-space: -pre-wrap; /* Opera 4-6 */
- white-space: -o-pre-wrap; /* Opera 7 */
- word-wrap: break-word; /* Internet Explorer 5.5+ */
-}
-
-.is-horizontally-scrollable {
- overflow-x: auto;
-}
-
-.is-vertically-scrollable {
- overflow-y: auto;
-}
-
-pre {
- background-color: $interface-000;
- color: $interface-600;
-}
-
-.dropdown-menu.is-large {
- width: 26rem;
-}
-
-.pre-wrapped {
- white-space: pre-wrap;
-}
-
-.is-main-section {
- overflow-x: auto;
-}
-
-.line-clamp {
- // supported in all browsers but with prefix
- display: -webkit-box;
- -webkit-line-clamp: 5;
- -webkit-box-orient: vertical;
- max-width: 600px;
- overflow: hidden;
-}
-
-.switch {
- &:hover input[type=checkbox]:disabled+.check {
- background: rgba(181, 181, 181, 1);
- }
-
- &:hover input[type=checkbox]:checked:disabled+.check {
- background: rgba(94, 0, 214, 1);
- }
-
- input[type=checkbox]:disabled+.check {
- opacity: 0.5;
- cursor: not-allowed;
- }
-}
diff --git a/apps/web/assets/css/tables.scss b/apps/web/assets/css/tables.scss
deleted file mode 100644
index d8a795af7..000000000
--- a/apps/web/assets/css/tables.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-$widths: (1, 2, 3, 4, 5, 6, 7);
-
-@each $width in $widths {
- $i: index($widths, $width);
- .table thead th.is-#{$i} {
- width: #{$width}em !important;
- }
-}
diff --git a/apps/web/assets/esbuild.js b/apps/web/assets/esbuild.js
deleted file mode 100644
index e0571a194..000000000
--- a/apps/web/assets/esbuild.js
+++ /dev/null
@@ -1,4 +0,0 @@
-const esbuild = require('esbuild')
-const { config } = require(`./config.${process.argv[2]}`)
-
-esbuild.build(config).catch(() => process.exit(1))
diff --git a/apps/web/assets/js/admin.js b/apps/web/assets/js/admin.js
deleted file mode 100644
index e5d81144a..000000000
--- a/apps/web/assets/js/admin.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import css from "../css/app.scss"
-import "admin-one-bulma-dashboard/src/js/main.js"
-
-/* Application fonts */
-import "@fontsource/fira-sans"
-import "@fontsource/open-sans"
-import "@fontsource/fira-mono"
-
-import "phoenix_html"
-import "./live_view.js"
-import "./event_listeners.js"
diff --git a/apps/web/assets/js/app.js b/apps/web/assets/js/app.js
new file mode 100644
index 000000000..df0cdd9f6
--- /dev/null
+++ b/apps/web/assets/js/app.js
@@ -0,0 +1,41 @@
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+import {Socket} from "phoenix"
+import {LiveSocket} from "phoenix_live_view"
+import topbar from "../vendor/topbar"
+
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
+
+// Show progress bar on live navigation and form submits
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
+window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
+
+// connect if there are any LiveViews on the page
+liveSocket.connect()
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket
+
diff --git a/apps/web/assets/js/crypto.js b/apps/web/assets/js/crypto.js
deleted file mode 100644
index 298bcb81e..000000000
--- a/apps/web/assets/js/crypto.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { box } from "tweetnacl/nacl-fast"
-import { encodeBase64 } from "tweetnacl-util"
-
-let fzCrypto = {
- generateKeyPair () {
- let kp = box.keyPair()
- return {
- privateKey: encodeBase64(kp.secretKey),
- publicKey: encodeBase64(kp.publicKey)
- }
- }
-}
-
-export { fzCrypto }
diff --git a/apps/web/assets/js/event_listeners.js b/apps/web/assets/js/event_listeners.js
deleted file mode 100644
index ad46fd487..000000000
--- a/apps/web/assets/js/event_listeners.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import {FormatTimestamp} from "./util.js"
-
-// Notification dismiss
-document.addEventListener('DOMContentLoaded', () => {
- (document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
- const $notification = $delete.parentNode
-
- $delete.addEventListener('click', () => {
- $notification.parentNode.removeChild($notification)
- })
- })
-})
-
-document.addEventListener('DOMContentLoaded', () => {
- (document.querySelectorAll('[data-timestamp]') || []).forEach(($span) => {
- $span.innerHTML = FormatTimestamp($span.dataset.timestamp)
- })
-})
-
-document.addEventListener("firezone:clipcopy", (event) => {
- if ("clipboard" in navigator) {
- const text = event.target.textContent
- navigator.clipboard.writeText(text)
- const dispatcher = event.detail.dispatcher
- const span = dispatcher.getElementsByTagName("span")[0]
- const icon = dispatcher.getElementsByTagName("i")[0]
-
- span.classList.add("has-text-success")
- icon.classList.replace("mdi-content-copy", "mdi-check-bold")
-
- setTimeout(() => {
- span.classList.remove("has-text-success")
- icon.classList.replace("mdi-check-bold", "mdi-content-copy")
- }, 1000)
- } else {
- alert("Sorry, your browser does not support clipboard copy.")
- }
-})
diff --git a/apps/web/assets/js/hooks.js b/apps/web/assets/js/hooks.js
deleted file mode 100644
index 0a19f7d04..000000000
--- a/apps/web/assets/js/hooks.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import hljs from "highlight.js"
-import { FormatTimestamp, PasswordStrength } from "./util.js"
-import { renderConfig } from "./wg_conf.js"
-import { renderQR } from "./qrcode.js"
-import { fzCrypto } from "./crypto.js"
-
-const highlightCode = function () {
- hljs.highlightAll()
-}
-
-const formatTimestamp = function () {
- let t = this.el.dataset.timestamp
- this.el.innerHTML = FormatTimestamp(t)
-}
-
-const passwordStrength = function () {
- const field = this.el
- const fieldClasses = "password input "
- const progress = document.getElementById(field.dataset.target)
- const reset = function () {
- field.className = fieldClasses
- progress.className = "is-hidden"
- progress.setAttribute("value", "0")
- progress.innerHTML = "0%"
- }
- field.addEventListener("input", () => {
- if (field.value === "") return reset()
- const score = PasswordStrength(field.value)
- switch (score) {
- case 0:
- case 1:
- field.className = fieldClasses + "is-danger"
- progress.className = "progress is-small is-danger"
- progress.setAttribute("value", "33")
- progress.innerHTML = "33%"
- break
- case 2:
- case 3:
- field.className = fieldClasses + "is-warning"
- progress.className = "progress is-small is-warning"
- progress.setAttribute("value", "67")
- progress.innerHTML = "67%"
- break
- case 4:
- field.className = fieldClasses + "is-success"
- progress.className = "progress is-small is-success"
- progress.setAttribute("value", "100")
- progress.innerHTML = "100%"
- break
- default:
- reset()
- }
- })
-}
-
-const generateKeyPair = function () {
- let kp = fzCrypto.generateKeyPair()
- this.el.value = kp.publicKey
-
- // XXX: Verify
- sessionStorage.setItem(kp.publicKey, kp.privateKey)
-}
-
-const clipboardCopy = function () {
- let button = this.el
- let data = button.dataset.clipboard
- button.addEventListener("click", () => {
- button.dataset.tooltip = "Copied!"
- navigator.clipboard.writeText(data)
- })
-}
-
-let Hooks = {}
-Hooks.ClipboardCopy = {
- mounted: clipboardCopy,
- updated: clipboardCopy
-}
-Hooks.HighlightCode = {
- mounted: highlightCode,
- updated: highlightCode
-}
-Hooks.FormatTimestamp = {
- mounted: formatTimestamp,
- updated: formatTimestamp
-}
-Hooks.PasswordStrength = {
- mounted: passwordStrength,
- updated: passwordStrength
-}
-Hooks.RenderConfig = {
- mounted: renderConfig,
- updated: renderConfig
-}
-Hooks.RenderQR = {
- mounted: renderQR,
- updated: renderQR
-}
-Hooks.GenerateKeyPair = {
- mounted: generateKeyPair
-}
-
-export default Hooks
diff --git a/apps/web/assets/js/live_view.js b/apps/web/assets/js/live_view.js
deleted file mode 100644
index fb3c8f2a1..000000000
--- a/apps/web/assets/js/live_view.js
+++ /dev/null
@@ -1,73 +0,0 @@
-// Encapsulates LiveView initialization
-import Hooks from "./hooks.js"
-import {Socket, Presence} from "phoenix"
-import {LiveSocket} from "phoenix_live_view"
-import {FormatTimestamp} from "./util.js"
-
-// User Socket
-const userToken = document
- .querySelector("meta[name='user-token']")
- .getAttribute("content")
-const userSocket = new Socket("/socket", {
- params: {
- token: userToken
- }
-})
-
-// Notifications
-const channelToken = document
- .querySelector("meta[name='channel-token']")
- .getAttribute("content")
-const notificationChannel =
- userSocket.channel("notification:session", {
- token: channelToken
- })
-
-// LiveView setup
-const csrfToken = document
- .querySelector("meta[name='csrf-token']")
- .getAttribute("content")
-const liveSocket = new LiveSocket(
- "/live",
- Socket,
- {
- hooks: Hooks,
- params: {
- _csrf_token: csrfToken
- }
- }
-)
-
-const toggleConnectStatus = function (info) {
- let success = document.getElementById("web-ui-connect-success")
- let error = document.getElementById("web-ui-connect-error")
- if (success && error) {
- if (userSocket.isConnected()) {
- success.classList.remove("is-hidden")
- error.classList.add("is-hidden")
- } else {
- success.classList.add("is-hidden")
- error.classList.remove("is-hidden")
- }
- }
-}
-
-userSocket.onError(toggleConnectStatus)
-userSocket.onOpen(toggleConnectStatus)
-userSocket.onClose(toggleConnectStatus)
-
-// uncomment to connect if there are any LiveViews on the page
-liveSocket.connect()
-userSocket.connect()
-
-notificationChannel.join()
- // .receive("ok", ({messages}) => console.log("catching up", messages))
- // .receive("error", ({reason}) => console.log("error", reason))
- // .receive("timeout", () => console.log("Networking issue. Still waiting..."))
-
-// expose liveSocket on window for web console debug logs and latency simulation:
-// >> liveSocket.enableDebug()
-// >> liveSocket.enableLatencySim(1000)
-
-window.liveSocket = liveSocket
-window.userSocket = userSocket
diff --git a/apps/web/assets/js/qrcode.js b/apps/web/assets/js/qrcode.js
deleted file mode 100644
index 2de05d235..000000000
--- a/apps/web/assets/js/qrcode.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const QRCode = require('qrcode')
-
-const qrError = function (el, error) {
- console.error(error)
- el.parentNode.removeChild(el)
-}
-
-const renderQR = function (data, canvas, width) {
- canvas ||= this.el
- data ||= canvas.dataset.qrdata
- width ||= canvas.dataset.size || 375
- if (canvas) {
- QRCode.toCanvas(canvas, data, {
- errorCorrectionLevel: "L",
- margin: 0,
- width: +width
- }, function (error) {
- if (error) qrError(canvas, error)
- })
- }
-}
-
-export { renderQR }
diff --git a/apps/web/assets/js/root.js b/apps/web/assets/js/root.js
deleted file mode 100644
index 87c9725f5..000000000
--- a/apps/web/assets/js/root.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import css from "../css/app.scss"
-
-/* Application fonts */
-import "@fontsource/fira-sans"
-import "@fontsource/open-sans"
-import "@fontsource/fira-mono"
-
-import "phoenix_html"
-import Hooks from "./hooks.js"
-import { Socket } from "phoenix"
-import { LiveSocket } from "phoenix_live_view"
-import "./event_listeners.js"
-
-// Basic LiveView setup
-const csrfToken = document
- .querySelector("meta[name='csrf-token']")
- .getAttribute("content")
-const liveSocket = new LiveSocket(
- "/live",
- Socket,
- {
- hooks: Hooks,
- params: {
- _csrf_token: csrfToken
- }
- }
-)
-
-liveSocket.connect()
-window.liveSocket = liveSocket
diff --git a/apps/web/assets/js/unprivileged.js b/apps/web/assets/js/unprivileged.js
deleted file mode 100644
index 456355370..000000000
--- a/apps/web/assets/js/unprivileged.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import css from "../css/app.scss"
-
-/* Application fonts */
-import "@fontsource/fira-sans"
-import "@fontsource/open-sans"
-import "@fontsource/fira-mono"
-
-import "phoenix_html"
-import "./live_view.js"
-import "./event_listeners.js"
diff --git a/apps/web/assets/js/util.js b/apps/web/assets/js/util.js
deleted file mode 100644
index 426a788e3..000000000
--- a/apps/web/assets/js/util.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import zxcvbn from "zxcvbn"
-
-const dateFormatter = new Intl.DateTimeFormat(
- 'en-US',
- {
- weekday: 'short',
- month: 'short',
- day: 'numeric',
- year: 'numeric',
- hour: 'numeric',
- minute: 'numeric'
- }
-)
-
-const FormatTimestamp = function (timestamp) {
- if (timestamp) {
- return dateFormatter.format(new Date(timestamp))
- } else {
- return "Never"
- }
-}
-
-const PasswordStrength = function (password) {
- const result = zxcvbn(password)
- return result.score
-}
-
-export { PasswordStrength, FormatTimestamp }
diff --git a/apps/web/assets/js/wg_conf.js b/apps/web/assets/js/wg_conf.js
deleted file mode 100644
index 6f509fd91..000000000
--- a/apps/web/assets/js/wg_conf.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { renderQR } from './qrcode.js'
-
-const alertPrivateKeyError = function () {
-}
-
-// 1. Load generated keypair from previous step
-// 2. Replace config PrivateKey sentinel with PrivateKey
-// 3. Set code el innerHTML to new config
-// 4. render QR code
-// 5. render download button
-const renderConfig = function () {
- const publicKey = this.el.dataset.publicKey
- const deviceName = this.el.dataset.deviceName
- if (publicKey) {
- const privateKey = sessionStorage.getItem(publicKey)
-
- // XXX: Clear all private keys
- setTimeout(() => {
- sessionStorage.removeItem(publicKey)
- }, 5000);
-
- const placeholder = document.getElementById("generating-config")
-
- if (privateKey) {
- const templateConfig = atob(this.el.dataset.config)
- const config = templateConfig.replace("REPLACE_ME", privateKey)
-
- renderDownloadButton(config, deviceName)
- renderQR(config, document.getElementById("qr-canvas"))
- renderTunnel(config)
-
- placeholder.classList.add("is-hidden")
- } else {
- placeholder.innerHTML =
- `
- Error generating configuration. Could not load private key from
- sessionStorage. Close window and try again. If the issue persists,
- please contact support@firezone.dev.
-
`
- }
- }
-}
-
-const renderDownloadButton = function (config, deviceName) {
- let button = document.getElementById("download-config")
- button.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(config))
- button.setAttribute("download", deviceName + ".conf")
- button.addEventListener('click', (event) => {
- event.stopPropagation()
- })
- button.classList.remove("is-hidden")
-}
-
-const renderTunnel = function (config) {
- let code = document.getElementById("wg-conf")
- let container = document.getElementById("wg-conf-container")
- code.innerHTML = config
- container.classList.remove("is-hidden")
-}
-
-export { renderConfig }
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.editorconfig b/apps/web/assets/local_modules/admin-one-bulma-dashboard/.editorconfig
deleted file mode 100644
index 6904b00dd..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.editorconfig
+++ /dev/null
@@ -1,15 +0,0 @@
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-insert_final_newline = true
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = true
-
-[*.md]
-trim_trailing_whitespace = false
-
-[*.{html,js,yml,yaml,js,scss,css,json}]
-indent_size = 2
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.gitattributes b/apps/web/assets/local_modules/admin-one-bulma-dashboard/.gitattributes
deleted file mode 100644
index dfe077042..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.gitattributes
+++ /dev/null
@@ -1,2 +0,0 @@
-# Auto detect text files and perform LF normalization
-* text=auto
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.gitignore b/apps/web/assets/local_modules/admin-one-bulma-dashboard/.gitignore
deleted file mode 100644
index 580a52d07..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Files
-.DS_Store
-npm-debug.log
-
-# Folders
-.idea/
-node_modules
-
-# Hot
-hot
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.tool-versions b/apps/web/assets/local_modules/admin-one-bulma-dashboard/.tool-versions
deleted file mode 100644
index a3a6ae685..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/.tool-versions
+++ /dev/null
@@ -1 +0,0 @@
-nodejs 16.6.2
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/LICENSE b/apps/web/assets/local_modules/admin-one-bulma-dashboard/LICENSE
deleted file mode 100644
index e6fce4dcd..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2019-2020 JustBoil.me (https://justboil.me)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/README.md b/apps/web/assets/local_modules/admin-one-bulma-dashboard/README.md
deleted file mode 100644
index aba35bb33..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/README.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# [Admin One HTML — Free Bulma Admin Dashboard](https://justboil.me/bulma-admin-template/one-html)
-
-[](https://justboil.me/bulma-admin-template/one-html) [](https://justboil.me/bulma-admin-template/one-html)
-
-[](https://vikdiesel.github.io/admin-one-bulma-dashboard/)
-
-**Admin One HTML** is simple, beautiful and free Bulma admin dashboard (pure HTML version).
-
-* Free under MIT License
-* Built with Bulma CSS Framework
-* Pure HTML & CSS/SCSS
-* No js framework dependencies
-* Ready-to-use CSS files
-* SCSS sources with variables
-* [Premium version](https://justboil.me/bulma-admin-template/one-html) available
-
-## Table of Contents
-
-* [Other versions](#other-versions)
-* [Description & Demo](#description--demo)
-* [Quick Start](#quick-start)
-* [Browser Support](#browser-support)
-* [Reporting Issues](#reporting-issues)
-* [Licensing](#licensing)
-* [Useful Links](#useful-links)
-
-## Other versions
-
-This is Bulma HTML/CSS/SCSS dashboard version.
-
-### Tailwind CSS
-
-
-
-
-
-
-
-
-
-
- Tailwind admin dashboard Pure HTML/CSSFree
- Tailwind admin dashboard Vue.js 3Free
-
-
-
-### Bulma
-
-More info on free & premium versions of Admin One Dashboard: https://justboil.me/bulma-admin-template/one
-
-
-
-
-
-
-
-
-
-
-
-
- Bulma admin dashboard HTML/CSS/SCSSFree | Premium
- Bulma admin dashboard Vue.js BuefyFree | Premium
- Bulma admin dashboard Nuxt.js BuefyFree | Premium
- Bulma admin dashboard LaravelFree
-
-
-
-## Description & Demo
-
-#### Description
-
-https://justboil.me/bulma-admin-template/one-html
-
-#### Free Dashboard Demo
-
-https://vikdiesel.github.io/admin-one-bulma-dashboard/
-
-#### Premium Dashboard Demo
-
-https://admin-one-html.justboil.me
-
-## Quick Start
-
-#### Get the repo
-
-* [Create new repo](https://github.com/vikdiesel/admin-one-bulma-dashboard/generate) from this template
-* Clone the repo on GitHub
-* … or [download .zip](https://github.com/vikdiesel/admin-vue-bulma-dashboard/archive/master.zip) from GitHub
-
-#### HTML & CSS
-
-Check `demo` directory.
-
-#### npm tools
-
-##### Install
-
-`cd` to project's dir and run `npm install`
-
-##### Build
-
-`npm run build` to rebuild `demo` from sources in `src` directory
-
-## Browser Support
-
-We try to make sure Dashboard works well in the latest versions of all major browsers
-
-
-
-## Reporting Issues
-
-JustBoil's free items are limited to community support on GitHub.
-
-The issue list is reserved exclusively for bug reports and feature requests. That means we do not accept usage questions. If you open an issue that does not conform to the requirements, it will be closed.
-
-1. Make sure that you are using the latest version of the Dashboard. Issues for outdated versions are irrelevant
-2. Provide steps to reproduce
-3. Provide an expected behavior
-4. Describe what is actually happening
-5. Platform, Browser & version as some issues may be browser specific
-
-## Licensing
-
-- Copyright © 2019-2020 JustBoil.me (https://justboil.me)
-- Licensed under MIT
-
-## Useful Links
-
-- [JustBoil.me](https://justboil.me)
-- [Bulma](https://bulma.io)
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/package.json b/apps/web/assets/local_modules/admin-one-bulma-dashboard/package.json
deleted file mode 100644
index 4169e0ebf..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "admin-one-bulma-dashboard",
- "version": "1.5.5",
- "description": "Admin One Bulma Admin Dashboard (HTML version)",
- "homepage": "https://justboil.me/bulma-admin-template/one-html",
- "keywords": [
- "sass",
- "bulma",
- "css",
- "html",
- "dashboard"
- ],
- "author": "Viktor Kuzhelnyi (https://justboil.me)",
- "contributors": [
- "Jamil Bou Kheir "
- ],
- "license": "MIT",
- "dependencies": {
- "bulma": "^0.9.0",
- "bulma-checkbox": "^1.1.1",
- "bulma-radio": "^1.1.1",
- "bulma-responsive-tables": "^1.2.3",
- "bulma-switch-control": "^1.1.1",
- "bulma-upload-control": "^1.2.0",
- "node-sass": "^7.0.1"
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/js/main.js b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/js/main.js
deleted file mode 100644
index 933eb62e7..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/js/main.js
+++ /dev/null
@@ -1,66 +0,0 @@
-
-/* Aside: submenus toggle */
-Array.from(document.getElementsByClassName('menu is-menu-main')).forEach(el => {
- Array.from(el.getElementsByClassName('has-dropdown-icon')).forEach(elA => {
- elA.addEventListener('click', e => {
- const dropdownIcon = e.currentTarget
- .getElementsByClassName('dropdown-icon')[0]
- .getElementsByClassName('mdi')[0]
-
- e.currentTarget.parentNode.classList.toggle('is-active')
- dropdownIcon.classList.toggle('mdi-plus')
- dropdownIcon.classList.toggle('mdi-minus')
- })
- })
-})
-
-/* Aside Mobile toggle */
-Array.from(document.getElementsByClassName('jb-aside-mobile-toggle')).forEach(el => {
- el.addEventListener('click', e => {
- const dropdownIcon = e.currentTarget
- .getElementsByClassName('icon')[0]
- .getElementsByClassName('mdi')[0]
-
- document.documentElement.classList.toggle('has-aside-mobile-expanded')
- dropdownIcon.classList.toggle('mdi-forwardburger')
- dropdownIcon.classList.toggle('mdi-backburger')
- })
-})
-
-/* NavBar menu mobile toggle */
-Array.from(document.getElementsByClassName('jb-navbar-menu-toggle')).forEach(el => {
- el.addEventListener('click', e => {
- const dropdownIcon = e.currentTarget
- .getElementsByClassName('icon')[0]
- .getElementsByClassName('mdi')[0]
-
- document.getElementById(e.currentTarget.getAttribute('data-target')).classList.toggle('is-active')
- dropdownIcon.classList.toggle('mdi-dots-vertical')
- dropdownIcon.classList.toggle('mdi-close')
- })
-})
-
-/* Modal: open */
-Array.from(document.getElementsByClassName('jb-modal')).forEach(el => {
- el.addEventListener('click', e => {
- const modalTarget = e.currentTarget.getAttribute('data-target')
-
- document.getElementById(modalTarget).classList.add('is-active')
- document.documentElement.classList.add('is-clipped')
- })
-});
-
-/* Modal: close */
-Array.from(document.getElementsByClassName('jb-modal-close')).forEach(el => {
- el.addEventListener('click', e => {
- e.currentTarget.closest('.modal').classList.remove('is-active')
- document.documentElement.classList.remove('is-clipped')
- })
-})
-
-/* Notification dismiss */
-Array.from(document.getElementsByClassName('jb-notification-dismiss')).forEach(el => {
- el.addEventListener('click', e => {
- e.currentTarget.closest('.notification').classList.add('is-hidden')
- })
-})
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_aside.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_aside.scss
deleted file mode 100644
index 0764fa3a8..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_aside.scss
+++ /dev/null
@@ -1,161 +0,0 @@
-@include desktop {
- html {
- &.has-aside-left {
- &.has-aside-expanded {
- nav.navbar, body {
- padding-left: $aside-width;
- }
- }
- nav.navbar, body {
- @include transition(padding-left);
- }
- aside.is-placed-left {
- display: block;
- }
- }
- }
-
- aside.aside.is-expanded {
- width: $aside-width;
-
- .menu-list {
- @include icon-with-update-mark($aside-icon-width);
-
- span.menu-item-label {
- display: inline-block;
- }
-
- li.is-active {
- ul {
- display: block;
- }
- }
- }
- }
-}
-
-aside.aside {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- z-index: 40;
- height: 100vh;
- padding: 0;
- box-shadow: $aside-box-shadow;
- background: $aside-background-color;
-
- .aside-tools {
- display: flex;
- flex-direction: row;
- width: 100%;
- background-color: $aside-tools-background-color;
- color: $aside-tools-color;
- line-height: $navbar-height;
- height: $navbar-height;
- padding-left: $default-padding * .5;
- flex: 1;
-
- .icon {
- margin-right: $default-padding * .5;
- }
- }
-
- .menu-list {
- li {
- a {
- &.has-dropdown-icon {
- position: relative;
- padding-right: $aside-icon-width;
-
- .dropdown-icon {
- position: absolute;
- top: $size-base * .5;
- right: 0;
- }
- }
- }
- ul {
- display: none;
- border-left: 0;
- background-color: darken($base-color, 2.5%);
- padding-left: 0;
- margin: 0 0 $default-padding * .5;
-
- li {
- a {
- padding: $default-padding * .5 0 $default-padding * .5 $default-padding * .5;
- font-size: $aside-submenu-font-size;
-
- &.has-icon {
- padding-left: 0;
- }
- &.is-active {
- &:not(:hover) {
- background: transparent;
- }
- }
- }
- }
- }
- }
- }
-
- .menu-label {
- padding: 0 $default-padding * .5;
- margin-top: $default-padding * .5;
- margin-bottom: $default-padding * .5;
- }
-
-}
-
-@include touch {
- #app, nav.navbar {
- @include transition(margin-left);
- }
- aside.aside {
- @include transition(left);
- }
- html.has-aside-mobile-transition {
- body {
- overflow-x: hidden;
- }
- body, #app, nav.navbar {
- width: 100vw;
- }
- aside.aside {
- width: $aside-mobile-width;
- display: block;
- left: $aside-mobile-width * -1;
-
- .image {
- img {
- max-width: $aside-mobile-width * .33;
- }
- }
-
- .menu-list {
- li.is-active {
- ul {
- display: block;
- }
- }
- a {
- @include icon-with-update-mark($aside-icon-width);
-
- span.menu-item-label {
- display: inline-block;
- }
- }
- }
- }
- }
- html.has-aside-mobile-expanded {
- #app, nav.navbar {
- margin-left: $aside-mobile-width;
- }
- aside.aside {
- left: 0;
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_card.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_card.scss
deleted file mode 100644
index bf1efb113..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_card.scss
+++ /dev/null
@@ -1,48 +0,0 @@
-.card:not(:last-child) {
- margin-bottom: $default-padding;
-}
-
-.card {
- border-radius: $radius-large;
- border: $card-border;
-
- &.has-table {
- .card-content {
- padding: 0;
- }
- .b-table {
- border-radius: $radius-large;
- overflow: hidden;
- }
- }
-
- &.is-card-widget {
- .card-content {
- padding: $default-padding * .5;
- }
- }
-
- .card-header {
- border-bottom: 1px solid $base-color-light;
- }
-
- .card-content {
- hr {
- margin-left: $card-content-padding * -1;
- margin-right: $card-content-padding * -1;
- }
- }
-
- .is-widget-icon {
- .icon {
- width: 5rem;
- height: 5rem;
- }
- }
-
- .is-widget-label {
- .subtitle {
- color: $grey;
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_footer.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_footer.scss
deleted file mode 100644
index 4e6c4ac6e..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_footer.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-footer.footer {
- .logo {
- img {
- width: auto;
- height: $footer-logo-height;
- }
- }
-}
-
-@include mobile {
- .footer-copyright {
- text-align: center;
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_form.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_form.scss
deleted file mode 100644
index d224e2bdf..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_form.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.field {
- &.has-check {
- .field-body {
- margin-top: $default-padding * .125;
- }
- }
- .control {
- .mdi-24px.mdi-set, .mdi-24px.mdi:before {
- font-size: inherit;
- }
- }
-}
-.upload {
- .upload-draggable {
- display: block;
- }
-}
-
-.input, .textarea, select {
- box-shadow: none;
-
- &:focus, &:active {
- box-shadow: none!important;
- }
-}
-
-.switch input[type=checkbox]+.check:before {
- box-shadow: none;
-}
-
-.switch, .b-checkbox.checkbox {
- input[type=checkbox] {
- &:focus + .check, &:focus:checked + .check {
- box-shadow: none!important;
- }
- }
-}
-
-.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
- &+.check {
- border: $checkbox-border;
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_hero-bar.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_hero-bar.scss
deleted file mode 100644
index 0a6781a5b..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_hero-bar.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-section.hero.is-hero-bar {
- background-color: $hero-bar-background;
- border-bottom: $light-border;
-
- .hero-body {
- padding: $default-padding;
-
- .level-item {
- &.is-hero-avatar-item {
- margin-right: $default-padding;
- }
-
- > div > .level {
- margin-bottom: $default-padding * .5;
- }
-
- .subtitle + p {
- margin-top: $default-padding * .5;
- }
- }
-
- .button {
- &.is-hero-button {
- background-color: rgba($white, .5);
- font-weight: 300;
- @include transition(background-color);
-
- &:hover {
- background-color: $white;
- }
- }
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_main-section.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_main-section.scss
deleted file mode 100644
index e55978d02..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_main-section.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-section.section.is-main-section {
- padding-top: $default-padding;
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_misc.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_misc.scss
deleted file mode 100644
index 3ab40b4f4..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_misc.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.is-user-avatar {
- &.has-max-width {
- max-width: $size-base * 7;
- }
-
- &.is-aligned-center {
- margin: 0 auto;
- }
-
- img {
- margin: 0 auto;
- border-radius: $radius-rounded;
- }
-}
-
-.icon.has-update-mark {
- position: relative;
-
- &:after {
- content: '';
- width: $icon-update-mark-size;
- height: $icon-update-mark-size;
- position: absolute;
- top: 1px;
- right: 1px;
- background-color: $icon-update-mark-color;
- border-radius: $radius-rounded;
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_mixins.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_mixins.scss
deleted file mode 100644
index d5b98464f..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_mixins.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-@mixin transition($t) {
- transition: $t 250ms ease-in-out 50ms;
-}
-
-@mixin icon-with-update-mark ($icon-base-width) {
- .icon {
- width: $icon-base-width;
-
- &.has-update-mark:after {
- right: ($icon-base-width / 2) - .85;
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_modal.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_modal.scss
deleted file mode 100644
index 8323c52e4..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_modal.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.modal-card {
- width: $modal-card-width;
-}
-
-.modal-card-foot {
- background-color: $modal-card-foot-background-color;
-}
-
-@include mobile {
- .modal .modal-card {
- width: $modal-card-width-mobile;
- margin: 0 auto;
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_nav-bar.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_nav-bar.scss
deleted file mode 100644
index a463dfd48..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_nav-bar.scss
+++ /dev/null
@@ -1,123 +0,0 @@
-nav.navbar {
- box-shadow: $navbar-box-shadow;
-
- .navbar-item {
- &.has-user-avatar {
- .is-user-avatar {
- margin-right: $default-padding * .5;
- display: inline-flex;
- width: $navbar-avatar-size;
- height: $navbar-avatar-size;
- }
- }
-
- &.has-divider {
- border-right: $navbar-divider-border;
- }
-
- &.no-left-space {
- padding-left: 0;
- }
-
- &.has-dropdown {
- padding-right: 0;
- padding-left: 0;
-
- .navbar-link {
- padding-right: $navbar-item-h-padding;
- padding-left: $navbar-item-h-padding;
- }
- }
-
- &.has-control {
- padding-top: 0;
- padding-bottom: 0;
- }
-
- .control {
- .input {
- color: $navbar-input-color;
- border: 0;
- box-shadow: none;
- background: transparent;
-
- &::placeholder {
- color: $navbar-input-placeholder-color;
- }
- }
- }
- }
-}
-
-@include touch {
- nav.navbar {
- display: flex;
- padding-right: 0;
-
- .navbar-brand {
- flex: 1;
-
- &.is-right {
- flex: none;
- }
- }
-
- .navbar-item {
- &.no-left-space-touch {
- padding-left: 0;
- }
- }
-
- .navbar-menu {
- position: absolute;
- width: 100vw;
- padding-top: 0;
- top: $navbar-height;
- left: 0;
-
- .navbar-item {
- .icon:first-child {
- margin-right: $default-padding * .5;
- }
-
- &.has-dropdown {
- >.navbar-link {
- background-color: $white-ter;
- .icon:last-child {
- display: none;
- }
- }
- }
-
- &.has-user-avatar {
- >.navbar-link {
- display: flex;
- align-items: center;
- padding-top: $default-padding * .5;
- padding-bottom: $default-padding * .5;
- }
- }
- }
- }
- }
-}
-
-@include desktop {
- nav.navbar {
- .navbar-item {
- padding-right: $navbar-item-h-padding;
- padding-left: $navbar-item-h-padding;
-
- &:not(.is-desktop-icon-only) {
- .icon:first-child {
- margin-right: $default-padding * .5;
- }
- }
- &.is-desktop-icon-only {
- span:not(.icon) {
- display: none;
- }
- }
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_table.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_table.scss
deleted file mode 100644
index 1128eb398..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_table.scss
+++ /dev/null
@@ -1,152 +0,0 @@
-table.table {
- thead {
- th {
- border-bottom-width: 1px;
- }
- }
-
- td, th {
- &.checkbox-cell {
- .b-checkbox.checkbox:not(.button) {
- margin-right: 0;
- width: 20px;
-
- .control-label {
- display: none;
- padding: 0;
- }
- }
- }
- }
-
- td {
- .image {
- margin: 0 auto;
- width: $table-avatar-size;
- height: $table-avatar-size;
- }
-
- &.is-progress-col {
- min-width: 5rem;
- vertical-align: middle;
- }
- }
-}
-
-.b-table {
- .table {
- border: 0;
- border-radius: 0;
- }
-
- /* This stylizes buefy's pagination */
- .table-wrapper {
- margin-bottom: 0;
- }
-
- .table-wrapper + .level {
- padding: $notification-padding;
- padding-left: $card-content-padding;
- padding-right: $card-content-padding;
- margin: 0;
- border-top: $base-color-light;
- background: $notification-background-color;
-
- .pagination-link {
- background: $button-background-color;
- color: $button-color;
- border-color: $button-border-color;
-
- &.is-current {
- border-color: $button-active-border-color;
- }
- }
-
- .pagination-previous, .pagination-next, .pagination-link {
- border-color: $button-border-color;
- color: $base-color;
-
- &[disabled] {
- background-color: transparent;
- }
- }
- }
-}
-
-@include mobile {
- .card {
- &.has-table {
- .b-table {
- .table-wrapper + .level {
- .level-left + .level-right {
- margin-top: 0;
- }
- }
- }
- }
- &.has-mobile-sort-spaced {
- .b-table {
- .field.table-mobile-sort {
- padding-top: $default-padding * .5;
- }
- }
- }
- }
- .b-table {
- .field.table-mobile-sort {
- padding: 0 $default-padding * .5;
- }
-
- .table-wrapper.has-mobile-cards {
- tr {
- box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
- margin-bottom: 3px!important;
- }
- td {
- &.is-progress-col {
- span, progress {
- display: flex;
- width: 45%;
- align-items: center;
- align-self: center;
- }
- }
-
- &.checkbox-cell, &.is-image-cell {
- border-bottom: 0!important;
- }
-
- &.checkbox-cell, &.is-actions-cell {
- &:before {
- display: none;
- }
- }
-
- &.has-no-head-mobile {
- &:before {
- display: none;
- }
-
- span {
- display: block;
- width: 100%;
- }
-
- &.is-progress-col {
- progress {
- width: 100%;
- }
- }
-
- &.is-image-cell {
- .image {
- width: $table-avatar-size-mobile;
- height: auto;
- margin: 0 auto $default-padding * .25;
- }
- }
- }
- }
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_theme-firezone.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_theme-firezone.scss
deleted file mode 100644
index 53eb480af..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_theme-firezone.scss
+++ /dev/null
@@ -1,154 +0,0 @@
-/* Firezone theme, using palette from https://app.getparade.com/firezoneinc/style-guide */
-
-/* Common variables */
-$primary-main: #FF7300;
-$primary-000: #331700;
-$primary-100: #5c2900;
-$primary-200: #7f3900;
-$primary-300: #c25700;
-$primary-400: #ff7605;
-$primary-500: #ff9a47;
-$primary-600: #ffbc85;
-$primary-700: #ffddc2;
-$primary-800: #fff1e5;
-$primary-900: #fff9f5;
-$accent-main: #5E00D6;
-$accent-000: #160033;
-$accent-100: #28005c;
-$accent-200: #37007f;
-$accent-300: #3400c2;
-$accent-400: #4805ff;
-$accent-500: #7847ff;
-$accent-600: #a585ff;
-$accent-700: #d2c2ff;
-$accent-800: #ece5ff;
-$accent-900: #f8f5ff;
-$interface-main: #1B140E;
-$interface-000: #1b140e;
-$interface-100: #4c3e33;
-$interface-200: #766a60;
-$interface-300: #90867f;
-$interface-400: #a7a3a0;
-$interface-500: #c7c4c2;
-$interface-600: #dfdedd;
-$interface-700: #ebebea;
-$interface-800: #f8f7f7;
-$interface-900: #fcfcfc;
-
-/* We'll need some initial vars to use here */
-@import "node_modules/bulma/sass/utilities/initial-variables";
-
-/* Base: Size */
-$size-base: 1rem;
-$default-padding: $size-base * 1.5;
-
-/* Default font */
-$family-sans-serif: "Fira Sans", sans-serif;
-$family-primary: $family-sans-serif;
-
-/* Monospace font */
-$family-monospace: "Fira Mono", monospace;
-
-/* Text color */
-$text: $interface-100;
-$text-light: $interface-200;
-$text-strong: $interface-000;
-
-/* Base color */
-$base-color: $interface-main;
-$base-color-light: rgba(24, 28, 33, .06);
-
-/* General overrides */
-/* See https://coolors.co/331700-990c00-ff0000 */
-$info: $accent-300;
-$primary: $accent-main;
-$success: #80B900;
-$warning: #FFB900;
-$danger: #990C00;
-$body-background-color: $interface-800;
-$link: $accent-300;
-$link-visited: $accent-600;
-$light-border: 1px solid $base-color-light;
-$hr-height: 1px;
-
-/* NavBar: specifics */
-$navbar-input-color: $interface-100;
-$navbar-input-placeholder-color: $interface-600;
-$navbar-box-shadow: 0 1px 0 rgba(24,28,33,.04);
-$navbar-divider-border: 1px solid rgba($interface-600, .25);
-$navbar-item-h-padding: $default-padding * .75;
-$navbar-avatar-size: 1.75rem;
-
-/* Aside: Bulma override */
-$menu-item-radius: 0;
-$menu-list-link-padding: $size-base * .5 0;
-$menu-label-color: $interface-300;
-$menu-item-color: $interface-400;
-$menu-item-hover-color: $interface-800;
-$menu-item-hover-background-color: darken($base-color, 3.5%);
-$menu-item-active-color: $interface-800;
-$menu-item-active-background-color: darken($base-color, 2.5%);
-
-/* Aside: specifics */
-$aside-width: $size-base * 14;
-$aside-mobile-width: $size-base * 15;
-$aside-icon-width: $size-base * 3;
-$aside-submenu-font-size: $size-base * .95;
-$aside-box-shadow: none;
-$aside-background-color: $base-color;
-$aside-tools-background-color: darken($aside-background-color, 10%);
-$aside-tools-color: $interface-800;
-
-/* Title Bar: specifics */
-$title-bar-color: $interface-400;
-$title-bar-active-color: $interface-100;
-
-/* Hero Bar: specifics */
-$hero-bar-background: $interface-800;
-
-/* Card: Bulma override */
-$card-shadow: none;
-$card-header-shadow: none;
-
-/* Card: specifics */
-$card-border: 1px solid $base-color-light;
-$card-header-border-bottom-color: $base-color-light;
-
-/* Table: Bulma override */
-$table-cell-border: 1px solid $interface-700;
-
-/* Table: specifics */
-$table-avatar-size: $size-base * 1.5;
-$table-avatar-size-mobile: 25vw;
-
-/* Form */
-$checkbox-border: 1px solid $base-color;
-
-/* Modal card: Bulma override */
-$modal-card-head-background-color: $interface-800;
-$modal-card-title-size: $size-base;
-$modal-card-body-padding: $default-padding 20px;
-$modal-card-head-border-bottom: 1px solid $interface-800;
-$modal-card-foot-border-top: 0;
-
-/* Modal card: specifics */
-$modal-card-width: 40vw;
-$modal-card-width-mobile: 90vw;
-$modal-card-foot-background-color: $interface-800;
-
-/* Notification: Bulma override */
-$notification-padding: $default-padding * .75 $default-padding;
-
-/* Footer: Bulma override */
-$footer-background-color: $white;
-$footer-padding: $default-padding * .33 $default-padding;
-
-/* Footer: specifics */
-$footer-logo-height: $size-base * 2;
-
-/* Progress: Bulma override */
-$progress-bar-background-color: $interface-600;
-
-/* Icon: specifics */
-$icon-update-mark-size: $size-base * .5;
-$icon-update-mark-color: $primary-main;
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_tiles.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_tiles.scss
deleted file mode 100644
index b6327eb22..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_tiles.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.is-tiles-wrapper {
- margin-bottom: $default-padding;
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_title-bar.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_title-bar.scss
deleted file mode 100644
index a56b44b70..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/_title-bar.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-section.section.is-title-bar {
- padding: $default-padding;
- border-bottom: $light-border;
-
- ul {
- li {
- display: inline-block;
- padding: 0 $default-padding * .5 0 0;
- font-size: $default-padding;
- color: $title-bar-color;
-
- &:after {
- display: inline-block;
- content: '/';
- padding-left: $default-padding * .5;
- }
-
- &:last-child {
- padding-right: 0;
- font-weight: 900;
- color: $title-bar-active-color;
-
- &:after {
- display: none;
- }
- }
- }
- }
-}
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/libs/_all.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/libs/_all.scss
deleted file mode 100644
index 7a0592f65..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/libs/_all.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-@import "node_modules/bulma-radio/bulma-radio";
-@import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
-@import "node_modules/bulma-checkbox/bulma-checkbox";
-@import "node_modules/bulma-switch-control/bulma-switch-control";
-@import "node_modules/bulma-upload-control/bulma-upload-control";
-
-/* Bulma */
-@import "node_modules/bulma/bulma";
diff --git a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/main.scss b/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/main.scss
deleted file mode 100644
index 5b03b1d24..000000000
--- a/apps/web/assets/local_modules/admin-one-bulma-dashboard/src/scss/main.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-/* Theme style (colors & sizes) */
-@import "theme-firezone";
-
-/* Core Libs & Lib configs */
-@import "libs/all";
-
-/* Mixins */
-@import "mixins";
-
-/* Theme components */
-@import "nav-bar";
-@import "aside";
-@import "title-bar";
-@import "hero-bar";
-@import "card";
-@import "table";
-@import "tiles";
-@import "form";
-@import "main-section";
-@import "modal";
-@import "footer";
-@import "misc";
diff --git a/apps/web/assets/package.json b/apps/web/assets/package.json
deleted file mode 100644
index 42cbc0309..000000000
--- a/apps/web/assets/package.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "name": "firezone",
- "version": "0.1.0",
- "repository": {
- "type": "git",
- "url": "https://github.com/firezone/firezone.git"
- },
- "license": "Apache-2.0",
- "scripts": {
- "deploy": "node esbuild.js prod",
- "watch": "node esbuild.js dev"
- },
- "engines": {
- "node": ">= 14.0.0"
- },
- "dependencies": {
- "buffer": "^6.0.3",
- "buffer-from": "^1.1.1",
- "glob": "^7.1.7",
- "highlight.js": "^11.4.0",
- "phoenix": "file:../../../deps/phoenix",
- "phoenix_html": "file:../../../deps/phoenix_html",
- "phoenix_live_view": "file:../../../deps/phoenix_live_view",
- "qrcode": "^1.3.3",
- "tweetnacl": "^1.0.3",
- "tweetnacl-util": "^0.15.1"
- },
- "devDependencies": {
- "@creativebulma/bulma-tooltip": "^1.2.0",
- "@fontsource/fira-mono": "^4.5.1",
- "@fontsource/fira-sans": "^4.5.1",
- "@fontsource/open-sans": "^4.5.3",
- "@fortawesome/fontawesome-free": "^5.15.3",
- "@mdi/font": "^6.5.95",
- "admin-one-bulma-dashboard": "file:local_modules/admin-one-bulma-dashboard",
- "esbuild": "^0.14.43",
- "esbuild-sass-plugin": "^2.2.6",
- "zxcvbn": "^4.4.2"
- }
-}
diff --git a/apps/web/assets/postcss.config.js b/apps/web/assets/postcss.config.js
deleted file mode 100644
index 83dfc31cb..000000000
--- a/apps/web/assets/postcss.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- plugins: [
- require('autoprefixer'),
- ]
-}
diff --git a/apps/web/assets/tailwind.config.js b/apps/web/assets/tailwind.config.js
new file mode 100644
index 000000000..e3bf24134
--- /dev/null
+++ b/apps/web/assets/tailwind.config.js
@@ -0,0 +1,26 @@
+// See the Tailwind configuration guide for advanced usage
+// https://tailwindcss.com/docs/configuration
+
+const plugin = require("tailwindcss/plugin")
+
+module.exports = {
+ content: [
+ "./js/**/*.js",
+ "../lib/*_web.ex",
+ "../lib/*_web/**/*.*ex"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ brand: "#FD4F00",
+ }
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
+ plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
+ ]
+}
diff --git a/apps/web/assets/vendor/topbar.js b/apps/web/assets/vendor/topbar.js
new file mode 100644
index 000000000..41957274d
--- /dev/null
+++ b/apps/web/assets/vendor/topbar.js
@@ -0,0 +1,165 @@
+/**
+ * @license MIT
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function (delay) {
+ if (showing) return;
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/apps/web/assets/yarn.lock b/apps/web/assets/yarn.lock
index dc71a42bd..fb57ccd13 100644
--- a/apps/web/assets/yarn.lock
+++ b/apps/web/assets/yarn.lock
@@ -2,2300 +2,3 @@
# yarn lockfile v1
-"@babel/code-frame@^7.0.0":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
- integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
- dependencies:
- "@babel/highlight" "^7.18.6"
-
-"@babel/helper-validator-identifier@^7.18.6":
- version "7.19.1"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
- integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
-
-"@babel/highlight@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
- integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
- dependencies:
- "@babel/helper-validator-identifier" "^7.18.6"
- chalk "^2.0.0"
- js-tokens "^4.0.0"
-
-"@creativebulma/bulma-tooltip@^1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@creativebulma/bulma-tooltip/-/bulma-tooltip-1.2.0.tgz#84dcdd59d94c09c2975fadbec1d3d765ae29c471"
- integrity sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==
-
-"@esbuild/android-arm@0.15.18":
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80"
- integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==
-
-"@esbuild/linux-loong64@0.14.54":
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
- integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
-
-"@esbuild/linux-loong64@0.15.18":
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239"
- integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==
-
-"@fontsource/fira-mono@^4.5.1":
- version "4.5.10"
- resolved "https://registry.yarnpkg.com/@fontsource/fira-mono/-/fira-mono-4.5.10.tgz#443be4b2b4fc6e685b88431fcfdaf8d5f5639bbf"
- integrity sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==
-
-"@fontsource/fira-sans@^4.5.1":
- version "4.5.10"
- resolved "https://registry.yarnpkg.com/@fontsource/fira-sans/-/fira-sans-4.5.10.tgz#63a6dacfa482017a840b40e9c2cfefb187a0e515"
- integrity sha512-4Edj+GA0LYSqfXOvdTwVGmCShT8Ycd8bKzdfzM302n+I6Hsg6h3gBkBeNgN19PhkcngDznZyHv3EkyrKqvMTGw==
-
-"@fontsource/open-sans@^4.5.3":
- version "4.5.13"
- resolved "https://registry.yarnpkg.com/@fontsource/open-sans/-/open-sans-4.5.13.tgz#2c99e4c055d129a75f14c11b77d040282e0388b8"
- integrity sha512-/UzqP7ZFk145XAq8KG4pvFPP7UQhtreDGXgqXZjagCDreKxcrhwn/x7DYz9rPcycWkLUVApIybcoczGZiM0cRg==
-
-"@fortawesome/fontawesome-free@^5.15.3":
- version "5.15.4"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
- integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==
-
-"@gar/promisify@^1.0.1":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
- integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
-
-"@mdi/font@^6.5.95":
- version "6.9.96"
- resolved "https://registry.yarnpkg.com/@mdi/font/-/font-6.9.96.tgz#c68da7e0895885dd09e60dc08c5ecc0d77f67efb"
- integrity sha512-z3QVZStyHVwkDsFR7A7F2PIvZJPWgdSFw4BEEy2Gc9HUN5NfK9mGbjgaYClRcbMWiYEV45srmiYtczmBtCqR8w==
-
-"@npmcli/fs@^1.0.0":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
- integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==
- dependencies:
- "@gar/promisify" "^1.0.1"
- semver "^7.3.5"
-
-"@npmcli/move-file@^1.0.1":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
- integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
- dependencies:
- mkdirp "^1.0.4"
- rimraf "^3.0.2"
-
-"@tootallnate/once@1":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
- integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
-
-"@types/minimist@^1.2.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
- integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
-
-"@types/normalize-package-data@^2.4.0":
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
- integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
-
-abbrev@1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
- integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-
-"admin-one-bulma-dashboard@file:local_modules/admin-one-bulma-dashboard":
- version "1.5.5"
- dependencies:
- bulma "^0.9.0"
- bulma-checkbox "^1.1.1"
- bulma-radio "^1.1.1"
- bulma-responsive-tables "^1.2.3"
- bulma-switch-control "^1.1.1"
- bulma-upload-control "^1.2.0"
- node-sass "^7.0.1"
-
-agent-base@6, agent-base@^6.0.2:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
- integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
- dependencies:
- debug "4"
-
-agentkeepalive@^4.1.3:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717"
- integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==
- dependencies:
- debug "^4.1.0"
- depd "^1.1.2"
- humanize-ms "^1.2.1"
-
-aggregate-error@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
- integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
- dependencies:
- clean-stack "^2.0.0"
- indent-string "^4.0.0"
-
-ajv@^6.12.3:
- version "6.12.6"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
- integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
- dependencies:
- fast-deep-equal "^3.1.1"
- fast-json-stable-stringify "^2.0.0"
- json-schema-traverse "^0.4.1"
- uri-js "^4.2.2"
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
- integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
- dependencies:
- color-convert "^1.9.0"
-
-ansi-styles@^4.0.0, ansi-styles@^4.1.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-anymatch@~3.1.2:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
- integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
- dependencies:
- normalize-path "^3.0.0"
- picomatch "^2.0.4"
-
-"aproba@^1.0.3 || ^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
- integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
-
-are-we-there-yet@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
- integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
- dependencies:
- delegates "^1.0.0"
- readable-stream "^3.6.0"
-
-are-we-there-yet@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
- integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==
- dependencies:
- delegates "^1.0.0"
- readable-stream "^3.6.0"
-
-arrify@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
- integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
-
-asn1@~0.2.3:
- version "0.2.6"
- resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
- integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
- dependencies:
- safer-buffer "~2.1.0"
-
-assert-plus@1.0.0, assert-plus@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
- integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
-
-async-foreach@^0.1.3:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
- integrity sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==
-
-asynckit@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
- integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
-
-aws-sign2@~0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
- integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==
-
-aws4@^1.8.0:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
- integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@^1.3.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-bcrypt-pbkdf@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
- integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
- dependencies:
- tweetnacl "^0.14.3"
-
-binary-extensions@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
- integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-braces@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
- dependencies:
- fill-range "^7.0.1"
-
-buffer-from@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
- integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-
-buffer@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
- integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
- dependencies:
- base64-js "^1.3.1"
- ieee754 "^1.2.1"
-
-bulma-checkbox@^1.1.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/bulma-checkbox/-/bulma-checkbox-1.2.1.tgz#7b77826babf07b98a01ec92cdcdbd980119137d5"
- integrity sha512-Ad7kSzwYwHLYyow92IJPz9jgolDDo5ivlFdSBe7W4LR9WnLt/Gd2iE07m3uhoU/g37oIZcMHNC33ZxJKqAuSzQ==
- dependencies:
- bulma "^0.9.0"
-
-bulma-radio@^1.1.1:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/bulma-radio/-/bulma-radio-1.2.0.tgz#7c0276706b34be92641f1eecda92fcd5f404675c"
- integrity sha512-rIzqALGakpKf9Eju4sGMt2Pwnn7X+AdYh6itjsCxLCJ/Ext4Cdd/M7uevQlXDy0MSwrQBMBLR8buSToBCuI+zA==
- dependencies:
- bulma "^0.9.0"
-
-bulma-responsive-tables@^1.2.3:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/bulma-responsive-tables/-/bulma-responsive-tables-1.2.5.tgz#06a493d7c675ea20b49e1d347452dde8bf9454a1"
- integrity sha512-8/qYiv21cJnGYMkjIF52iKCV/B6XIswu58Vwi3/TS+wLavvA7OFXhBy0quxOnqPNqnovHly2dTCyVCqHLJU7Sg==
- dependencies:
- bulma "^0.9.0"
-
-bulma-switch-control@^1.1.1:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/bulma-switch-control/-/bulma-switch-control-1.2.2.tgz#236277049783b6e11ec0620239b02388490a1cc2"
- integrity sha512-1eHlga1Z4RBRU6DIxNiwb6+I9n9vDkj9/MmwS4pL68P7STE1vbwRutxh9oFeFWuxLXGNLILJEXJXiwyEjT9upw==
- dependencies:
- bulma "^0.9.0"
-
-bulma-upload-control@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/bulma-upload-control/-/bulma-upload-control-1.2.0.tgz#b44c497ce64deef3f01012d43c4085f43c7e510b"
- integrity sha512-2raueVPVoG3KjHH+7Aok44nGSPIl76qzdkLKX/ziHAOwbiXBrlEYHXca8Hk0UDa0KElLiPT6Eb2Cvz+8FFUwBw==
- dependencies:
- bulma "^0.9.0"
-
-bulma@^0.9.0:
- version "0.9.4"
- resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.4.tgz#0ca8aeb1847a34264768dba26a064c8be72674a1"
- integrity sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==
-
-cacache@^15.2.0:
- version "15.3.0"
- resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
- integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
- dependencies:
- "@npmcli/fs" "^1.0.0"
- "@npmcli/move-file" "^1.0.1"
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- glob "^7.1.4"
- infer-owner "^1.0.4"
- lru-cache "^6.0.0"
- minipass "^3.1.1"
- minipass-collect "^1.0.2"
- minipass-flush "^1.0.5"
- minipass-pipeline "^1.2.2"
- mkdirp "^1.0.3"
- p-map "^4.0.0"
- promise-inflight "^1.0.1"
- rimraf "^3.0.2"
- ssri "^8.0.1"
- tar "^6.0.2"
- unique-filename "^1.1.1"
-
-camelcase-keys@^6.2.2:
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
- integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
- dependencies:
- camelcase "^5.3.1"
- map-obj "^4.0.0"
- quick-lru "^4.0.1"
-
-camelcase@^5.0.0, camelcase@^5.3.1:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
- integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
-caseless@~0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
- integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
-
-chalk@^2.0.0:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
- integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
-chalk@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
- integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
- dependencies:
- ansi-styles "^4.1.0"
- supports-color "^7.1.0"
-
-"chokidar@>=3.0.0 <4.0.0":
- version "3.5.3"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
- integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
- dependencies:
- anymatch "~3.1.2"
- braces "~3.0.2"
- glob-parent "~5.1.2"
- is-binary-path "~2.1.0"
- is-glob "~4.0.1"
- normalize-path "~3.0.0"
- readdirp "~3.6.0"
- optionalDependencies:
- fsevents "~2.3.2"
-
-chownr@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
- integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
-clean-stack@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
- integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
-
-cliui@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
- integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^6.2.0"
-
-cliui@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
- integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.1"
- wrap-ansi "^7.0.0"
-
-color-convert@^1.9.0:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
- integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
- dependencies:
- color-name "1.1.3"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
- integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-color-support@^1.1.2, color-support@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
- integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
-
-combined-stream@^1.0.6, combined-stream@~1.0.6:
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
- integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
- dependencies:
- delayed-stream "~1.0.0"
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-
-console-control-strings@^1.0.0, console-control-strings@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
- integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
-
-core-util-is@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
- integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
-
-core-util-is@~1.0.0:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
- integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-
-cross-spawn@^7.0.3:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
- dependencies:
- path-key "^3.1.0"
- shebang-command "^2.0.0"
- which "^2.0.1"
-
-dashdash@^1.12.0:
- version "1.14.1"
- resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
- integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==
- dependencies:
- assert-plus "^1.0.0"
-
-debug@4, debug@^4.1.0, debug@^4.3.3:
- version "4.3.4"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
- integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
- dependencies:
- ms "2.1.2"
-
-decamelize-keys@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8"
- integrity sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==
- dependencies:
- decamelize "^1.1.0"
- map-obj "^1.0.0"
-
-decamelize@^1.1.0, decamelize@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
- integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
-
-delayed-stream@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
- integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
-
-delegates@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
- integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
-
-depd@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
- integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
-
-dijkstrajs@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
- integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
-
-ecc-jsbn@~0.1.1:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
- integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==
- dependencies:
- jsbn "~0.1.0"
- safer-buffer "^2.1.0"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-encode-utf8@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
- integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
-
-encoding@^0.1.12:
- version "0.1.13"
- resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
- integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
- dependencies:
- iconv-lite "^0.6.2"
-
-env-paths@^2.2.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
- integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
-
-err-code@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
- integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
-
-error-ex@^1.3.1:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
- integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
- dependencies:
- is-arrayish "^0.2.1"
-
-esbuild-android-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
- integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
-
-esbuild-android-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5"
- integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==
-
-esbuild-android-arm64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
- integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
-
-esbuild-android-arm64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04"
- integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==
-
-esbuild-darwin-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
- integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
-
-esbuild-darwin-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410"
- integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==
-
-esbuild-darwin-arm64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
- integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
-
-esbuild-darwin-arm64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337"
- integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==
-
-esbuild-freebsd-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
- integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
-
-esbuild-freebsd-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2"
- integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==
-
-esbuild-freebsd-arm64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
- integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
-
-esbuild-freebsd-arm64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635"
- integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==
-
-esbuild-linux-32@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
- integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
-
-esbuild-linux-32@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce"
- integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==
-
-esbuild-linux-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
- integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
-
-esbuild-linux-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz#532738075397b994467b514e524aeb520c191b6c"
- integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==
-
-esbuild-linux-arm64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
- integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
-
-esbuild-linux-arm64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d"
- integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==
-
-esbuild-linux-arm@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
- integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
-
-esbuild-linux-arm@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc"
- integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==
-
-esbuild-linux-mips64le@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
- integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
-
-esbuild-linux-mips64le@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb"
- integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==
-
-esbuild-linux-ppc64le@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
- integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
-
-esbuild-linux-ppc64le@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507"
- integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==
-
-esbuild-linux-riscv64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
- integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
-
-esbuild-linux-riscv64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6"
- integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==
-
-esbuild-linux-s390x@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
- integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
-
-esbuild-linux-s390x@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb"
- integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==
-
-esbuild-netbsd-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
- integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
-
-esbuild-netbsd-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998"
- integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==
-
-esbuild-openbsd-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
- integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
-
-esbuild-openbsd-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8"
- integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==
-
-esbuild-sass-plugin@^2.2.6:
- version "2.4.5"
- resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-2.4.5.tgz#636b8ce5f3369fceb38bac11ae5a5130f93b3fee"
- integrity sha512-di2hLaIwhRXe513uaPPxv+5bjynxAgrS8R+u38lbBfvp1g1xOki4ACXV2aXip2CRPGTbAVDySSxujd9iArFV0w==
- dependencies:
- esbuild "^0.15.17"
- resolve "^1.22.1"
- sass "^1.56.1"
-
-esbuild-sunos-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
- integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
-
-esbuild-sunos-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971"
- integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==
-
-esbuild-windows-32@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
- integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
-
-esbuild-windows-32@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3"
- integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==
-
-esbuild-windows-64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
- integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
-
-esbuild-windows-64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0"
- integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==
-
-esbuild-windows-arm64@0.14.54:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
- integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
-
-esbuild-windows-arm64@0.15.18:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7"
- integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==
-
-esbuild@^0.14.43:
- version "0.14.54"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
- integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
- optionalDependencies:
- "@esbuild/linux-loong64" "0.14.54"
- esbuild-android-64 "0.14.54"
- esbuild-android-arm64 "0.14.54"
- esbuild-darwin-64 "0.14.54"
- esbuild-darwin-arm64 "0.14.54"
- esbuild-freebsd-64 "0.14.54"
- esbuild-freebsd-arm64 "0.14.54"
- esbuild-linux-32 "0.14.54"
- esbuild-linux-64 "0.14.54"
- esbuild-linux-arm "0.14.54"
- esbuild-linux-arm64 "0.14.54"
- esbuild-linux-mips64le "0.14.54"
- esbuild-linux-ppc64le "0.14.54"
- esbuild-linux-riscv64 "0.14.54"
- esbuild-linux-s390x "0.14.54"
- esbuild-netbsd-64 "0.14.54"
- esbuild-openbsd-64 "0.14.54"
- esbuild-sunos-64 "0.14.54"
- esbuild-windows-32 "0.14.54"
- esbuild-windows-64 "0.14.54"
- esbuild-windows-arm64 "0.14.54"
-
-esbuild@^0.15.17:
- version "0.15.18"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d"
- integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==
- optionalDependencies:
- "@esbuild/android-arm" "0.15.18"
- "@esbuild/linux-loong64" "0.15.18"
- esbuild-android-64 "0.15.18"
- esbuild-android-arm64 "0.15.18"
- esbuild-darwin-64 "0.15.18"
- esbuild-darwin-arm64 "0.15.18"
- esbuild-freebsd-64 "0.15.18"
- esbuild-freebsd-arm64 "0.15.18"
- esbuild-linux-32 "0.15.18"
- esbuild-linux-64 "0.15.18"
- esbuild-linux-arm "0.15.18"
- esbuild-linux-arm64 "0.15.18"
- esbuild-linux-mips64le "0.15.18"
- esbuild-linux-ppc64le "0.15.18"
- esbuild-linux-riscv64 "0.15.18"
- esbuild-linux-s390x "0.15.18"
- esbuild-netbsd-64 "0.15.18"
- esbuild-openbsd-64 "0.15.18"
- esbuild-sunos-64 "0.15.18"
- esbuild-windows-32 "0.15.18"
- esbuild-windows-64 "0.15.18"
- esbuild-windows-arm64 "0.15.18"
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-escape-string-regexp@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
- integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
-
-extend@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
- integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
-
-extsprintf@1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
- integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
-
-extsprintf@^1.2.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
- integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
-
-fast-deep-equal@^3.1.1:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
- integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-
-fast-json-stable-stringify@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
- integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
- dependencies:
- to-regex-range "^5.0.1"
-
-find-up@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
- integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
- dependencies:
- locate-path "^5.0.0"
- path-exists "^4.0.0"
-
-forever-agent@~0.6.1:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
- integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
-
-form-data@~2.3.2:
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
- integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.6"
- mime-types "^2.1.12"
-
-fs-minipass@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
- integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
- dependencies:
- minipass "^3.0.0"
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
-
-fsevents@~2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-gauge@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
- integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
- dependencies:
- aproba "^1.0.3 || ^2.0.0"
- color-support "^1.1.2"
- console-control-strings "^1.0.0"
- has-unicode "^2.0.1"
- object-assign "^4.1.1"
- signal-exit "^3.0.0"
- string-width "^4.2.3"
- strip-ansi "^6.0.1"
- wide-align "^1.1.2"
-
-gauge@^4.0.3:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce"
- integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==
- dependencies:
- aproba "^1.0.3 || ^2.0.0"
- color-support "^1.1.3"
- console-control-strings "^1.1.0"
- has-unicode "^2.0.1"
- signal-exit "^3.0.7"
- string-width "^4.2.3"
- strip-ansi "^6.0.1"
- wide-align "^1.1.5"
-
-gaze@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
- integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
- dependencies:
- globule "^1.0.0"
-
-get-caller-file@^2.0.1, get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-get-stdin@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
- integrity sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==
-
-getpass@^0.1.1:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
- integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==
- dependencies:
- assert-plus "^1.0.0"
-
-glob-parent@~5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
- integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
- dependencies:
- is-glob "^4.0.1"
-
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7:
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
- integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.1.1"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-glob@~7.1.1:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-globule@^1.0.0:
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.4.tgz#7c11c43056055a75a6e68294453c17f2796170fb"
- integrity sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==
- dependencies:
- glob "~7.1.1"
- lodash "^4.17.21"
- minimatch "~3.0.2"
-
-graceful-fs@^4.2.6:
- version "4.2.10"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
- integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
-
-har-schema@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
- integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==
-
-har-validator@~5.1.3:
- version "5.1.5"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
- integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
- dependencies:
- ajv "^6.12.3"
- har-schema "^2.0.0"
-
-hard-rejection@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
- integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-
-has-flag@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
- integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-has-unicode@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
- integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
-
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
- dependencies:
- function-bind "^1.1.1"
-
-highlight.js@^11.4.0:
- version "11.7.0"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
- integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
-
-hosted-git-info@^2.1.4:
- version "2.8.9"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
- integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
-
-hosted-git-info@^4.0.1:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224"
- integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==
- dependencies:
- lru-cache "^6.0.0"
-
-http-cache-semantics@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
- integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
-
-http-proxy-agent@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
- integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
- dependencies:
- "@tootallnate/once" "1"
- agent-base "6"
- debug "4"
-
-http-signature@~1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
- integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==
- dependencies:
- assert-plus "^1.0.0"
- jsprim "^1.2.2"
- sshpk "^1.7.0"
-
-https-proxy-agent@^5.0.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
- integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
- dependencies:
- agent-base "6"
- debug "4"
-
-humanize-ms@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
- integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
- dependencies:
- ms "^2.0.0"
-
-iconv-lite@^0.6.2:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
- integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
- dependencies:
- safer-buffer ">= 2.1.2 < 3.0.0"
-
-ieee754@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
- integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-immutable@^4.0.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.1.tgz#8a4025691018c560a40c67e43d698f816edc44d4"
- integrity sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==
-
-imurmurhash@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
- integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
-
-indent-string@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
- integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
-infer-owner@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
- integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2, inherits@^2.0.3, inherits@~2.0.3:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-ip@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
- integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
-
-is-arrayish@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
- integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
-
-is-binary-path@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
- integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
- dependencies:
- binary-extensions "^2.0.0"
-
-is-core-module@^2.5.0, is-core-module@^2.9.0:
- version "2.11.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
- integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
- dependencies:
- has "^1.0.3"
-
-is-extglob@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
- integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1, is-glob@~4.0.1:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
- integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
- dependencies:
- is-extglob "^2.1.1"
-
-is-lambda@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
- integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
-
-is-number@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
- integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-is-plain-obj@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
- integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==
-
-is-typedarray@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
- integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
-
-isarray@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
- integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-
-isstream@~0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
- integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
-
-js-base64@^2.4.9:
- version "2.6.4"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
- integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
-
-js-tokens@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
- integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-jsbn@~0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
- integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
-
-json-parse-even-better-errors@^2.3.0:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
- integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
-
-json-schema-traverse@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
- integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-
-json-schema@0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
- integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
-
-json-stringify-safe@~5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
- integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
-
-jsprim@^1.2.2:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
- integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
- dependencies:
- assert-plus "1.0.0"
- extsprintf "1.3.0"
- json-schema "0.4.0"
- verror "1.10.0"
-
-kind-of@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
- integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
-lines-and-columns@^1.1.6:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
- integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
-
-locate-path@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
- integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
- dependencies:
- p-locate "^4.1.0"
-
-lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.21:
- version "4.17.21"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
- integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-lru-cache@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
- integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
- dependencies:
- yallist "^4.0.0"
-
-make-fetch-happen@^9.1.0:
- version "9.1.0"
- resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
- integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
- dependencies:
- agentkeepalive "^4.1.3"
- cacache "^15.2.0"
- http-cache-semantics "^4.1.0"
- http-proxy-agent "^4.0.1"
- https-proxy-agent "^5.0.0"
- is-lambda "^1.0.1"
- lru-cache "^6.0.0"
- minipass "^3.1.3"
- minipass-collect "^1.0.2"
- minipass-fetch "^1.3.2"
- minipass-flush "^1.0.5"
- minipass-pipeline "^1.2.4"
- negotiator "^0.6.2"
- promise-retry "^2.0.1"
- socks-proxy-agent "^6.0.0"
- ssri "^8.0.0"
-
-map-obj@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
- integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==
-
-map-obj@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
- integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
-
-meow@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
- integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
- dependencies:
- "@types/minimist" "^1.2.0"
- camelcase-keys "^6.2.2"
- decamelize "^1.2.0"
- decamelize-keys "^1.1.0"
- hard-rejection "^2.1.0"
- minimist-options "4.1.0"
- normalize-package-data "^3.0.0"
- read-pkg-up "^7.0.1"
- redent "^3.0.0"
- trim-newlines "^3.0.0"
- type-fest "^0.18.0"
- yargs-parser "^20.2.3"
-
-mime-db@1.52.0:
- version "1.52.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
- integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-
-mime-types@^2.1.12, mime-types@~2.1.19:
- version "2.1.35"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
- integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
- dependencies:
- mime-db "1.52.0"
-
-min-indent@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
- integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-
-minimatch@^3.0.4, minimatch@^3.1.1:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
- integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimatch@~3.0.2:
- version "3.0.8"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
- integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist-options@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
- integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
- dependencies:
- arrify "^1.0.1"
- is-plain-obj "^1.1.0"
- kind-of "^6.0.3"
-
-minipass-collect@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
- integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
- dependencies:
- minipass "^3.0.0"
-
-minipass-fetch@^1.3.2:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6"
- integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==
- dependencies:
- minipass "^3.1.0"
- minipass-sized "^1.0.3"
- minizlib "^2.0.0"
- optionalDependencies:
- encoding "^0.1.12"
-
-minipass-flush@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
- integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
- dependencies:
- minipass "^3.0.0"
-
-minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
- integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
- dependencies:
- minipass "^3.0.0"
-
-minipass-sized@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
- integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
- dependencies:
- minipass "^3.0.0"
-
-minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
- integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
- dependencies:
- yallist "^4.0.0"
-
-minipass@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.0.tgz#7cebb0f9fa7d56f0c5b17853cbe28838a8dbbd3b"
- integrity sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==
- dependencies:
- yallist "^4.0.0"
-
-minizlib@^2.0.0, minizlib@^2.1.1:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
- integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
- dependencies:
- minipass "^3.0.0"
- yallist "^4.0.0"
-
-mkdirp@^1.0.3, mkdirp@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
- integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
-ms@2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
- integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-ms@^2.0.0:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
- integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-nan@^2.13.2:
- version "2.17.0"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
- integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
-
-negotiator@^0.6.2:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
- integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
-
-node-gyp@^8.4.1:
- version "8.4.1"
- resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
- integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
- dependencies:
- env-paths "^2.2.0"
- glob "^7.1.4"
- graceful-fs "^4.2.6"
- make-fetch-happen "^9.1.0"
- nopt "^5.0.0"
- npmlog "^6.0.0"
- rimraf "^3.0.2"
- semver "^7.3.5"
- tar "^6.1.2"
- which "^2.0.2"
-
-node-sass@^7.0.1:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-7.0.3.tgz#7620bcd5559c2bf125c4fbb9087ba75cd2df2ab2"
- integrity sha512-8MIlsY/4dXUkJDYht9pIWBhMil3uHmE8b/AdJPjmFn1nBx9X9BASzfzmsCy0uCCb8eqI3SYYzVPDswWqSx7gjw==
- dependencies:
- async-foreach "^0.1.3"
- chalk "^4.1.2"
- cross-spawn "^7.0.3"
- gaze "^1.0.0"
- get-stdin "^4.0.1"
- glob "^7.0.3"
- lodash "^4.17.15"
- meow "^9.0.0"
- nan "^2.13.2"
- node-gyp "^8.4.1"
- npmlog "^5.0.0"
- request "^2.88.0"
- sass-graph "^4.0.1"
- stdout-stream "^1.4.0"
- "true-case-path" "^1.0.2"
-
-nopt@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
- integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
- dependencies:
- abbrev "1"
-
-normalize-package-data@^2.5.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
- integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
- dependencies:
- hosted-git-info "^2.1.4"
- resolve "^1.10.0"
- semver "2 || 3 || 4 || 5"
- validate-npm-package-license "^3.0.1"
-
-normalize-package-data@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
- integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
- dependencies:
- hosted-git-info "^4.0.1"
- is-core-module "^2.5.0"
- semver "^7.3.4"
- validate-npm-package-license "^3.0.1"
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
- integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-npmlog@^5.0.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
- integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
- dependencies:
- are-we-there-yet "^2.0.0"
- console-control-strings "^1.1.0"
- gauge "^3.0.0"
- set-blocking "^2.0.0"
-
-npmlog@^6.0.0:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830"
- integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==
- dependencies:
- are-we-there-yet "^3.0.0"
- console-control-strings "^1.1.0"
- gauge "^4.0.3"
- set-blocking "^2.0.0"
-
-oauth-sign@~0.9.0:
- version "0.9.0"
- resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
- integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-
-object-assign@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
- integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
-
-once@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
- dependencies:
- wrappy "1"
-
-p-limit@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
- integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
- dependencies:
- p-try "^2.0.0"
-
-p-locate@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
- integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
- dependencies:
- p-limit "^2.2.0"
-
-p-map@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
- integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
- dependencies:
- aggregate-error "^3.0.0"
-
-p-try@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
- integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-parse-json@^5.0.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
- integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
- dependencies:
- "@babel/code-frame" "^7.0.0"
- error-ex "^1.3.1"
- json-parse-even-better-errors "^2.3.0"
- lines-and-columns "^1.1.6"
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
-
-path-key@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
- integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-path-parse@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
- integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
-
-performance-now@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
- integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
-
-"phoenix@file:../../../deps/phoenix":
- version "1.7.0-rc.0"
-
-"phoenix_html@file:../../../deps/phoenix_html":
- version "3.2.0"
-
-"phoenix_live_view@file:../../../deps/phoenix_live_view":
- version "0.18.3"
-
-picomatch@^2.0.4, picomatch@^2.2.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-
-pngjs@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
- integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
-
-process-nextick-args@~2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
- integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
-promise-inflight@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
- integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
-
-promise-retry@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
- integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
- dependencies:
- err-code "^2.0.2"
- retry "^0.12.0"
-
-psl@^1.1.28:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
- integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
-
-punycode@^2.1.0, punycode@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
- integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-
-qrcode@^1.3.3:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.1.tgz#0103f97317409f7bc91772ef30793a54cd59f0cb"
- integrity sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==
- dependencies:
- dijkstrajs "^1.0.1"
- encode-utf8 "^1.0.3"
- pngjs "^5.0.0"
- yargs "^15.3.1"
-
-qs@~6.5.2:
- version "6.5.3"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
- integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
-
-quick-lru@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
- integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-
-read-pkg-up@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
- integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
- dependencies:
- find-up "^4.1.0"
- read-pkg "^5.2.0"
- type-fest "^0.8.1"
-
-read-pkg@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
- integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
- dependencies:
- "@types/normalize-package-data" "^2.4.0"
- normalize-package-data "^2.5.0"
- parse-json "^5.0.0"
- type-fest "^0.6.0"
-
-readable-stream@^2.0.1:
- version "2.3.7"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
- integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.3"
- isarray "~1.0.0"
- process-nextick-args "~2.0.0"
- safe-buffer "~5.1.1"
- string_decoder "~1.1.1"
- util-deprecate "~1.0.1"
-
-readable-stream@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
- integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
- dependencies:
- inherits "^2.0.3"
- string_decoder "^1.1.1"
- util-deprecate "^1.0.1"
-
-readdirp@~3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
- integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
- dependencies:
- picomatch "^2.2.1"
-
-redent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
- integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
- dependencies:
- indent-string "^4.0.0"
- strip-indent "^3.0.0"
-
-request@^2.88.0:
- version "2.88.2"
- resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
- integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
- dependencies:
- aws-sign2 "~0.7.0"
- aws4 "^1.8.0"
- caseless "~0.12.0"
- combined-stream "~1.0.6"
- extend "~3.0.2"
- forever-agent "~0.6.1"
- form-data "~2.3.2"
- har-validator "~5.1.3"
- http-signature "~1.2.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.19"
- oauth-sign "~0.9.0"
- performance-now "^2.1.0"
- qs "~6.5.2"
- safe-buffer "^5.1.2"
- tough-cookie "~2.5.0"
- tunnel-agent "^0.6.0"
- uuid "^3.3.2"
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
-
-require-main-filename@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
- integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-
-resolve@^1.10.0, resolve@^1.22.1:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
- integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
- dependencies:
- is-core-module "^2.9.0"
- path-parse "^1.0.7"
- supports-preserve-symlinks-flag "^1.0.0"
-
-retry@^0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
- integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
-
-rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
-safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
- integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-safe-buffer@~5.1.0, safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
- integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-sass-graph@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-4.0.1.tgz#2ff8ca477224d694055bf4093f414cf6cfad1d2e"
- integrity sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==
- dependencies:
- glob "^7.0.0"
- lodash "^4.17.11"
- scss-tokenizer "^0.4.3"
- yargs "^17.2.1"
-
-sass@^1.56.1:
- version "1.57.1"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5"
- integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==
- dependencies:
- chokidar ">=3.0.0 <4.0.0"
- immutable "^4.0.0"
- source-map-js ">=0.6.2 <2.0.0"
-
-scss-tokenizer@^0.4.3:
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz#1058400ee7d814d71049c29923d2b25e61dc026c"
- integrity sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==
- dependencies:
- js-base64 "^2.4.9"
- source-map "^0.7.3"
-
-"semver@2 || 3 || 4 || 5":
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
- integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
-semver@^7.3.4, semver@^7.3.5:
- version "7.3.8"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
- integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
- dependencies:
- lru-cache "^6.0.0"
-
-set-blocking@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
- integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
-
-shebang-command@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
- integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
- dependencies:
- shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
- integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-signal-exit@^3.0.0, signal-exit@^3.0.7:
- version "3.0.7"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
- integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
-
-smart-buffer@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
- integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
-
-socks-proxy-agent@^6.0.0:
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce"
- integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==
- dependencies:
- agent-base "^6.0.2"
- debug "^4.3.3"
- socks "^2.6.2"
-
-socks@^2.6.2:
- version "2.7.1"
- resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
- integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
- dependencies:
- ip "^2.0.0"
- smart-buffer "^4.2.0"
-
-"source-map-js@>=0.6.2 <2.0.0":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
- integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
-
-source-map@^0.7.3:
- version "0.7.4"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
- integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
-
-spdx-correct@^3.0.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
- integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
- dependencies:
- spdx-expression-parse "^3.0.0"
- spdx-license-ids "^3.0.0"
-
-spdx-exceptions@^2.1.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
- integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
-
-spdx-expression-parse@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
- integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
- dependencies:
- spdx-exceptions "^2.1.0"
- spdx-license-ids "^3.0.0"
-
-spdx-license-ids@^3.0.0:
- version "3.0.12"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779"
- integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==
-
-sshpk@^1.7.0:
- version "1.17.0"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
- integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
- dependencies:
- asn1 "~0.2.3"
- assert-plus "^1.0.0"
- bcrypt-pbkdf "^1.0.0"
- dashdash "^1.12.0"
- ecc-jsbn "~0.1.1"
- getpass "^0.1.1"
- jsbn "~0.1.0"
- safer-buffer "^2.0.2"
- tweetnacl "~0.14.0"
-
-ssri@^8.0.0, ssri@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
- integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
- dependencies:
- minipass "^3.1.1"
-
-stdout-stream@^1.4.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
- integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
- dependencies:
- readable-stream "^2.0.1"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string_decoder@^1.1.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
- integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
- dependencies:
- safe-buffer "~5.2.0"
-
-string_decoder@~1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
- integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
- dependencies:
- safe-buffer "~5.1.0"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-indent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
- integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
- dependencies:
- min-indent "^1.0.0"
-
-supports-color@^5.3.0:
- version "5.5.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
- integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
- dependencies:
- has-flag "^3.0.0"
-
-supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
-supports-preserve-symlinks-flag@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
- integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
-
-tar@^6.0.2, tar@^6.1.2:
- version "6.1.13"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b"
- integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==
- dependencies:
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- minipass "^4.0.0"
- minizlib "^2.1.1"
- mkdirp "^1.0.3"
- yallist "^4.0.0"
-
-to-regex-range@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
- integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
- dependencies:
- is-number "^7.0.0"
-
-tough-cookie@~2.5.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
- integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
- dependencies:
- psl "^1.1.28"
- punycode "^2.1.1"
-
-trim-newlines@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
- integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
-
-"true-case-path@^1.0.2":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
- integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==
- dependencies:
- glob "^7.1.2"
-
-tunnel-agent@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
- integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
- dependencies:
- safe-buffer "^5.0.1"
-
-tweetnacl-util@^0.15.1:
- version "0.15.1"
- resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
- integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==
-
-tweetnacl@^0.14.3, tweetnacl@~0.14.0:
- version "0.14.5"
- resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
- integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
-
-tweetnacl@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
- integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
-
-type-fest@^0.18.0:
- version "0.18.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
- integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
-
-type-fest@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
- integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
-
-type-fest@^0.8.1:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
- integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
-unique-filename@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
- integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
- dependencies:
- unique-slug "^2.0.0"
-
-unique-slug@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
- integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
- dependencies:
- imurmurhash "^0.1.4"
-
-uri-js@^4.2.2:
- version "4.4.1"
- resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
- integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
- dependencies:
- punycode "^2.1.0"
-
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-uuid@^3.3.2:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
- integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-
-validate-npm-package-license@^3.0.1:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
- integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
- dependencies:
- spdx-correct "^3.0.0"
- spdx-expression-parse "^3.0.0"
-
-verror@1.10.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
- integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
- dependencies:
- assert-plus "^1.0.0"
- core-util-is "1.0.2"
- extsprintf "^1.2.0"
-
-which-module@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
- integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
-
-which@^2.0.1, which@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
-wide-align@^1.1.2, wide-align@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
- integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
- dependencies:
- string-width "^1.0.2 || 2 || 3 || 4"
-
-wrap-ansi@^6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
- integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
-
-y18n@^4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
- integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yallist@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
- integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
-yargs-parser@^18.1.2:
- version "18.1.3"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
- integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
- dependencies:
- camelcase "^5.0.0"
- decamelize "^1.2.0"
-
-yargs-parser@^20.2.3:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs-parser@^21.1.1:
- version "21.1.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
- integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
-
-yargs@^15.3.1:
- version "15.4.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
- integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
- dependencies:
- cliui "^6.0.0"
- decamelize "^1.2.0"
- find-up "^4.1.0"
- get-caller-file "^2.0.1"
- require-directory "^2.1.1"
- require-main-filename "^2.0.0"
- set-blocking "^2.0.0"
- string-width "^4.2.0"
- which-module "^2.0.0"
- y18n "^4.0.0"
- yargs-parser "^18.1.2"
-
-yargs@^17.2.1:
- version "17.6.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541"
- integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==
- dependencies:
- cliui "^8.0.1"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.3"
- y18n "^5.0.5"
- yargs-parser "^21.1.1"
-
-zxcvbn@^4.4.2:
- version "4.4.2"
- resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
- integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==
diff --git a/apps/web/coveralls.json b/apps/web/coveralls.json
deleted file mode 100644
index 3d771278d..000000000
--- a/apps/web/coveralls.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "skip_files": [
- "test"
- ]
-}
diff --git a/apps/web/lib/web.ex b/apps/web/lib/web.ex
index 50fb9a4ea..8fe7d0e98 100644
--- a/apps/web/lib/web.ex
+++ b/apps/web/lib/web.ex
@@ -1,96 +1,29 @@
defmodule Web do
@moduledoc """
The entrypoint for defining your web interface, such
- as controllers, views, channels and so on.
+ as controllers, components, channels, and so on.
This can be used in your application as:
use Web, :controller
- use Web, :view
+ use Web, :html
- The definitions below will be executed for every view,
- controller, etc, so keep them short and clean, focused
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
- below. Instead, define any helper function in modules
- and import those modules here.
+ below. Instead, define additional modules and import
+ those modules here.
"""
- def controller do
- quote do
- use Phoenix.Controller, namespace: Web
-
- import Plug.Conn
- import Web.Gettext
- import Phoenix.LiveView.Controller
- import Web.ControllerHelpers
- import Web.DocHelpers
-
- unquote(verified_routes())
- end
- end
-
- def view do
- quote do
- use Phoenix.View,
- root: "lib/web/templates",
- namespace: Web
-
- # Import convenience functions from controllers
- import Phoenix.Controller, only: [view_module: 1]
-
- # Use all HTML functionality (forms, tags, etc)
- use Phoenix.HTML
-
- # Use all LiveView functionality
- use Phoenix.Component, global_prefixes: ~w(x-)
-
- import Web.ErrorHelpers
- import Web.AuthorizationHelpers
- import Web.Gettext
- import Web.LiveHelpers
-
- unquote(verified_routes())
- end
- end
-
- def live_view do
- quote do
- use Phoenix.LiveView, layout: {Web.LayoutView, :live}
- import Web.LiveHelpers
-
- alias Phoenix.LiveView.JS
-
- unquote(view_helpers())
- end
- end
-
- def live_view_without_layout do
- quote do
- use Phoenix.LiveView
- import Web.LiveHelpers
-
- alias Phoenix.LiveView.JS
-
- unquote(view_helpers())
- end
- end
-
- def live_component do
- quote do
- import Phoenix.LiveView
- use Phoenix.LiveComponent
- use Phoenix.Component, global_prefixes: ~w(x-)
- import Web.LiveHelpers
-
- unquote(view_helpers())
- end
- end
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def router do
quote do
- use Phoenix.Router
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
@@ -100,38 +33,68 @@ defmodule Web do
def channel do
quote do
use Phoenix.Channel
- import Web.Gettext
end
end
- def helper do
+ def controller do
quote do
- unquote(verified_routes())
- end
- end
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: Web.Layouts]
- defp view_helpers do
- quote do
- # Use all HTML functionality (forms, tags, etc)
- use Phoenix.HTML
-
- # Import LiveView helpers (live_render, live_component, live_patch, etc)
- import Phoenix.Component
-
- # Import basic rendering functionality (render, render_layout, etc)
- import Phoenix.View
-
- # Authorization Helpers
- import Web.AuthorizationHelpers
-
- import Web.ErrorHelpers
+ import Plug.Conn
import Web.Gettext
+ import Web.ControllerDocumentation
unquote(verified_routes())
end
end
- def static_paths, do: ~w(dist fonts images uploads robots.txt)
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {Web.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import Web.CoreComponents
+ import Web.Gettext
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
def verified_routes do
quote do
diff --git a/apps/web/lib/web/application.ex b/apps/web/lib/web/application.ex
index fffd5445e..fd56f94a4 100644
--- a/apps/web/lib/web/application.ex
+++ b/apps/web/lib/web/application.ex
@@ -1,19 +1,21 @@
defmodule Web.Application do
use Application
+ @impl true
def start(_type, _args) do
- Supervisor.start_link(children(), strategy: :one_for_one, name: __MODULE__.Supervisor)
+ children = [
+ Web.Telemetry,
+ {Phoenix.PubSub, name: Web.PubSub},
+ Web.Endpoint
+ ]
+
+ opts = [strategy: :one_for_one, name: Web.Supervisor]
+ Supervisor.start_link(children, opts)
end
+ @impl true
def config_change(changed, _new, removed) do
Web.Endpoint.config_change(changed, removed)
:ok
end
-
- defp children do
- [
- Web.Presence,
- Web.Endpoint
- ]
- end
end
diff --git a/apps/web/lib/web/auth/html/authentication.ex b/apps/web/lib/web/auth/html/authentication.ex
deleted file mode 100644
index 90eef762c..000000000
--- a/apps/web/lib/web/auth/html/authentication.ex
+++ /dev/null
@@ -1,105 +0,0 @@
-defmodule Web.Auth.HTML.Authentication do
- @moduledoc """
- HTML Authentication implementation module for Guardian.
- """
- use Guardian, otp_app: :web
- use Web, :controller
- alias Domain.Auth
- alias Domain.Telemetry
- alias Domain.Users
- alias Domain.Users.User
-
- @guardian_token_name "guardian_default_token"
-
- @impl Guardian
- def subject_for_token(%Auth.Subject{actor: {:user, user}}, _claims) do
- {:ok, user.id}
- end
-
- @impl Guardian
- def resource_from_claims(%{"sub" => id}) do
- with {:ok, user} <- Users.fetch_user_by_id(id) do
- # XXX: Guardian doesn't allow us to access the conn params here
- subject = Auth.fetch_subject!(user, nil, nil)
- {:ok, subject}
- else
- {:error, :not_found} -> {:error, :resource_not_found}
- end
- end
-
- @doc """
- Authenticates a user against a password hash. Only makes sense
- for local auth.
- """
- def authenticate(%User{} = user, password) when is_binary(password) do
- if user.password_hash do
- authenticate(
- user,
- password,
- Argon2.verify_pass(password, user.password_hash)
- )
- else
- {:error, :invalid_credentials}
- end
- end
-
- def authenticate(_user, _password) do
- authenticate(nil, nil, Argon2.no_user_verify())
- end
-
- defp authenticate(user, _password, true) do
- {:ok, user}
- end
-
- defp authenticate(_user, _password, false) do
- {:error, :invalid_credentials}
- end
-
- def sign_in(conn, user, auth) do
- Telemetry.login()
- Users.update_last_signed_in(user, auth)
- subject = Auth.fetch_subject!(user, nil, nil)
- %{provider: provider_id} = auth
-
- conn
- |> Plug.Conn.put_session("login_method", provider_id)
- |> Plug.Conn.put_session("logged_in_at", DateTime.utc_now())
- |> __MODULE__.Plug.sign_in(subject)
- end
-
- def sign_out(conn) do
- with provider_id when not is_nil(provider_id) <- Plug.Conn.get_session(conn, "login_method"),
- token when not is_nil(token) <- Plug.Conn.get_session(conn, "id_token"),
- {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id),
- {:ok, end_session_uri} <-
- OpenIDConnect.end_session_uri(config, %{
- id_token_hint: token,
- post_logout_redirect_uri: url(~p"/")
- }) do
- conn
- |> __MODULE__.Plug.sign_out()
- |> Plug.Conn.configure_session(drop: true)
- |> Phoenix.Controller.redirect(external: end_session_uri)
- else
- _ ->
- conn
- |> __MODULE__.Plug.sign_out()
- |> Plug.Conn.configure_session(drop: true)
- |> Phoenix.Controller.redirect(to: ~p"/")
- end
- end
-
- def get_current_subject(%Plug.Conn{} = conn) do
- __MODULE__.Plug.current_resource(conn)
- end
-
- def get_current_subject(%{@guardian_token_name => token} = _session) do
- case Guardian.resource_from_token(__MODULE__, token) do
- {:ok, resource, _claims} ->
- resource
-
- {:error, _reason} ->
- nil
- end
- end
-end
diff --git a/apps/web/lib/web/auth/html/error_handler.ex b/apps/web/lib/web/auth/html/error_handler.ex
deleted file mode 100644
index 8528e14db..000000000
--- a/apps/web/lib/web/auth/html/error_handler.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-defmodule Web.Auth.HTML.ErrorHandler do
- @moduledoc """
- HTML Error Handler module implementation for Guardian.
- """
-
- use Web, :controller
- alias Web.Auth.HTML.Authentication
- import Web.ControllerHelpers, only: [root_path_for_user: 1]
- require Logger
-
- @behaviour Guardian.Plug.ErrorHandler
-
- @impl Guardian.Plug.ErrorHandler
- def auth_error(conn, {:already_authenticated, _reason}, _opts) do
- if subject = Authentication.get_current_subject(conn) do
- {:user, user} = subject.actor
- redirect(conn, to: root_path_for_user(user))
- else
- redirect(conn, to: root_path_for_user(nil))
- end
- end
-
- @impl Guardian.Plug.ErrorHandler
- def auth_error(conn, {:unauthenticated, _reason}, _opts) do
- conn
- |> redirect(to: ~p"/")
- end
-
- @impl Guardian.Plug.ErrorHandler
- def auth_error(conn, {type, reason}, _opts) do
- Logger.info("Web auth error. Type: #{type}. Reason: #{reason}.")
-
- conn
- |> put_resp_content_type("text/plain")
- |> send_resp(401, to_string(type))
- end
-end
diff --git a/apps/web/lib/web/auth/html/pipeline.ex b/apps/web/lib/web/auth/html/pipeline.ex
deleted file mode 100644
index a405d5e6d..000000000
--- a/apps/web/lib/web/auth/html/pipeline.ex
+++ /dev/null
@@ -1,15 +0,0 @@
-defmodule Web.Auth.HTML.Pipeline do
- @moduledoc """
- HTML Plug implementation module for Guardian.
- """
-
- use Guardian.Plug.Pipeline,
- otp_app: :web,
- error_handler: Web.Auth.HTML.ErrorHandler,
- module: Web.Auth.HTML.Authentication
-
- @claims %{"typ" => "access"}
-
- plug Guardian.Plug.VerifySession, claims: @claims, refresh_from_cookie: true
- plug Guardian.Plug.LoadResource, allow_blank: true
-end
diff --git a/apps/web/lib/web/auth/json/authentication.ex b/apps/web/lib/web/auth/json/authentication.ex
deleted file mode 100644
index f521fce88..000000000
--- a/apps/web/lib/web/auth/json/authentication.ex
+++ /dev/null
@@ -1,42 +0,0 @@
-defmodule Web.Auth.JSON.Authentication do
- @moduledoc """
- API Authentication implementation module for Guardian.
- """
- use Guardian, otp_app: :web
-
- alias Domain.{
- Auth,
- ApiTokens.ApiToken,
- ApiTokens
- }
-
- @impl Guardian
- def subject_for_token(%Auth.Subject{actor: {:user, user}}, _claims) do
- {:ok, user.id}
- end
-
- @impl Guardian
- def resource_from_claims(%{"api" => api_token_id}) do
- with {:ok, %ApiTokens.ApiToken{} = api_token} <-
- ApiTokens.fetch_unexpired_api_token_by_id(api_token_id) do
- subject = Auth.fetch_subject!(api_token, nil, nil)
- {:ok, subject}
- else
- {:error, :not_found} -> {:error, :resource_not_found}
- end
- end
-
- def fz_encode_and_sign(%ApiToken{} = api_token) do
- claims = %{
- "api" => api_token.id,
- "exp" => DateTime.to_unix(api_token.expires_at)
- }
-
- subject = Auth.fetch_subject!(api_token, nil, nil)
- Guardian.encode_and_sign(__MODULE__, subject, claims)
- end
-
- def get_current_subject(%Plug.Conn{} = conn) do
- __MODULE__.Plug.current_resource(conn)
- end
-end
diff --git a/apps/web/lib/web/auth/json/error_handler.ex b/apps/web/lib/web/auth/json/error_handler.ex
deleted file mode 100644
index 250f45e42..000000000
--- a/apps/web/lib/web/auth/json/error_handler.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-defmodule Web.Auth.JSON.ErrorHandler do
- @moduledoc """
- API Error Handler module implementation for Guardian.
- """
- use Web, :controller
- require Logger
-
- @behaviour Guardian.Plug.ErrorHandler
-
- @impl Guardian.Plug.ErrorHandler
- def auth_error(conn, {type, reason}, _opts) do
- Logger.info("API auth error. Type: #{type}. Reason: #{reason}.")
-
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(401, Jason.encode!(%{"errors" => %{"auth" => to_string(type)}}))
- end
-end
diff --git a/apps/web/lib/web/auth/json/pipeline.ex b/apps/web/lib/web/auth/json/pipeline.ex
deleted file mode 100644
index e2c1d43e6..000000000
--- a/apps/web/lib/web/auth/json/pipeline.ex
+++ /dev/null
@@ -1,17 +0,0 @@
-defmodule Web.Auth.JSON.Pipeline do
- @moduledoc """
- API Plug implementation module for Guardian.
- """
-
- use Guardian.Plug.Pipeline,
- otp_app: :web,
- error_handler: Web.Auth.JSON.ErrorHandler,
- module: Web.Auth.JSON.Authentication
-
- # 90 days
- @max_age 60 * 60 * 24 * 90
-
- plug Guardian.Plug.VerifyHeader, max_age: @max_age
- plug Guardian.Plug.EnsureAuthenticated
- plug Guardian.Plug.LoadResource
-end
diff --git a/apps/web/lib/web/authorization_helpers.ex b/apps/web/lib/web/authorization_helpers.ex
deleted file mode 100644
index 41f79869c..000000000
--- a/apps/web/lib/web/authorization_helpers.ex
+++ /dev/null
@@ -1,13 +0,0 @@
-defmodule Web.AuthorizationHelpers do
- @moduledoc """
- Authorization-related helpers
- """
- use Web, :helper
- import Phoenix.LiveView
-
- def not_authorized(socket) do
- socket
- |> put_flash(:error, "Not authorized.")
- |> redirect(to: ~p"/")
- end
-end
diff --git a/apps/web/lib/web/channels/notification_channel.ex b/apps/web/lib/web/channels/notification_channel.ex
deleted file mode 100644
index 2b9e440ed..000000000
--- a/apps/web/lib/web/channels/notification_channel.ex
+++ /dev/null
@@ -1,51 +0,0 @@
-defmodule Web.NotificationChannel do
- @moduledoc """
- Handles dispatching realtime notifications to users' browser sessions.
- """
- use Web, :channel
- alias Domain.Users
- alias Web.Presence
-
- @impl Phoenix.Channel
- def join("notification:session", _attrs, socket) do
- socket = Web.Sandbox.allow_channel_sql_sandbox(socket)
-
- with {:ok, user} <- Users.fetch_user_by_id(socket.assigns.current_user_id) do
- socket = assign(socket, :current_user, user)
- send(self(), :after_join)
- {:ok, socket}
- else
- _ -> {:error, %{reason: "unauthorized"}}
- end
- end
-
- @impl Phoenix.Channel
- def handle_info(:after_join, socket) do
- track(socket)
- {:noreply, socket}
- end
-
- defp track(socket) do
- user = socket.assigns.current_user
-
- tracking_info = %{
- email: user.email,
- online_at: DateTime.utc_now(),
- last_signed_in_at: user.last_signed_in_at,
- last_signed_in_method: user.last_signed_in_method,
- remote_ip: socket.assigns.remote_ip,
- user_agent: socket.assigns.user_agent
- }
-
- {:ok, _} = Presence.track(socket, user.id, tracking_info)
-
- push(socket, "presence_state", presence_list(socket))
- end
-
- defp presence_list(socket) do
- ids_to_show = [socket.assigns.current_user.id]
-
- Presence.list(socket)
- |> Map.take(ids_to_show)
- end
-end
diff --git a/apps/web/lib/web/components/core_components.ex b/apps/web/lib/web/components/core_components.ex
new file mode 100644
index 000000000..e0bbd4404
--- /dev/null
+++ b/apps/web/lib/web/components/core_components.ex
@@ -0,0 +1,661 @@
+defmodule Web.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ The components in this module use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
+ customize the generated components in this module.
+
+ Icons are provided by [heroicons](https://heroicons.com), using the
+ [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+ import Web.Gettext
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ Are you sure?
+ <:confirm>OK
+ <:cancel>Cancel
+
+
+ JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
+ for the caller to react to each button press, for example:
+
+ <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
+ Are you sure you?
+ <:confirm>OK
+ <:cancel>Cancel
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ attr :on_confirm, JS, default: %JS{}
+
+ slot :inner_block, required: true
+ slot :title
+ slot :subtitle
+ slot :confirm
+ slot :cancel
+
+ def modal(assigns) do
+ ~H"""
+
+
+
+
+
+ <.focus_wrap
+ id={"#{@id}-container"}
+ phx-mounted={@show && show_modal(@id)}
+ phx-window-keydown={hide_modal(@on_cancel, @id)}
+ phx-key="escape"
+ phx-click-away={hide_modal(@on_cancel, @id)}
+ class="hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition"
+ >
+
+
+
+
+
+
+
+ <%= render_slot(@inner_block) %>
+
+ <.button
+ :for={confirm <- @confirm}
+ id={"#{@id}-confirm"}
+ phx-click={@on_confirm}
+ phx-disable-with
+ class="py-2 px-3"
+ >
+ <%= render_slot(confirm) %>
+
+ <.link
+ :for={cancel <- @cancel}
+ phx-click={hide_modal(@on_cancel, @id)}
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+ >
+ <%= render_slot(cancel) %>
+
+
+
+
+
+
+
+
+ """
+ end
+
+ @doc """
+ Renders flash notices.
+
+ ## Examples
+
+ <.flash kind={:info} flash={@flash} />
+ <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
+ """
+ attr :id, :string, default: "flash", doc: "the optional id of flash container"
+ attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
+ attr :title, :string, default: nil
+ attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
+ attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
+ attr :close, :boolean, default: true, doc: "whether the flash can be closed"
+ attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
+
+ slot :inner_block, doc: "the optional inner block that renders the flash message"
+
+ def flash(assigns) do
+ ~H"""
+ hide("##{@id}")}
+ role="alert"
+ class={[
+ "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
+ @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
+ @kind == :error && "bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
+ ]}
+ {@rest}
+ >
+
+
+
+ <%= @title %>
+
+
<%= msg %>
+
+
+
+
+ """
+ end
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+
+ def flash_group(assigns) do
+ ~H"""
+ <.flash kind={:info} title="Success!" flash={@flash} />
+ <.flash kind={:error} title="Error!" flash={@flash} />
+ <.flash
+ id="disconnected"
+ kind={:error}
+ title="We can't find the internet"
+ close={false}
+ autoshow={false}
+ phx-disconnected={show("#disconnected")}
+ phx-connected={hide("#disconnected")}
+ >
+ Attempting to reconnect
+
+ """
+ end
+
+ @doc """
+ Renders a simple form.
+
+ ## Examples
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
+ <.input field={@form[:email]} label="Email"/>
+ <.input field={@form[:username]} label="Username" />
+ <:actions>
+ <.button>Save
+
+
+ """
+ attr :for, :any, required: true, doc: "the datastructure for the form"
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+
+ attr :rest, :global,
+ include: ~w(autocomplete name rel action enctype method novalidate target),
+ doc: "the arbitrary HTML attributes to apply to the form tag"
+
+ slot :inner_block, required: true
+ slot :actions, doc: "the slot for form actions, such as a submit button"
+
+ def simple_form(assigns) do
+ ~H"""
+ <.form :let={f} for={@for} as={@as} {@rest}>
+
+ <%= render_slot(@inner_block, f) %>
+
+ <%= render_slot(action, f) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a button.
+
+ ## Examples
+
+ <.button>Send!
+ <.button phx-click="go" class="ml-2">Send!
+ """
+ attr :type, :string, default: nil
+ attr :class, :string, default: nil
+ attr :rest, :global, include: ~w(disabled form name value)
+
+ slot :inner_block, required: true
+
+ def button(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `%Phoenix.HTML.Form{}` and field name may be passed to the input
+ to build input names and error messages, or all the attributes and
+ errors may be passed explicitly.
+
+ ## Examples
+
+ <.input field={@form[:email]} type="email" />
+ <.input name="my-input" errors={["oh no!"]} />
+ """
+ attr :id, :any, default: nil
+ attr :name, :any
+ attr :label, :string, default: nil
+ attr :value, :any
+
+ attr :type, :string,
+ default: "text",
+ values: ~w(checkbox color date datetime-local email file hidden month number password
+ range radio search select tel text textarea time url week)
+
+ attr :field, Phoenix.HTML.FormField,
+ doc: "a form field struct retrieved from the form, for example: @form[:email]"
+
+ attr :errors, :list, default: []
+ attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
+ attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
+ attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
+ attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
+ attr :rest, :global, include: ~w(autocomplete cols disabled form max maxlength min minlength
+ pattern placeholder readonly required rows size step)
+ slot :inner_block
+
+ def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
+ assigns
+ |> assign(field: nil, id: assigns.id || field.id)
+ |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
+ |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+ |> assign_new(:value, fn -> field.value end)
+ |> input()
+ end
+
+ def input(%{type: "checkbox", value: value} = assigns) do
+ assigns =
+ assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
+
+ ~H"""
+
+
+
+
+ <%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(%{type: "select"} = assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <%= @prompt %>
+ <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(%{type: "textarea"} = assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ @doc """
+ Renders a label.
+ """
+ attr :for, :string, default: nil
+ slot :inner_block, required: true
+
+ def label(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Generates a generic error message.
+ """
+ slot :inner_block, required: true
+
+ def error(assigns) do
+ ~H"""
+
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Renders a header with title.
+ """
+ attr :class, :string, default: nil
+
+ slot :inner_block, required: true
+ slot :subtitle
+ slot :actions
+
+ def header(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc ~S"""
+ Renders a table with generic styling.
+
+ ## Examples
+
+ <.table id="users" rows={@users}>
+ <:col :let={user} label="id"><%= user.id %>
+ <:col :let={user} label="username"><%= user.username %>
+
+ """
+ attr :id, :string, required: true
+ attr :rows, :list, required: true
+ attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
+
+ attr :row_item, :any,
+ default: &Function.identity/1,
+ doc: "the function for mapping each row before calling the :col and :action slots"
+
+ slot :col, required: true do
+ attr :label, :string
+ end
+
+ slot :action, doc: "the slot for showing user actions in the last table column"
+
+ def table(assigns) do
+ assigns =
+ with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
+ assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
+ end
+
+ ~H"""
+
+
+
+
+ <%= col[:label] %>
+ <%= gettext("Actions") %>
+
+
+
+
+
+
+
+
+ <%= render_slot(col, @row_item.(row)) %>
+
+
+
+
+
+
+
+ <%= render_slot(action, @row_item.(row)) %>
+
+
+
+
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a data list.
+
+ ## Examples
+
+ <.list>
+ <:item title="Title"><%= @post.title %>
+ <:item title="Views"><%= @post.views %>
+
+ """
+ slot :item, required: true do
+ attr :title, :string, required: true
+ end
+
+ def list(assigns) do
+ ~H"""
+
+
+
+
<%= item.title %>
+ <%= render_slot(item) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a back navigation link.
+
+ ## Examples
+
+ <.back navigate={~p"/posts"}>Back to posts
+ """
+ attr :navigate, :any, required: true
+ slot :inner_block, required: true
+
+ def back(assigns) do
+ ~H"""
+
+ <.link
+ navigate={@navigate}
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+ >
+
+ <%= render_slot(@inner_block) %>
+
+
+ """
+ end
+
+ ## JS Commands
+
+ def show(js \\ %JS{}, selector) do
+ JS.show(js,
+ to: selector,
+ transition:
+ {"transition-all transform ease-out duration-300",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+ "opacity-100 translate-y-0 sm:scale-100"}
+ )
+ end
+
+ def hide(js \\ %JS{}, selector) do
+ JS.hide(js,
+ to: selector,
+ time: 200,
+ transition:
+ {"transition-all transform ease-in duration-200",
+ "opacity-100 translate-y-0 sm:scale-100",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+ )
+ end
+
+ def show_modal(js \\ %JS{}, id) when is_binary(id) do
+ js
+ |> JS.show(to: "##{id}")
+ |> JS.show(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
+ )
+ |> show("##{id}-container")
+ |> JS.add_class("overflow-hidden", to: "body")
+ |> JS.focus_first(to: "##{id}-content")
+ end
+
+ def hide_modal(js \\ %JS{}, id) do
+ js
+ |> JS.hide(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
+ )
+ |> hide("##{id}-container")
+ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
+ |> JS.remove_class("overflow-hidden", to: "body")
+ |> JS.pop_focus()
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate "is invalid" in the "errors" domain
+ # dgettext("errors", "is invalid")
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ # This requires us to call the Gettext module passing our gettext
+ # backend as first argument.
+ #
+ # Note we use the "errors" domain, which means translations
+ # should be written to the errors.po file. The :count option is
+ # set by Ecto and indicates we should also apply plural rules.
+ if count = opts[:count] do
+ Gettext.dngettext(Web.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(Web.Gettext, "errors", msg, opts)
+ end
+ end
+
+ @doc """
+ Translates the errors for a field from a keyword list of errors.
+ """
+ def translate_errors(errors, field) when is_list(errors) do
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+ end
+end
diff --git a/apps/web/lib/web/components/layouts.ex b/apps/web/lib/web/components/layouts.ex
new file mode 100644
index 000000000..1f4f44c47
--- /dev/null
+++ b/apps/web/lib/web/components/layouts.ex
@@ -0,0 +1,5 @@
+defmodule Web.Layouts do
+ use Web, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/apps/web/lib/web/components/layouts/app.html.heex b/apps/web/lib/web/components/layouts/app.html.heex
new file mode 100644
index 000000000..aa3878d0b
--- /dev/null
+++ b/apps/web/lib/web/components/layouts/app.html.heex
@@ -0,0 +1,43 @@
+
+
+
+ <.flash_group flash={@flash} />
+ <%= @inner_content %>
+
+
diff --git a/apps/web/lib/web/components/layouts/root.html.heex b/apps/web/lib/web/components/layouts/root.html.heex
new file mode 100644
index 000000000..ca8f88688
--- /dev/null
+++ b/apps/web/lib/web/components/layouts/root.html.heex
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <.live_title suffix=" · Phoenix Framework">
+ <%= assigns[:page_title] || "Web" %>
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/apps/web/lib/web/doc_helpers.ex b/apps/web/lib/web/controller_documentation.ex
similarity index 92%
rename from apps/web/lib/web/doc_helpers.ex
rename to apps/web/lib/web/controller_documentation.ex
index 3aeef3c57..eb94b0fe0 100644
--- a/apps/web/lib/web/doc_helpers.ex
+++ b/apps/web/lib/web/controller_documentation.ex
@@ -1,4 +1,4 @@
-defmodule Web.DocHelpers do
+defmodule Web.ControllerDocumentation do
def group(name, children) do
{:group, {name, children}}
end
diff --git a/apps/web/lib/web/controller_helpers.ex b/apps/web/lib/web/controller_helpers.ex
deleted file mode 100644
index 4ccda7508..000000000
--- a/apps/web/lib/web/controller_helpers.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule Web.ControllerHelpers do
- @moduledoc """
- Useful helpers for controllers
- """
- use Web, :helper
-
- alias Domain.Users.User
-
- def root_path_for_user(nil) do
- ~p"/"
- end
-
- def root_path_for_user(%User{role: :admin}) do
- ~p"/users"
- end
-
- def root_path_for_user(%User{role: :unprivileged}) do
- ~p"/user_devices"
- end
-end
diff --git a/apps/web/lib/web/controllers/auth_controller.ex b/apps/web/lib/web/controllers/auth_controller.ex
deleted file mode 100644
index 806f18c4f..000000000
--- a/apps/web/lib/web/controllers/auth_controller.ex
+++ /dev/null
@@ -1,198 +0,0 @@
-defmodule Web.AuthController do
- @moduledoc """
- Implements the CRUD for a Session
- """
- use Web, :controller
- alias Domain.Users
- alias Domain.Auth
- alias Web.Auth.HTML.Authentication
- alias Web.OAuth.PKCE
- alias Web.OIDC.State
- alias Web.UserFromAuth
- require Logger
-
- # Uncomment when Helpers.callback_url/1 is fixed
- # alias Ueberauth.Strategy.Helpers
-
- plug Ueberauth
-
- def request(conn, _params) do
- path = ~p"/auth/identity/callback"
-
- conn
- |> render("request.html", callback_path: path)
- end
-
- def callback(%{assigns: %{ueberauth_failure: %{errors: errors}}} = conn, _params) do
- msg = Enum.map_join(errors, ". ", fn error -> error.message end)
-
- conn
- |> put_flash(:error, msg)
- |> redirect(to: ~p"/")
- end
-
- def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
- case UserFromAuth.find_or_create(auth) do
- {:ok, user} ->
- do_sign_in(conn, user, auth)
-
- {:error, reason} when reason in [:not_found, :invalid_credentials] ->
- conn
- |> put_flash(
- :error,
- "Error signing in: user credentials are invalid or user does not exist"
- )
- |> request(%{})
-
- {:error, reason} ->
- conn
- |> put_flash(:error, "Error signing in: #{reason}")
- |> request(%{})
- end
- end
-
- # This can be called if the user attempts to visit one of the callback redirect URLs
- # directly.
- def callback(conn, params) do
- conn
- |> put_flash(:error, inspect(params) <> inspect(conn.assigns))
- |> redirect(to: ~p"/")
- end
-
- def oidc_callback(conn, %{"provider" => provider_id, "state" => state} = params)
- when is_binary(provider_id) do
- token_params = Map.merge(params, PKCE.token_params(conn))
-
- with :ok <- State.verify_state(conn, state),
- {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id),
- {:ok, tokens} <- OpenIDConnect.fetch_tokens(config, token_params),
- {:ok, claims} <- OpenIDConnect.verify(config, tokens["id_token"]) do
- case UserFromAuth.find_or_create(provider_id, claims) do
- {:ok, user} ->
- # only first-time connect will include refresh token
- # XXX: Remove this when SCIM 2.0 is implemented
- with %{"refresh_token" => refresh_token} <- tokens do
- Domain.Auth.OIDC.create_connection(user.id, provider_id, refresh_token)
- end
-
- conn
- |> put_session("id_token", tokens["id_token"])
- |> do_sign_in(user, %{provider: provider_id})
-
- {:error, reason} ->
- conn
- |> put_flash(:error, "Error signing in: #{reason}")
- |> redirect(to: ~p"/")
- end
- else
- # Error verifying state, claims or fetching tokens
- {:error, error} ->
- msg = "An OpenIDConnect error occurred. Details: #{inspect(error)}"
- Logger.error(msg)
-
- conn
- |> put_flash(:error, msg)
- |> redirect(to: ~p"/")
- end
- end
-
- def saml_callback(conn, _params) do
- key = {idp, _} = get_session(conn, "samly_assertion_key")
- assertion = %Samly.Assertion{} = Samly.State.get_assertion(conn, key)
-
- with {:ok, user} <-
- UserFromAuth.find_or_create(:saml, idp, %{"email" => assertion.subject.name}) do
- do_sign_in(conn, user, %{provider: idp})
- else
- {:error, %{errors: [email: {"is invalid email address", _metadata}]}} ->
- conn
- |> put_flash(
- :error,
- "SAML provider did not return a valid email address in `name` assertion"
- )
- |> redirect(to: ~p"/")
-
- {:error, reason} when is_binary(reason) ->
- conn
- |> put_flash(:error, reason)
- |> redirect(to: ~p"/")
-
- other ->
- other
- end
- end
-
- def delete(conn, _params) do
- Authentication.sign_out(conn)
- end
-
- def reset_password(conn, _params) do
- render(conn, "reset_password.html")
- end
-
- def magic_link(conn, %{"email" => email}) do
- with {:ok, user} <- Users.fetch_user_by_email(email),
- {:ok, user} <- Users.request_sign_in_token(user) do
- Web.Mailer.AuthEmail.magic_link(user)
- |> Web.Mailer.deliver!()
-
- conn
- |> put_flash(:info, "Please check your inbox for the magic link.")
- |> redirect(to: ~p"/")
- else
- {:error, :not_found} ->
- conn
- |> put_flash(:warning, "Failed to send magic link email.")
- |> redirect(to: ~p"/auth/reset_password")
- end
- end
-
- def magic_sign_in(conn, %{"user_id" => user_id, "token" => token}) do
- with {:ok, user} <- Users.fetch_user_by_id(user_id),
- {:ok, _user} <- Users.consume_sign_in_token(user, token) do
- do_sign_in(conn, user, %{provider: :magic_link})
- else
- {:error, _reason} ->
- conn
- |> put_flash(:error, "The magic link is not valid or has expired.")
- |> redirect(to: ~p"/")
- end
- end
-
- def redirect_oidc_auth_uri(conn, %{"provider" => provider_id}) when is_binary(provider_id) do
- verifier = PKCE.code_verifier()
-
- params = %{
- access_type: :offline,
- state: State.new(),
- code_challenge_method: PKCE.code_challenge_method(),
- code_challenge: PKCE.code_challenge(verifier)
- }
-
- with {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id),
- {:ok, uri} <- OpenIDConnect.authorization_uri(config, params) do
- conn
- |> PKCE.put_cookie(verifier)
- |> State.put_cookie(params.state)
- |> redirect(external: uri)
- else
- {:error, :not_found} ->
- {:error, :not_found}
-
- {:error, reason} ->
- Logger.error("Cannot redirect user to OIDC auth uri", reason: inspect(reason))
-
- conn
- |> put_flash(:error, "Error while processing OpenID request.")
- |> redirect(to: ~p"/")
- end
- end
-
- defp do_sign_in(conn, user, auth) do
- conn
- |> Authentication.sign_in(user, auth)
- |> configure_session(renew: true)
- |> put_session(:live_socket_id, "users_socket:#{user.id}")
- |> redirect(to: root_path_for_user(user))
- end
-end
diff --git a/apps/web/lib/web/controllers/browser_controller.ex b/apps/web/lib/web/controllers/browser_controller.ex
deleted file mode 100644
index 1b35903c7..000000000
--- a/apps/web/lib/web/controllers/browser_controller.ex
+++ /dev/null
@@ -1,7 +0,0 @@
-defmodule Web.BrowserController do
- use Web, :controller
-
- def config(conn, _params) do
- render(conn, "browserconfig.xml")
- end
-end
diff --git a/apps/web/lib/web/controllers/debug_controller.ex b/apps/web/lib/web/controllers/debug_controller.ex
deleted file mode 100644
index 7715dee0a..000000000
--- a/apps/web/lib/web/controllers/debug_controller.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule Web.DebugController do
- @moduledoc """
- Dev only:
-
- /dev/session
- /dev/samly
- """
- use Web, :controller
-
- def samly(conn, _params) do
- resp = """
- Samly.Provider state:
- #{pretty(Application.get_env(:samly, Samly.Provider))}
-
- Service Providers:
- #{pretty(Application.get_env(:samly, :service_providers))}
-
- Identity Providers:
- #{pretty(Application.get_env(:samly, :identity_providers))}
-
- Samly Session:
- #{pretty(Samly.get_active_assertion(conn))}
- """
-
- send_resp(conn, :ok, resp)
- end
-
- def session(conn, _params) do
- send_resp(conn, :ok, pretty(get_session(conn)))
- end
-
- defp pretty(stuff) do
- inspect(stuff, pretty: true)
- end
-end
diff --git a/apps/web/lib/web/controllers/error_html.ex b/apps/web/lib/web/controllers/error_html.ex
new file mode 100644
index 000000000..4edc23f26
--- /dev/null
+++ b/apps/web/lib/web/controllers/error_html.ex
@@ -0,0 +1,19 @@
+defmodule Web.ErrorHTML do
+ use Web, :html
+
+ # If you want to customize your error pages,
+ # uncomment the embed_templates/1 call below
+ # and add pages to the error directory:
+ #
+ # * lib/web_web/controllers/error_html/404.html.heex
+ # * lib/web_web/controllers/error_html/500.html.heex
+ #
+ # embed_templates "error_html/*"
+
+ # The default is to render a plain text page based on
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/apps/web/lib/web/controllers/error_json.ex b/apps/web/lib/web/controllers/error_json.ex
new file mode 100644
index 000000000..0613c95d4
--- /dev/null
+++ b/apps/web/lib/web/controllers/error_json.ex
@@ -0,0 +1,15 @@
+defmodule Web.ErrorJSON do
+ # If you want to customize a particular status code,
+ # you may add your own clauses, such as:
+ #
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/apps/web/lib/web/controllers/json/configuration_controller.ex b/apps/web/lib/web/controllers/json/configuration_controller.ex
deleted file mode 100644
index d8f2005f0..000000000
--- a/apps/web/lib/web/controllers/json/configuration_controller.ex
+++ /dev/null
@@ -1,33 +0,0 @@
-defmodule Web.JSON.ConfigurationController do
- @moduledoc api_doc: [title: "Configurations", group: "Configuration"]
- @moduledoc """
- This endpoint allows an administrator to manage Configurations.
-
- Updates here can be applied at runtime with little to no downtime of affected services.
- """
- use Web, :controller
- alias Domain.Config
- alias Web.Auth.JSON.Authentication
-
- action_fallback(Web.JSON.FallbackController)
-
- @doc api_doc: [summary: "Get Configuration"]
- def show(conn, _params) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, configuration} <- Config.fetch_db_config(subject) do
- render(conn, "show.json", configuration: configuration)
- end
- end
-
- @doc api_doc: [summary: "Update Configuration"]
- def update(conn, %{"configuration" => params}) do
- subject = Authentication.get_current_subject(conn)
- configuration = Config.fetch_db_config!()
-
- with {:ok, %Config.Configuration{} = configuration} <-
- Config.update_config(configuration, params, subject) do
- render(conn, "show.json", configuration: configuration)
- end
- end
-end
diff --git a/apps/web/lib/web/controllers/json/device_controller.ex b/apps/web/lib/web/controllers/json/device_controller.ex
deleted file mode 100644
index 5ef8eae65..000000000
--- a/apps/web/lib/web/controllers/json/device_controller.ex
+++ /dev/null
@@ -1,67 +0,0 @@
-defmodule Web.JSON.DeviceController do
- @moduledoc api_doc: [title: "Devices", group: "Devices"]
- @moduledoc """
- This endpoint allows an administrator to manage Devices.
- """
- use Web, :controller
- alias Domain.{Users, Devices}
- alias Web.Auth.JSON.Authentication
-
- action_fallback(Web.JSON.FallbackController)
-
- @doc api_doc: [summary: "List all Devices"]
- def index(conn, _attrs) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, devices} <- Devices.list_devices(subject) do
- defaults = Devices.defaults()
- render(conn, "index.json", devices: devices, defaults: defaults)
- end
- end
-
- @doc api_doc: [summary: "Create a Device"]
- def create(conn, %{"device" => attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, user} <- Users.fetch_user_by_id(attrs["user_id"], subject),
- {:ok, device} <- Devices.create_device_for_user(user, attrs, subject) do
- defaults = Devices.defaults()
-
- conn
- |> put_status(:created)
- |> put_resp_header("location", ~p"/v0/devices/#{device}")
- |> render("show.json", device: device, defaults: defaults)
- end
- end
-
- @doc api_doc: [summary: "Get Device by ID"]
- def show(conn, %{"id" => id}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, device} <- Devices.fetch_device_by_id(id, subject) do
- defaults = Devices.defaults()
- render(conn, "show.json", device: device, defaults: defaults)
- end
- end
-
- @doc api_doc: [summary: "Update a Device"]
- def update(conn, %{"id" => id, "device" => attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, device} <- Devices.fetch_device_by_id(id, subject),
- {:ok, device} <- Devices.update_device(device, attrs, subject) do
- defaults = Devices.defaults()
- render(conn, "show.json", device: device, defaults: defaults)
- end
- end
-
- @doc api_doc: [summary: "Delete a Device"]
- def delete(conn, %{"id" => id}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, device} <- Devices.fetch_device_by_id(id, subject),
- {:ok, _device} <- Devices.delete_device(device, subject) do
- send_resp(conn, :no_content, "")
- end
- end
-end
diff --git a/apps/web/lib/web/controllers/json/fallback_controller.ex b/apps/web/lib/web/controllers/json/fallback_controller.ex
deleted file mode 100644
index 0dcb9cd5a..000000000
--- a/apps/web/lib/web/controllers/json/fallback_controller.ex
+++ /dev/null
@@ -1,44 +0,0 @@
-defmodule Web.JSON.FallbackController do
- @moduledoc """
- Translates controller action results into valid `Plug.Conn` responses.
-
- See `Phoenix.Controller.action_fallback/1` for more details.
- """
- use Web, :controller
-
- # This clause is an example of how to handle resources that cannot be found.
- def call(conn, {:error, :not_found}) do
- conn
- |> put_status(:not_found)
- |> put_view(Web.ErrorView)
- |> render("404.json")
- end
-
- def call(conn, {:error, :unauthorized}) do
- conn
- |> put_status(:unauthorized)
- |> put_view(Web.ErrorView)
- |> render("401.json")
- end
-
- def call(conn, {:error, {:unauthorized, _context}}) do
- conn
- |> put_status(:unauthorized)
- |> put_view(Web.ErrorView)
- |> render("401.json")
- end
-
- def call(conn, {:error, :internal_server_error}) do
- conn
- |> put_status(:internal_server_error)
- |> put_view(Web.ErrorView)
- |> render("500.json")
- end
-
- def call(conn, {:error, %Ecto.Changeset{valid?: false} = changeset}) do
- conn
- |> put_status(422)
- |> put_view(Web.JSON.ChangesetView)
- |> render("error.json", changeset: changeset)
- end
-end
diff --git a/apps/web/lib/web/controllers/json/rule_controller.ex b/apps/web/lib/web/controllers/json/rule_controller.ex
deleted file mode 100644
index d8bdef9d3..000000000
--- a/apps/web/lib/web/controllers/json/rule_controller.ex
+++ /dev/null
@@ -1,61 +0,0 @@
-defmodule Web.JSON.RuleController do
- @moduledoc api_doc: [title: "Rules", group: "Rules"]
- @moduledoc """
- This endpoint allows an adminisrator to manage Rules.
- """
- use Web, :controller
- alias Domain.Rules
- alias Web.Auth.JSON.Authentication
-
- action_fallback(Web.JSON.FallbackController)
-
- @doc api_doc: [summary: "List all Rules"]
- def index(conn, _params) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, rules} <- Rules.list_rules(subject) do
- render(conn, "index.json", rules: rules)
- end
- end
-
- @doc api_doc: [summary: "Create a Rule"]
- def create(conn, %{"rule" => attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, rule} <- Rules.create_rule(attrs, subject) do
- conn
- |> put_status(:created)
- |> put_resp_header("location", ~p"/v0/rules/#{rule}")
- |> render("show.json", rule: rule)
- end
- end
-
- @doc api_doc: [summary: "Get Rule by ID"]
- def show(conn, %{"id" => id}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, rule} <- Rules.fetch_rule_by_id(id, subject) do
- render(conn, "show.json", rule: rule)
- end
- end
-
- @doc api_doc: [summary: "Update a Rule"]
- def update(conn, %{"id" => id, "rule" => attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, rule} <- Rules.fetch_rule_by_id(id, subject),
- {:ok, rule} <- Rules.update_rule(rule, attrs, subject) do
- render(conn, "show.json", rule: rule)
- end
- end
-
- @doc api_doc: [summary: "Delete a Rule"]
- def delete(conn, %{"id" => id}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, rule} <- Rules.fetch_rule_by_id(id, subject),
- {:ok, _rule} <- Rules.delete_rule(rule, subject) do
- send_resp(conn, :no_content, "")
- end
- end
-end
diff --git a/apps/web/lib/web/controllers/json/user_controller.ex b/apps/web/lib/web/controllers/json/user_controller.ex
deleted file mode 100644
index 6bf48dceb..000000000
--- a/apps/web/lib/web/controllers/json/user_controller.ex
+++ /dev/null
@@ -1,116 +0,0 @@
-defmodule Web.JSON.UserController do
- @moduledoc api_doc: [title: "Users", sidebar_position: 2, toc_max_heading_level: 4]
- @moduledoc """
- This endpoint allows an administrator to manage Users.
-
- ## Auto-Create Users from OpenID or SAML providers
-
- You can set Configuration option `auto_create_users` to `true` to automatically create users
- from OpenID or SAML providers. Use it with care as anyone with access to the provider will be
- able to log-in to Firezone.
-
- If `auto_create_users` is `false`, then you need to provision users with `password` attribute,
- otherwise they will have no means to log in.
-
- ## Disabling users
-
- Even though API returns `disabled_at` attribute, currently, it's not possible to disable users via API,
- since this field is only for internal use by automatic user disabling mechanism on OIDC/SAML errors.
- """
- use Web, :controller
- alias Domain.Users
- alias Web.Auth.JSON.Authentication
-
- action_fallback(Web.JSON.FallbackController)
-
- @doc api_doc: [action: "List all Users"]
- def index(conn, _attrs) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, users} <- Users.list_users(subject) do
- render(conn, "index.json", users: users)
- end
- end
-
- @doc """
- Create a new User.
-
- This endpoint is useful in two cases:
-
- 1. When [Local Authentication](/docs/authenticate/local-auth/) is enabled (discouraged in
- production deployments), it allows an administrator to provision users with their passwords;
- 2. When `auto_create_users` in the associated OpenID or SAML configuration is disabled,
- it allows an administrator to provision users with their emails beforehand, effectively
- whitelisting specific users for authentication.
-
- If `auto_create_users` is `true` in the associated OpenID or SAML configuration, there is no need
- to provision users; they will be created automatically when they log in for the first time using
- the associated OpenID or SAML provider.
-
- #### User Attributes
-
- | Attribute | Type | Required | Description |
- | --------- | ---- | -------- | ----------- |
- | `role` | `admin` or `unprivileged` (default) | No | User role. |
- | `email` | `string` | Yes | Email which will be used to identify the user. |
- | `password` | `string` | No | A password that can be used for login-password authentication. |
- | `password_confirmation` | `string` | -> | Is required when the `password` is set. |
- """
- @doc api_doc: [action: "Create a User"]
- def create(conn, %{"user" => %{"role" => "admin"} = attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, %Users.User{} = user} <- Users.create_user(:admin, attrs, subject) do
- conn
- |> put_status(:created)
- |> put_resp_header("location", ~p"/v0/users/#{user}")
- |> render("show.json", user: user)
- end
- end
-
- def create(conn, %{"user" => attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, %Users.User{} = user} <- Users.create_user(:unprivileged, attrs, subject) do
- conn
- |> put_status(:created)
- |> put_resp_header("location", ~p"/v0/users/#{user}")
- |> render("show.json", user: user)
- end
- end
-
- @doc api_doc: [summary: "Get User by ID or Email"]
- def show(conn, %{"id" => id_or_email}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, %Users.User{} = user} <-
- Users.fetch_user_by_id_or_email(id_or_email, subject) do
- render(conn, "show.json", user: user)
- end
- end
-
- @doc """
- For details please see [Create a User](#create-a-user-post-v0users) section.
- """
- @doc api_doc: [action: "Update a User"]
- def update(conn, %{"id" => id_or_email, "user" => attrs}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, %Users.User{} = user} <- Users.fetch_user_by_id_or_email(id_or_email, subject),
- {:ok, %Users.User{} = user} <- Users.update_user(user, attrs, subject) do
- render(conn, "show.json", user: user)
- end
- end
-
- @doc api_doc: [summary: "Delete a User"]
- def delete(conn, %{"id" => id_or_email}) do
- subject = Authentication.get_current_subject(conn)
-
- with {:ok, %Users.User{} = user} <- Users.fetch_user_by_id_or_email(id_or_email, subject),
- {:ok, %Users.User{}} <- Users.delete_user(user, subject) do
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(:no_content, "")
- end
- end
-end
diff --git a/apps/web/lib/web/controllers/page_controller.ex b/apps/web/lib/web/controllers/page_controller.ex
new file mode 100644
index 000000000..835cfab44
--- /dev/null
+++ b/apps/web/lib/web/controllers/page_controller.ex
@@ -0,0 +1,9 @@
+defmodule Web.PageController do
+ use Web, :controller
+
+ def home(conn, _params) do
+ # The home page is often custom made,
+ # so skip the default app layout.
+ render(conn, :home, layout: false)
+ end
+end
diff --git a/apps/web/lib/web/controllers/page_html.ex b/apps/web/lib/web/controllers/page_html.ex
new file mode 100644
index 000000000..608e6bfa1
--- /dev/null
+++ b/apps/web/lib/web/controllers/page_html.ex
@@ -0,0 +1,5 @@
+defmodule Web.PageHTML do
+ use Web, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/apps/web/lib/web/controllers/page_html/home.html.heex b/apps/web/lib/web/controllers/page_html/home.html.heex
new file mode 100644
index 000000000..6a7480de6
--- /dev/null
+++ b/apps/web/lib/web/controllers/page_html/home.html.heex
@@ -0,0 +1,237 @@
+<.flash_group flash={@flash} />
+
+
+
+
+
+
+
+ Phoenix Framework
+
+ v1.7
+
+
+
+ Peace of mind from prototype to production.
+
+
+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
+
+
+
+
diff --git a/apps/web/lib/web/controllers/root_controller.ex b/apps/web/lib/web/controllers/root_controller.ex
deleted file mode 100644
index 735f26d48..000000000
--- a/apps/web/lib/web/controllers/root_controller.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Web.RootController do
- @moduledoc """
- Firezone landing page -- show auth methods.
- """
- use Web, :controller
-
- def index(conn, _params) do
- %{
- local_auth_enabled: {_, local_auth_enabled},
- openid_connect_providers: {_, openid_connect_providers},
- saml_identity_providers: {_, saml_identity_providers}
- } =
- Domain.Config.fetch_source_and_configs!([
- :local_auth_enabled,
- :openid_connect_providers,
- :saml_identity_providers
- ])
-
- conn
- |> render(
- "auth.html",
- local_enabled: local_auth_enabled,
- openid_connect_providers: openid_connect_providers,
- saml_identity_providers: saml_identity_providers
- )
- end
-end
diff --git a/apps/web/lib/web/controllers/user_controller.ex b/apps/web/lib/web/controllers/user_controller.ex
deleted file mode 100644
index 28fc739b7..000000000
--- a/apps/web/lib/web/controllers/user_controller.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-defmodule Web.UserController do
- @moduledoc """
- Implements synchronous User requests.
- """
- use Web, :controller
- alias Domain.Users
- alias Web.Auth.HTML.Authentication
- require Logger
-
- def delete(conn, _params) do
- %{actor: {:user, user}} = subject = Authentication.get_current_subject(conn)
-
- case Users.delete_user(user, subject) do
- {:ok, _user} ->
- Web.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
- Authentication.sign_out(conn)
-
- {:error, :cant_delete_the_last_admin} ->
- conn
- |> put_status(:unprocessable_entity)
- |> put_view(Web.ErrorView)
- |> render("422.json", reason: "Can't delete the last admin user.")
-
- {:error, %Ecto.Changeset{errors: [id: {"is stale", _}]}} ->
- not_found(conn)
-
- {:error, {:unauthorized, _context}} ->
- not_found(conn)
-
- {:error, :unauthorized} ->
- not_found(conn)
- end
- end
-
- defp not_found(conn) do
- conn
- |> put_status(:not_found)
- |> put_view(Web.ErrorView)
- |> render("404.json")
- end
-end
diff --git a/apps/web/lib/web/endpoint.ex b/apps/web/lib/web/endpoint.ex
index 0edfc8bfd..a093b319c 100644
--- a/apps/web/lib/web/endpoint.ex
+++ b/apps/web/lib/web/endpoint.ex
@@ -1,22 +1,5 @@
defmodule Web.Endpoint do
use Phoenix.Endpoint, otp_app: :web
- alias Web.ProxyHeaders
- alias Web.HeaderHelpers
- alias Web.Session
-
- plug Web.Plug.PathPrefix
-
- if Application.compile_env(:web, :sql_sandbox) do
- plug Phoenix.Ecto.SQL.Sandbox
- end
-
- socket "/socket", Web.UserSocket,
- websocket: [
- connect_info: [:user_agent, :peer_data, :x_headers, :uri],
- # XXX: channel token should prevent CSWH but double check
- check_origin: false
- ],
- longpoll: false
socket "/live", Phoenix.LiveView.Socket,
websocket: [
@@ -25,10 +8,8 @@ defmodule Web.Endpoint do
:peer_data,
:x_headers,
:uri,
- session: {Session, :options, []}
- ],
- # XXX: csrf token should prevent CSWH but double check
- check_origin: false
+ session: {Web.Session, :options, []}
+ ]
],
longpoll: false
@@ -45,20 +26,18 @@ defmodule Web.Endpoint do
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
- socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket,
- websocket: [
- connect_info: [
- session: {Session, :options, []}
- ],
- # XXX: csrf token should prevent CSWH but double check
- check_origin: false
- ]
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :domain
end
+ plug RemoteIp,
+ headers: ["x-forwarded-for"],
+ proxies: {__MODULE__, :external_trusted_proxies, []},
+ clients: {__MODULE__, :clients, []}
+
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
@@ -69,15 +48,19 @@ defmodule Web.Endpoint do
plug Plug.MethodOverride
plug Plug.Head
- plug(:session)
- if HeaderHelpers.proxied?() do
- plug ProxyHeaders
- end
+ # TODO: ensure that phoenix configured to resolve opts at runtime
+ plug Plug.Session, Web.Session.options()
plug Web.Router
- defp session(conn, _opts) do
- Plug.Session.call(conn, Plug.Session.init(Session.options()))
+ def external_trusted_proxies do
+ Domain.Config.fetch_env!(:web, :external_trusted_proxies)
+ |> Enum.map(&to_string/1)
+ end
+
+ def clients do
+ Domain.Config.fetch_env!(:web, :private_clients)
+ |> Enum.map(&to_string/1)
end
end
diff --git a/apps/web/lib/web/error_helpers.ex b/apps/web/lib/web/error_helpers.ex
deleted file mode 100644
index a20466198..000000000
--- a/apps/web/lib/web/error_helpers.ex
+++ /dev/null
@@ -1,80 +0,0 @@
-defmodule Web.ErrorHelpers do
- @moduledoc """
- Conveniences for translating and building error messages.
- """
- use Phoenix.HTML
- import Ecto.Changeset, only: [traverse_errors: 2]
-
- def aggregated_errors(%Ecto.Changeset{} = changeset) do
- traverse_errors(changeset, fn {msg, opts} ->
- Enum.reduce(opts, msg, fn
- {key, {:array, value}}, acc ->
- String.replace(acc, "%{#{key}}", to_string(value))
-
- {key, value}, acc ->
- String.replace(acc, "%{#{key}}", to_string(value))
- end)
- end)
- |> Enum.reduce("", fn {key, value}, acc ->
- joined_errors = Enum.join(value, "; ")
- "#{acc}#{key}: #{joined_errors}\n"
- end)
- end
-
- @doc """
- Generates tag for inlined form input errors.
- """
- def error_tag(form, field) do
- values = Keyword.get_values(form.errors, field)
-
- values
- |> Enum.map(fn error ->
- content_tag(:span, translate_error(error),
- class: "help-block"
- # XXX: data: [phx_error_for: input_id(form, field)]
- )
- end)
- |> Enum.intersperse(", ")
- end
-
- @doc """
- Adds "is-danger" to input elements that have errors
- """
- def input_error_class(form, field) do
- case Keyword.get_values(form.errors, field) do
- [] ->
- ""
-
- _ ->
- "is-danger"
- end
- end
-
- @doc """
- Translates an error message using gettext.
- """
- def translate_error({msg, opts}) do
- # When using gettext, we typically pass the strings we want
- # to translate as a static argument:
- #
- # # Translate "is invalid" in the "errors" domain
- # dgettext("errors", "is invalid")
- #
- # # Translate the number of files with plural rules
- # dngettext("errors", "1 file", "%{count} files", count)
- #
- # Because the error messages we show in our forms and APIs
- # are defined inside Ecto, we need to translate them dynamically.
- # This requires us to call the Gettext module passing our gettext
- # backend as first argument.
- #
- # Note we use the "errors" domain, which means translations
- # should be written to the errors.po file. The :count option is
- # set by Ecto and indicates we should also apply plural rules.
- if count = opts[:count] do
- Gettext.dngettext(Web.Gettext, "errors", msg, msg, count, opts)
- else
- Gettext.dgettext(Web.Gettext, "errors", msg, opts)
- end
- end
-end
diff --git a/apps/web/lib/web/gettext.ex b/apps/web/lib/web/gettext.ex
index 6c93d0608..488e07d45 100644
--- a/apps/web/lib/web/gettext.ex
+++ b/apps/web/lib/web/gettext.ex
@@ -1,24 +1,3 @@
defmodule Web.Gettext do
- @moduledoc """
- A module providing Internationalization with a gettext-based API.
-
- By using [Gettext](https://hexdocs.pm/gettext),
- your module gains a set of macros for translations, for example:
-
- import Web.Gettext
-
- # Simple translation
- gettext("Here is the string to translate")
-
- # Plural translation
- ngettext("Here is the string to translate",
- "Here are the strings to translate",
- 3)
-
- # Domain-based translation
- dgettext("errors", "Here is the error message to translate")
-
- See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
- """
use Gettext, otp_app: :web
end
diff --git a/apps/web/lib/web/header_helpers.ex b/apps/web/lib/web/header_helpers.ex
deleted file mode 100644
index 99609fca0..000000000
--- a/apps/web/lib/web/header_helpers.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Web.HeaderHelpers do
- @moduledoc """
- Helper functionalities with regards to headers
- """
-
- @remote_ip_headers ["x-forwarded-for"]
-
- def external_trusted_proxies do
- Domain.Config.fetch_env!(:web, :external_trusted_proxies)
- |> Enum.map(&to_string/1)
- end
-
- def clients do
- Domain.Config.fetch_env!(:web, :private_clients)
- |> Enum.map(&to_string/1)
- end
-
- def proxied?, do: external_trusted_proxies() != []
-
- def remote_ip_opts do
- [
- headers: @remote_ip_headers,
- proxies: external_trusted_proxies(),
- clients: clients()
- ]
- end
-end
diff --git a/apps/web/lib/web/live/connectivity_check_live/index.html.heex b/apps/web/lib/web/live/connectivity_check_live/index.html.heex
deleted file mode 100644
index 7d7a9d337..000000000
--- a/apps/web/lib/web/live/connectivity_check_live/index.html.heex
+++ /dev/null
@@ -1,42 +0,0 @@
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
-
-
-
- Checked At
- Resolved IP
- Status
-
-
-
- <%= for connectivity_check <- @connectivity_checks do %>
-
-
- …
-
- <%= connectivity_check.response_body %>
-
-
-
-
-
-
- <% end %>
-
-
-
-
diff --git a/apps/web/lib/web/live/connectivity_check_live/index_live.ex b/apps/web/lib/web/live/connectivity_check_live/index_live.ex
deleted file mode 100644
index 64f7bcae3..000000000
--- a/apps/web/lib/web/live/connectivity_check_live/index_live.ex
+++ /dev/null
@@ -1,29 +0,0 @@
-defmodule Web.ConnectivityCheckLive.Index do
- @moduledoc """
- Manages the connectivity_checks view.
- """
- use Web, :live_view
-
- alias Domain.ConnectivityChecks
-
- @page_title "WAN Connectivity Checks"
- @page_subtitle """
- 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.
- """
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- connectivity_checks =
- ConnectivityChecks.list_connectivity_checks(socket.assigns.subject, limit: 20)
-
- socket =
- socket
- |> assign(:connectivity_checks, connectivity_checks)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:page_title, @page_title)
-
- {:ok, socket}
- end
-end
diff --git a/apps/web/lib/web/live/device_live/admin/index.html.heex b/apps/web/lib/web/live/device_live/admin/index.html.heex
deleted file mode 100644
index 312b8768c..000000000
--- a/apps/web/lib/web/live/device_live/admin/index.html.heex
+++ /dev/null
@@ -1,22 +0,0 @@
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
- <%= render(Web.SharedView, "devices_table.html",
- devices: @devices,
- show_user: true,
- socket: @socket
- ) %>
-
-
-
- Devices can be added when viewing a User. <%= live_redirect("Go to users ->",
- to: ~p"/users"
- ) %>
-
-
diff --git a/apps/web/lib/web/live/device_live/admin/index_live.ex b/apps/web/lib/web/live/device_live/admin/index_live.ex
deleted file mode 100644
index 695bab35e..000000000
--- a/apps/web/lib/web/live/device_live/admin/index_live.ex
+++ /dev/null
@@ -1,38 +0,0 @@
-defmodule Web.DeviceLive.Admin.Index do
- @moduledoc """
- Handles Device LiveViews.
- """
- use Web, :live_view
- alias Domain.{Devices, Repo}
-
- @page_title "All Devices"
- @page_subtitle """
- Each device corresponds to a WireGuard configuration for connecting to this Firezone server.
- """
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- with {:ok, devices} <- Devices.list_devices(socket.assigns.subject) do
- devices =
- devices
- |> Repo.preload(:user)
- |> Enum.sort_by(& &1.user_id)
-
- socket =
- socket
- |> assign(:devices, devices)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:page_title, @page_title)
-
- {:ok, socket}
- end
- end
-
- @doc """
- Needed because this view will receive handle_params when modal is closed.
- """
- @impl Phoenix.LiveView
- def handle_params(_params, _url, socket) do
- {:noreply, socket}
- end
-end
diff --git a/apps/web/lib/web/live/device_live/admin/show.html.heex b/apps/web/lib/web/live/device_live/admin/show.html.heex
deleted file mode 100644
index bac77952e..000000000
--- a/apps/web/lib/web/live/device_live/admin/show.html.heex
+++ /dev/null
@@ -1,2 +0,0 @@
-<%= render(Web.SharedView, "heading.html", page_title: "Devices |> #{@page_title}") %>
-<%= render(Web.SharedView, "show_device.html", assigns) %>
diff --git a/apps/web/lib/web/live/device_live/admin/show_live.ex b/apps/web/lib/web/live/device_live/admin/show_live.ex
deleted file mode 100644
index 2d386b907..000000000
--- a/apps/web/lib/web/live/device_live/admin/show_live.ex
+++ /dev/null
@@ -1,59 +0,0 @@
-defmodule Web.DeviceLive.Admin.Show do
- @moduledoc """
- Shows a device for an admin user.
- """
- use Web, :live_view
- alias Domain.{Devices, Users}
-
- @impl Phoenix.LiveView
- def mount(%{"id" => device_id} = _params, _session, socket) do
- with {:ok, device} <- Devices.fetch_device_by_id(device_id, socket.assigns.subject) do
- {:ok, assign(socket, assigns(device))}
- else
- {:error, {:unauthorized, _context}} ->
- {:ok, not_authorized(socket)}
-
- {:error, :not_found} ->
- {:ok, not_authorized(socket)}
- end
- end
-
- @doc """
- Needed because this view will receive handle_params when modal is closed.
- """
- @impl Phoenix.LiveView
- def handle_params(_params, _url, socket) do
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete_device", _params, socket) do
- case Devices.delete_device(socket.assigns.device, socket.assigns.subject) do
- {:ok, _deleted_device} ->
- {:noreply, redirect(socket, to: ~p"/devices")}
-
- {:error, {:unauthorized, _context}} ->
- {:noreply, not_authorized(socket)}
-
- {:error, msg} ->
- {:noreply, put_flash(socket, :error, "Error deleting device: #{msg}")}
- end
- end
-
- defp assigns(device) do
- defaults = Devices.defaults()
-
- [
- device: device,
- user: Users.fetch_user_by_id!(device.user_id),
- page_title: device.name,
- allowed_ips: Devices.get_allowed_ips(device, defaults),
- dns: Devices.get_dns(device, defaults),
- endpoint: Devices.get_endpoint(device, defaults),
- port: Domain.Config.fetch_env!(:domain, :wireguard_port),
- mtu: Devices.get_mtu(device, defaults),
- persistent_keepalive: Devices.get_persistent_keepalive(device, defaults),
- config: Web.WireguardConfigView.render("device.conf", %{device: device})
- ]
- end
-end
diff --git a/apps/web/lib/web/live/device_live/new_form_component.ex b/apps/web/lib/web/live/device_live/new_form_component.ex
deleted file mode 100644
index 870f198a0..000000000
--- a/apps/web/lib/web/live/device_live/new_form_component.ex
+++ /dev/null
@@ -1,113 +0,0 @@
-defmodule Web.DeviceLive.NewFormComponent do
- @moduledoc """
- Handles device form.
- """
- use Web, :live_component
- alias Domain.Devices
- alias Web.ErrorHelpers
-
- @impl Phoenix.LiveComponent
- def mount(socket) do
- socket =
- socket
- |> assign(:device, nil)
- |> assign(:config, nil)
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveComponent
- def update(assigns, socket) do
- changeset = Devices.new_device()
-
- config =
- Domain.Config.fetch_source_and_configs!(~w(
- default_client_mtu
- default_client_endpoint
- default_client_persistent_keepalive
- default_client_dns
- default_client_allowed_ips
- )a)
- |> Enum.into(%{}, fn {k, {_s, v}} -> {k, v} end)
-
- socket =
- socket
- |> assign(assigns)
- |> assign(config)
- |> assign_new(:changeset, fn -> changeset end)
- |> assign(use_default_fields(changeset))
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("change", %{"device" => device_params}, socket) do
- attrs =
- device_params
- |> Map.update("dns", nil, &binary_to_list/1)
- |> Map.update("allowed_ips", nil, &binary_to_list/1)
-
- # Note: change_device is used here because when you type in at some point
- # the input can be empty while you typing, which will immediately put back
- # an new default value from the changeset.
- changeset = Devices.change_device(%Devices.Device{}, attrs)
-
- socket =
- socket
- |> assign(:changeset, changeset)
- |> assign(use_default_fields(changeset))
-
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("save", %{"device" => device_params}, socket) do
- device_params
- |> Map.update("dns", nil, &binary_to_list/1)
- |> Map.update("allowed_ips", nil, &binary_to_list/1)
- |> create_device(socket)
- |> case do
- {:ok, device} ->
- send_update(Web.ModalComponent, id: :modal, hide_footer_content: true)
-
- device_config = Web.WireguardConfigView.render("base64_device.conf", %{device: device})
-
- socket =
- socket
- |> assign(:device, device)
- |> assign(:config, device_config)
-
- {:noreply, socket}
-
- {:error, {:unauthorized, _context}} ->
- {:noreply, not_authorized(socket)}
-
- {:error, changeset} ->
- {:noreply,
- socket
- |> put_flash(:error, ErrorHelpers.aggregated_errors(changeset))
- |> assign(:changeset, changeset)}
- end
- end
-
- defp use_default_fields(changeset) do
- ~w(
- use_default_allowed_ips
- use_default_dns
- use_default_endpoint
- use_default_mtu
- use_default_persistent_keepalive
- )a
- |> Map.new(&{&1, Ecto.Changeset.get_field(changeset, &1)})
- end
-
- defp create_device(attrs, socket) do
- Devices.create_device_for_user(socket.assigns.user, attrs, socket.assigns.subject)
- end
-
- defp binary_to_list(binary) when is_binary(binary),
- do: binary |> String.trim() |> String.split(",")
-
- defp binary_to_list(list) when is_list(list),
- do: list
-end
diff --git a/apps/web/lib/web/live/device_live/new_form_component.html.heex b/apps/web/lib/web/live/device_live/new_form_component.html.heex
deleted file mode 100644
index 3fd98d882..000000000
--- a/apps/web/lib/web/live/device_live/new_form_component.html.heex
+++ /dev/null
@@ -1,291 +0,0 @@
-
- <%= if @device && @config do %>
-
-
- Device added!
-
-
-
- To use this configuration, you'll need a WireGuard client installed
- for your device. See
-
- the Firezone documentation
-
- for a step-by-step guide.
-
-
-
- NOTE:
- This configuration WILL NOT
- be viewable again. Please ensure you've downloaded the
- configuration file or copied it somewhere safe
- before closing this window.
-
-
-
-
Rendering configuration...
-
-
-
-
-
- Download WireGuard Configuration
-
-
-
-
- Generating QR code...
-
-
-
-
-
-
- <% else %>
-
- <.form
- :let={f}
- for={@changeset}
- id="create-device"
- phx-change="change"
- phx-target={@myself}
- phx-submit="save"
- >
- <%= hidden_input(f, :public_key, id: "device-public-key", phx_hook: "GenerateKeyPair") %>
- <%= hidden_input(f, :preshared_key) %>
-
- <%= if @changeset.action do %>
-
-
- <%= error_tag(f, :base) %>
-
-
- <% end %>
-
-
- <%= label(f, :name, class: "label") %>
-
- <%= text_input(f, :name, class: "input #{input_error_class(f, :name)}") %>
-
-
- <%= error_tag(f, :name) %>
-
-
-
-
- <%= label(f, :description, class: "label") %>
-
- <%= textarea(
- f,
- :description,
- placeholder: "Enter an optional description for this device",
- class: "pre-wrapped input #{input_error_class(f, :description)}"
- ) %>
-
-
- <%= error_tag(f, :description) %>
-
-
-
- <%= if Domain.Devices.authorize_device_configuration(@subject) == :ok do %>
-
- <%= label(f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label") %>
-
-
- <%= radio_button(f, :use_default_allowed_ips, true) %> Yes
-
-
- <%= radio_button(f, :use_default_allowed_ips, false) %> No
-
-
-
- Default: <%= Enum.join(@default_client_allowed_ips, ", ") %>
-
-
-
-
- <%= label(f, :allowed_ips, "Allowed IPs", class: "label") %>
-
- <%= textarea(f, :allowed_ips,
- class: "textarea #{input_error_class(f, :allowed_ips)}",
- disabled: @use_default_allowed_ips,
- value: list_value(f, :allowed_ips)
- ) %>
-
-
- <%= error_tag(f, :allowed_ips) %>
-
-
-
-
- <%= label(f, :use_default_dns, "Use Default DNS Servers", class: "label") %>
-
-
- <%= radio_button(f, :use_default_dns, true) %> Yes
-
-
- <%= radio_button(f, :use_default_dns, false) %> No
-
-
-
- Default: <%= Enum.join(@default_client_dns, ", ") %>
-
-
-
-
- <%= label(f, :dns, "DNS Servers", class: "label") %>
-
- <%= text_input(f, :dns,
- class: "input #{input_error_class(f, :dns)}",
- disabled: @use_default_dns,
- value: list_value(f, :dns)
- ) %>
-
-
- <%= error_tag(f, :dns) %>
-
-
-
-
- <%= 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_client_endpoint %>
-
-
-
-
- <%= label(f, :endpoint, "Server Endpoint", class: "label") %>
-
The IP of the server this device should connect to.
-
- <%= text_input(f, :endpoint,
- class: "input #{input_error_class(f, :endpoint)}",
- disabled: @use_default_endpoint
- ) %>
-
-
- <%= error_tag(f, :endpoint) %>
-
-
-
-
- <%= label(f, :use_default_mtu, "Use Default MTU", class: "label") %>
-
-
- <%= radio_button(f, :use_default_mtu, true) %> Yes
-
-
- <%= radio_button(f, :use_default_mtu, false) %> No
-
-
-
- Default: <%= @default_client_mtu %>
-
-
-
-
- <%= label(f, :mtu, "Interface MTU", class: "label") %>
-
The WireGuard interface MTU for this Device.
-
- <%= text_input(f, :mtu,
- class: "input #{input_error_class(f, :mtu)}",
- disabled: @use_default_mtu
- ) %>
-
-
- <%= error_tag(f, :mtu) %>
-
-
-
-
- <%= label(f, :use_default_persistent_keepalive, "Use Default Persistent Keepalive",
- class: "label"
- ) %>
-
-
- <%= radio_button(f, :use_default_persistent_keepalive, true) %> Yes
-
-
- <%= radio_button(f, :use_default_persistent_keepalive, false) %> No
-
-
-
- Default: <%= @default_client_persistent_keepalive %>
-
-
-
-
- <%= label(f, :persistent_keepalive, "Persistent Keepalive", class: "label") %>
-
- Interval for WireGuard
- persistent keepalive . A value of 0 disables this. Leave this disabled
- unless you're experiencing NAT or firewall traversal problems.
-
-
- <%= text_input(f, :persistent_keepalive,
- class: "input #{input_error_class(f, :persistent_keepalive)}",
- disabled: @use_default_persistent_keepalive
- ) %>
-
-
- <%= error_tag(f, :persistent_keepalive) %>
-
-
-
-
- <%= label(f, :ipv4, "Tunnel IPv4 Address", class: "label") %>
-
-
- <%= text_input(
- f,
- :ipv4,
- placeholder: "Leave blank to let Firezone assign an IPv4 address",
- class: "input #{input_error_class(f, :ipv4)}"
- ) %>
-
-
- <%= error_tag(f, :ipv4) %>
-
-
-
-
- <%= label(f, :ipv6, "Tunnel IPv6 Address", class: "label") %>
-
-
- <%= text_input(
- f,
- :ipv6,
- placeholder: "Leave blank to let Firezone assign an IPv6 address",
- class: "input #{input_error_class(f, :ipv6)}"
- ) %>
-
-
- <%= error_tag(f, :ipv6) %>
-
-
- <% end %>
-
- <%= if @changeset.action do %>
-
-
- Error creating device. Scroll up to view fields with errors.
-
-
- <% end %>
-
-
- <% end %>
-
diff --git a/apps/web/lib/web/live/device_live/unprivileged/index.html.heex b/apps/web/lib/web/live/device_live/unprivileged/index.html.heex
deleted file mode 100644
index 5a3d5326f..000000000
--- a/apps/web/lib/web/live/device_live/unprivileged/index.html.heex
+++ /dev/null
@@ -1,134 +0,0 @@
-<%= if @live_action == :new do %>
- <%= live_modal(
- Web.DeviceLive.NewFormComponent,
- return_to: ~p"/user_devices",
- title: "Add Device",
- current_user: @current_user,
- user: @current_user,
- id: "create-device-component",
- form: "create-device",
- button_text: "Generate Configuration",
- subject: @subject
- ) %>
-<% end %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
- <%= @page_title %>
-
-
- <%= @page_subtitle %>
-
-
-
- You'll need a WireGuard client installed in order to connect to this Firezone
- instance. See
-
- the Firezone documentation
-
- for a step-by-step guide.
-
-
-
- <%= unless Enum.empty?(@devices) do %>
-
-
-
- Name
- Tunnel IPv4
- Tunnel IPv6
- Public key
- Created
-
-
-
- <%= for device <- @devices do %>
-
-
- <.link navigate={~p"/user_devices/#{device}"}>
- <%= device.name %>
-
-
- <%= device.ipv4 %>
- <%= device.ipv6 %>
- <%= device.public_key %>
-
- …
-
-
- <% end %>
-
-
- <% else %>
-
- No devices to show.
-
- <% end %>
-
-
- <%= if Domain.Devices.authorize_user_device_management(@current_user.id, @subject) == :ok do %>
-
- <.link replace={true} patch={~p"/user_devices/new"} class="button">
- Add Device
-
-
- <% end %>
-
- VPN Session
-
-
-
-
- <%= if vpn_sessions_expire?() do %>
- <%= if vpn_expired?(@current_user) do %>
-
- Your VPN session expired at:
-
- <% else %>
-
- Your VPN session expires at:
-
- <% end %>
-
-
-
- ...
-
-
-
- <%= link("Reauthenticate", to: ~p"/sign_out", method: :delete) %> to renew your VPN session.
- <% else %>
- Your VPN session is active indefinitely.
- <% end %>
-
-
-
-
-
-
- Signed in as <%= @current_user.email %>.
-
-
- <.link navigate={~p"/user_account"}>
- My Account
-
- /
- <%= link(to: ~p"/sign_out", method: :delete) do %>
- Sign out
- <% end %>
-
-
- For any issues, contact your Firezone administrator .
-
-
-
-
-
diff --git a/apps/web/lib/web/live/device_live/unprivileged/index_live.ex b/apps/web/lib/web/live/device_live/unprivileged/index_live.ex
deleted file mode 100644
index 6bf2b45a5..000000000
--- a/apps/web/lib/web/live/device_live/unprivileged/index_live.ex
+++ /dev/null
@@ -1,53 +0,0 @@
-defmodule Web.DeviceLive.Unprivileged.Index do
- @moduledoc """
- Handles Device LiveViews.
- """
- use Web, :live_view
- alias Domain.Devices
-
- @page_title "Your Devices"
- @page_subtitle """
- Each device corresponds to a WireGuard configuration for connecting to this Firezone server.
- """
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- with :ok <- authorize_socket_action(socket),
- {:ok, devices} <-
- Devices.list_devices_for_user(socket.assigns.current_user, socket.assigns.subject) do
- socket =
- socket
- |> assign(:devices, devices)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:page_title, @page_title)
-
- {:ok, socket}
- else
- {:error, {:unauthorized, _context}} ->
- {:ok, not_authorized(socket)}
- end
- end
-
- defp authorize_socket_action(%{assigns: %{live_action: :new}} = socket) do
- Devices.authorize_user_device_management(socket.assigns.current_user, socket.assigns.subject)
- end
-
- defp authorize_socket_action(_socket) do
- :ok
- end
-
- @doc """
- This is called when modal is closed. Conveniently, allows us to reload devices table.
- """
- @impl Phoenix.LiveView
- def handle_params(_params, _url, socket) do
- {:ok, devices} =
- Devices.list_devices_for_user(socket.assigns.current_user, socket.assigns.subject)
-
- socket =
- socket
- |> assign(:devices, devices)
-
- {:noreply, socket}
- end
-end
diff --git a/apps/web/lib/web/live/device_live/unprivileged/show.html.heex b/apps/web/lib/web/live/device_live/unprivileged/show.html.heex
deleted file mode 100644
index d219b5808..000000000
--- a/apps/web/lib/web/live/device_live/unprivileged/show.html.heex
+++ /dev/null
@@ -1,6 +0,0 @@
-
- <.link navigate={~p"/user_devices"}>
- <- Back to devices
-
-
-<%= render(Web.SharedView, "show_device.html", assigns) %>
diff --git a/apps/web/lib/web/live/device_live/unprivileged/show_live.ex b/apps/web/lib/web/live/device_live/unprivileged/show_live.ex
deleted file mode 100644
index cc832fab5..000000000
--- a/apps/web/lib/web/live/device_live/unprivileged/show_live.ex
+++ /dev/null
@@ -1,55 +0,0 @@
-defmodule Web.DeviceLive.Unprivileged.Show do
- @moduledoc """
- Shows a device for an unprivileged user.
- """
- use Web, :live_view
-
- alias Domain.Devices
- alias Domain.Users
-
- @impl Phoenix.LiveView
- def mount(%{"id" => device_id} = _params, _session, socket) do
- with {:ok, device} <- Devices.fetch_device_by_id(device_id, socket.assigns.subject) do
- {:ok, assign(socket, assigns(device))}
- else
- {:error, {:unauthorized, _}} ->
- {:ok, not_authorized(socket)}
-
- {:error, :not_found} ->
- {:ok, not_authorized(socket)}
- end
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete_device", _params, socket) do
- device = socket.assigns.device
-
- case Devices.delete_device(device, socket.assigns.subject) do
- {:ok, _deleted_device} ->
- {:noreply, redirect(socket, to: ~p"/user_devices")}
-
- {:error, {:unauthorized, _context}} ->
- {:noreply, not_authorized(socket)}
-
- {:error, msg} ->
- {:noreply, put_flash(socket, :error, "Error deleting device: #{msg}")}
- end
- end
-
- defp assigns(device) do
- defaults = Devices.defaults()
-
- [
- device: device,
- user: Users.fetch_user_by_id!(device.user_id),
- page_title: device.name,
- allowed_ips: Devices.get_allowed_ips(device, defaults),
- port: Domain.Config.fetch_env!(:domain, :wireguard_port),
- dns: Devices.get_dns(device, defaults),
- endpoint: Devices.get_endpoint(device, defaults),
- mtu: Devices.get_mtu(device, defaults),
- persistent_keepalive: Devices.get_persistent_keepalive(device, defaults),
- config: Web.WireguardConfigView.render("device.conf", %{device: device})
- ]
- end
-end
diff --git a/apps/web/lib/web/live/hooks/live_auth.ex b/apps/web/lib/web/live/hooks/live_auth.ex
deleted file mode 100644
index 6559c9145..000000000
--- a/apps/web/lib/web/live/hooks/live_auth.ex
+++ /dev/null
@@ -1,48 +0,0 @@
-defmodule Web.LiveAuth do
- @moduledoc """
- Handles loading default assigns and authorizing.
- """
- import Phoenix.Component
- import Web.AuthorizationHelpers
- alias Web.Auth.HTML.Authentication
- alias Domain.Auth
- require Logger
-
- def on_mount(role, _params, conn, socket) do
- case Authentication.get_current_subject(conn) do
- %Auth.Subject{actor: {:user, user}} = subject ->
- socket
- |> assign_new(:subject, fn -> subject end)
- |> assign_new(:current_user, fn -> user end)
- |> authorize_role(role)
-
- nil ->
- Logger.warn("Could not get_current_subject from session in LiveAuth.on_mount/4.")
- {:halt, not_authorized(socket)}
- end
- end
-
- def has_role?(_, :any) do
- true
- end
-
- def has_role?(%Phoenix.LiveView.Socket{} = socket, role) do
- socket.assigns.current_user && socket.assigns.current_user.role == role
- end
-
- def has_role?(%Domain.Users.User{} = user, role) do
- user.role == role
- end
-
- def has_role?(_, _) do
- false
- end
-
- defp authorize_role(socket, role) do
- if has_role?(socket, role) do
- {:cont, socket}
- else
- {:halt, not_authorized(socket)}
- end
- end
-end
diff --git a/apps/web/lib/web/live/hooks/live_mfa.ex b/apps/web/lib/web/live/hooks/live_mfa.ex
deleted file mode 100644
index 57faf7463..000000000
--- a/apps/web/lib/web/live/hooks/live_mfa.ex
+++ /dev/null
@@ -1,23 +0,0 @@
-defmodule Web.LiveMFA do
- @moduledoc """
- Guards content behind MFA
- """
- use Phoenix.Component
- use Web, :helper
- import Phoenix.LiveView
- alias Domain.Auth.MFA
-
- def on_mount(_arg, _params, %{"logged_in_at" => logged_in_at}, socket) do
- with {:ok, mfa} <- MFA.fetch_last_used_method_by_user_id(socket.assigns.current_user.id),
- true <- DateTime.compare(logged_in_at, mfa.last_used_at) == :gt do
- {:halt, redirect(socket, to: ~p"/mfa/auth/#{mfa.id}")}
- else
- {:error, :not_found} -> {:cont, socket}
- false -> {:cont, socket}
- end
- end
-
- def on_mount(_arg, _params, _session, socket) do
- {:halt, redirect(socket, to: ~p"/")}
- end
-end
diff --git a/apps/web/lib/web/live/hooks/live_nav.ex b/apps/web/lib/web/live/hooks/live_nav.ex
deleted file mode 100644
index 88326ba94..000000000
--- a/apps/web/lib/web/live/hooks/live_nav.ex
+++ /dev/null
@@ -1,21 +0,0 @@
-defmodule Web.LiveNav do
- @moduledoc """
- Handles admin navigation link highlight
- """
-
- use Phoenix.Component
- import Phoenix.LiveView
-
- def on_mount(nil, _params, _session, socket) do
- {:cont, assign(socket, path: nil)}
- end
-
- def on_mount(_role, _params, _session, socket) do
- {:cont, attach_hook(socket, :url, :handle_params, &assign_path/3)}
- end
-
- defp assign_path(_params, url, socket) do
- %{path: path} = URI.parse(url)
- {:cont, assign(socket, path: path)}
- end
-end
diff --git a/apps/web/lib/web/live/logo_component.ex b/apps/web/lib/web/live/logo_component.ex
deleted file mode 100644
index 3f76664b6..000000000
--- a/apps/web/lib/web/live/logo_component.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-defmodule Web.LogoComponent do
- @moduledoc """
- Logo component displays default, url and data logo
- """
- use Web, :live_component
- import Web.Endpoint, only: [static_path: 1]
-
- def render(%{url: url} = assigns) when is_binary(url) do
- ~H"""
-
- """
- end
-
- def render(%{file: file} = assigns) when is_binary(file) do
- ~H"""
- @file)} alt="Firezone App Logo" />
- """
- end
-
- def render(%{data: data, type: type} = assigns) when is_binary(data) and is_binary(type) do
- ~H"""
- @data} alt="Firezone App Logo" />
- """
- end
-
- def render(assigns) do
- ~H"""
-
- """
- end
-end
diff --git a/apps/web/lib/web/live/mfa_live/auth_live.ex b/apps/web/lib/web/live/mfa_live/auth_live.ex
deleted file mode 100644
index e5085eb21..000000000
--- a/apps/web/lib/web/live/mfa_live/auth_live.ex
+++ /dev/null
@@ -1,130 +0,0 @@
-defmodule Web.MFALive.Auth do
- @moduledoc """
- Handles MFA LiveViews.
- """
- use Web, :live_view
- import Web.ControllerHelpers
- alias Domain.Auth.MFA
-
- @page_title "Multi-factor Authentication"
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- {:ok, assign(socket, :page_title, @page_title)}
- end
-
- @impl Phoenix.LiveView
- def handle_params(_params, _uri, %{assigns: %{live_action: :types}} = socket) do
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
- socket = assign(socket, :methods, methods)
- {:noreply, socket}
- end
-
- def handle_params(%{"id" => id}, _uri, socket) do
- with {:ok, method} <- MFA.fetch_method_by_id(id) do
- changeset = MFA.use_method_changeset(method)
-
- socket =
- socket
- |> assign(:changeset, changeset)
- |> assign(:method, method)
-
- {:noreply, socket}
- else
- {:error, :not_found} -> {:halt, redirect(socket, to: ~p"/")}
- end
- end
-
- @impl Phoenix.LiveView
- def render(%{live_action: :auth} = assigns) do
- ~H"""
- <%= @page_title %>
-
-
- Authenticate with your configured MFA method.
-
-
-
-
-
- <.link navigate={~p"/mfa/types"}>
- Other authenticators ->
-
-
-
-
- <.form :let={f} for={@changeset} id="mfa-method-form" phx-submit="verify">
-
- <%= label(f, :code, class: "label") %>
-
- <%= text_input(f, :code,
- name: "code",
- placeholder: "123456",
- required: true,
- class: "input #{input_error_class(@changeset, :code)}"
- ) %>
-
- <%= error_tag(f, :code) %>
-
-
-
-
-
-
-
-
- <%= submit("Verify",
- phx_disable_with: "verifying...",
- class: "button"
- ) %>
-
-
- <%= link(to: ~p"/sign_out", method: :delete) do %>
- Sign out
- <% end %>
-
-
-
-
-
-
- """
- end
-
- @impl Phoenix.LiveView
- def render(%{live_action: :types} = assigns) do
- ~H"""
- <%= @page_title %>
-
-
- Select your MFA method:
-
-
-
-
- <%= for method <- @methods do %>
-
- <.link navigate={~p"/mfa/auth/#{method.id}"}>
- <%= "[#{method.type}] #{method.name} ->" %>
-
-
- <% end %>
-
-
- """
- end
-
- @impl Phoenix.LiveView
- def handle_event("verify", attrs, socket) do
- case MFA.use_method(socket.assigns.method, attrs) do
- {:ok, _method} ->
- root_path_for_user = root_path_for_user(socket.assigns.current_user)
- socket = push_redirect(socket, to: root_path_for_user)
- {:noreply, socket}
-
- {:error, changeset} ->
- socket = assign(socket, :changeset, changeset)
- {:noreply, socket}
- end
- end
-end
diff --git a/apps/web/lib/web/live/mfa_live/register_component.ex b/apps/web/lib/web/live/mfa_live/register_component.ex
deleted file mode 100644
index b1475c7ed..000000000
--- a/apps/web/lib/web/live/mfa_live/register_component.ex
+++ /dev/null
@@ -1,116 +0,0 @@
-defmodule Web.MFA.RegisterComponent do
- @moduledoc """
- MFA registration container
- """
- use Web, :live_component
- alias Domain.Auth.MFA
-
- @steps [
- {:pick_type, fields: ~w[type]a},
- {:register, fields: ~w[name]a},
- {:verify, fields: ~w[code]a},
- {:save, []}
- ]
-
- @impl Phoenix.LiveComponent
- def mount(socket) do
- secret = NimbleTOTP.secret()
-
- socket =
- socket
- |> assign(:secret, secret)
- |> assign(:params, %{"payload" => %{"secret" => Base.encode64(secret)}})
- |> assign(:remaining_steps, @steps)
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveComponent
- def update(assigns, socket) do
- changeset = MFA.create_method_changeset(socket.assigns.params, assigns.user.id)
-
- socket =
- socket
- |> assign(assigns)
- |> assign(:changeset, changeset)
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveComponent
- def render(%{remaining_steps: [{step, _opts} | _rest]} = assigns) do
- assigns = Map.put(assigns, :step, step)
-
- ~H"""
-
- <%= live_modal(
- Web.MFA.RegisterStepsComponent.render_step(%{
- secret: @secret,
- step: @step,
- changeset: @changeset,
- parent: @myself,
- user: @user
- }),
- return_to: @return_to,
- id: "register-mfa-modal",
- title: "Registering MFA Method",
- form: "mfa-method-form",
- button_text: if(@step == :save, do: "Save", else: "Next")
- ) %>
-
- """
- end
-
- @impl Phoenix.LiveComponent
- def handle_event(
- "next",
- params,
- %{assigns: %{remaining_steps: [{_step, step_opts} | rest_steps]}} = socket
- ) do
- params = Map.merge(socket.assigns.params, params)
- changeset = MFA.create_method_changeset(params, socket.assigns.user.id)
-
- step_fields = Keyword.fetch!(step_opts, :fields)
- error_fields = changeset.errors |> Keyword.keys()
-
- if Enum.any?(step_fields, &(&1 in error_fields)) do
- socket = assign(socket, :changeset, render_changeset_errors(changeset))
- {:noreply, socket}
- else
- socket =
- socket
- # XXX: The form helpers should not render errors if changeset.action is nil,
- # but we use custom form helpers and they do not respect this,
- # so we need to reset list of errors every time we move to the next step.
- # https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-a-note-on-errors
- |> assign(:changeset, %{changeset | errors: []})
- |> assign(:params, params)
- |> assign(:remaining_steps, rest_steps)
-
- {:noreply, socket}
- end
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("save", params, socket) do
- params = Map.merge(socket.assigns.params, params)
-
- case MFA.create_method(params, socket.assigns.user.id) do
- {:ok, _method} ->
- socket =
- socket
- |> put_flash(:info, "MFA method added!")
- |> push_redirect(to: socket.assigns.return_to)
-
- {:noreply, socket}
-
- {:error, changeset} ->
- socket =
- socket
- |> assign(:changeset, changeset)
- |> assign(:step, :save)
-
- {:noreply, socket}
- end
- end
-end
diff --git a/apps/web/lib/web/live/mfa_live/register_steps_component.ex b/apps/web/lib/web/live/mfa_live/register_steps_component.ex
deleted file mode 100644
index 0d950b19e..000000000
--- a/apps/web/lib/web/live/mfa_live/register_steps_component.ex
+++ /dev/null
@@ -1,146 +0,0 @@
-defmodule Web.MFA.RegisterStepsComponent do
- @moduledoc """
- MFA registration steps
- """
- use Phoenix.Component
- import Web.ErrorHelpers
-
- def render_step(assigns) do
- apply(__MODULE__, assigns.step, [assigns])
- end
-
- def pick_type(assigns) do
- ~H"""
-
- """
- end
-
- def register(assigns) do
- otpauth_uri =
- NimbleTOTP.otpauth_uri("Firezone:#{assigns.user.email}", assigns.secret, issuer: "Firezone")
-
- assigns =
- assigns
- |> Map.put(:uri, otpauth_uri)
- |> Map.put(:secret_base32_encoded, Base.encode32(assigns.secret))
-
- ~H"""
-
- """
- end
-
- def verify(assigns) do
- ~H"""
-
- """
- end
-
- def save(assigns) do
- ~H"""
-
- """
- end
-
- defp format_key(string) do
- string
- |> String.split("", trim: true)
- |> Enum.chunk_every(4)
- |> Enum.intersperse(" ")
- |> Enum.join("")
- end
-end
diff --git a/apps/web/lib/web/live/modal_component.ex b/apps/web/lib/web/live/modal_component.ex
deleted file mode 100644
index 5c26ca726..000000000
--- a/apps/web/lib/web/live/modal_component.ex
+++ /dev/null
@@ -1,51 +0,0 @@
-defmodule Web.ModalComponent do
- @moduledoc """
- Wraps a component in a modal.
- """
- use Web, :live_component
-
- @impl Phoenix.LiveComponent
- def render(assigns) do
- ~H"""
-
-
-
-
- <%= @opts[:title] %>
-
-
-
-
- <%= if is_atom(@component) do %>
- <.live_component module={@component} {@opts} />
- <% else %>
- <%= @component %>
- <% end %>
-
-
-
-
-
- """
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("close", _, socket) do
- {:noreply, push_patch(socket, to: socket.assigns.return_to)}
- end
-end
diff --git a/apps/web/lib/web/live/notifications_live/badge.ex b/apps/web/lib/web/live/notifications_live/badge.ex
deleted file mode 100644
index 4667e0e41..000000000
--- a/apps/web/lib/web/live/notifications_live/badge.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-defmodule Web.NotificationsLive.Badge do
- @moduledoc """
- Notifications badge that shows the status of current notifications.
- """
- use Web, :live_view_without_layout
-
- alias Domain.Notifications
- alias Phoenix.PubSub
-
- @topic "notifications_live"
-
- @impl Phoenix.LiveView
- def mount(_params, session, socket) do
- PubSub.subscribe(Domain.PubSub, @topic)
- pid = session["notifications_pid"]
-
- {:ok,
- socket
- |> assign(:notifications_pid, pid)
- |> assign(assigns(Notifications.current(pid)))}
- end
-
- @impl Phoenix.LiveView
- def handle_info({:notifications, notifications}, socket) do
- {:noreply,
- socket
- |> assign(assigns(notifications))}
- end
-
- defp assigns(notifications) do
- count = length(notifications)
- %{title: title(count), count: count}
- end
-
- defp title(0), do: "No Notifications"
- defp title(n), do: "#{n} Notifications"
-end
diff --git a/apps/web/lib/web/live/notifications_live/badge.html.heex b/apps/web/lib/web/live/notifications_live/badge.html.heex
deleted file mode 100644
index 7a8ff9d60..000000000
--- a/apps/web/lib/web/live/notifications_live/badge.html.heex
+++ /dev/null
@@ -1,12 +0,0 @@
-<%= live_redirect(
- to: ~p"/notifications",
- title: @title,
- class: "navbar-item has-divider is-desktop-icon-only",
- style: "height: 100%"
- ) do %>
- <%= if @count == 0 do %>
-
- <% else %>
- <%= @count %>
- <% end %>
-<% end %>
diff --git a/apps/web/lib/web/live/notifications_live/index.html.heex b/apps/web/lib/web/live/notifications_live/index.html.heex
deleted file mode 100644
index 68d00e2b8..000000000
--- a/apps/web/lib/web/live/notifications_live/index.html.heex
+++ /dev/null
@@ -1,53 +0,0 @@
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
-
- <%= if Enum.any?(@notifications) do %>
-
-
-
-
- Time
- User
- Notification
- Clear
-
-
-
- <%= for {notification, index} <- Enum.with_index(@notifications) do %>
-
-
- <%= icon(notification.type, assigns) %>
-
-
- ...
-
- <%= notification.user %>
- <%= notification.message %>
-
-
-
-
-
- <% end %>
-
-
- <% else %>
- No notifications to display.
- <% end %>
-
-
diff --git a/apps/web/lib/web/live/notifications_live/index_live.ex b/apps/web/lib/web/live/notifications_live/index_live.ex
deleted file mode 100644
index 8b4c2c6cc..000000000
--- a/apps/web/lib/web/live/notifications_live/index_live.ex
+++ /dev/null
@@ -1,49 +0,0 @@
-defmodule Web.NotificationsLive.Index do
- @moduledoc """
- Real time notifications live view.
- """
- use Web, :live_view
-
- alias Domain.Notifications
- alias Phoenix.PubSub
-
- require Logger
-
- @topic "notifications_live"
- @page_title "Notifications"
- @page_subtitle "Persisted notifications will appear below."
-
- @impl Phoenix.LiveView
- def mount(_params, session, socket) do
- PubSub.subscribe(Domain.PubSub, @topic)
- pid = session["notifications_pid"]
-
- {:ok,
- socket
- |> assign(:notifications_pid, pid)
- |> assign(:notifications, Notifications.current(pid))
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:page_title, @page_title)}
- end
-
- @impl Phoenix.LiveView
- def handle_info({:notifications, notifications}, socket) do
- {:noreply,
- socket
- |> assign(notifications: notifications)}
- end
-
- @impl Phoenix.LiveView
- def handle_event("clear_notification", %{"index" => index}, socket) do
- Notifications.clear_at(socket.assigns.notifications_pid, String.to_integer(index))
- {:noreply, socket}
- end
-
- defp icon(:error, assigns) do
- ~H"""
-
-
-
- """
- end
-end
diff --git a/apps/web/lib/web/live/oidc_live/connections_table_component.ex b/apps/web/lib/web/live/oidc_live/connections_table_component.ex
deleted file mode 100644
index 9301a24b7..000000000
--- a/apps/web/lib/web/live/oidc_live/connections_table_component.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule Web.OIDCLive.ConnectionsTableComponent do
- @moduledoc """
- OIDC Connections table
- """
- use Web, :live_component
- alias Domain.Auth.OIDC
-
- def handle_event("refresh", _payload, socket) do
- DynamicSupervisor.start_child(
- Domain.RefresherSupervisor,
- {Domain.Auth.OIDC.Refresher, {socket.assigns.user.id, 1000}}
- )
-
- {:noreply,
- socket
- |> put_flash(:info, "A refresh is underway, please check back in a minute.")
- |> push_redirect(to: ~p"/users/#{socket.assigns.user}")}
- end
-
- def handle_event("delete", %{"id" => id}, socket) do
- conn = OIDC.get_connection!(id)
- {:ok, _connection} = OIDC.delete_connection(conn)
-
- {:noreply,
- socket
- |> put_flash(:info, "The #{conn.provider} connection is deleted.")
- |> push_redirect(to: ~p"/users/#{socket.assigns.user}")}
- end
-
- defp delete_warning(conn) do
- "Deleting the connection will prevent their VPN session from being " <>
- "disabled for any OIDC errors from #{conn.provider} until the " <>
- "connection is re-established. Proceed?"
- end
-end
diff --git a/apps/web/lib/web/live/oidc_live/connections_table_component.html.heex b/apps/web/lib/web/live/oidc_live/connections_table_component.html.heex
deleted file mode 100644
index d2a380ea8..000000000
--- a/apps/web/lib/web/live/oidc_live/connections_table_component.html.heex
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
OIDC Connections
-
-
-
-
-
-
-
- Refresh Tokens
-
-
-
-
-
-
-
-
- Provider
- Refreshed At
- Refresh Result
-
-
-
-
- <%= for conn <- @connections do %>
-
-
- <%= conn.provider %>
-
-
- …
-
-
- <%= if match?(%{"error" => _}, conn.refresh_response) do %>
- ERROR: <%= conn.refresh_response["error"] %>
- <% else %>
- OK
- <% end %>
-
-
-
-
-
-
-
- Delete Connection
-
-
-
-
- <% end %>
-
-
-
diff --git a/apps/web/lib/web/live/rule_live/index.html.heex b/apps/web/lib/web/live/rule_live/index.html.heex
deleted file mode 100644
index be650a188..000000000
--- a/apps/web/lib/web/live/rule_live/index.html.heex
+++ /dev/null
@@ -1,38 +0,0 @@
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
-
-
-
-
-
- Rules apply to all devices by default. User-scoped rules apply only to
- devices belonging to that particular user.
-
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
- <%= live_component(
- Web.RuleLive.RuleListComponent,
- title: "Allowlist",
- header_icon: "mdi mdi-arrow-decision-outline",
- id: :allowlist,
- subject: @subject
- ) %>
-
-
- <%= live_component(
- Web.RuleLive.RuleListComponent,
- title: "Denylist",
- header_icon: "mdi mdi-alert-octagon",
- id: :denylist,
- subject: @subject
- ) %>
-
-
-
diff --git a/apps/web/lib/web/live/rule_live/index_live.ex b/apps/web/lib/web/live/rule_live/index_live.ex
deleted file mode 100644
index 9df7f6a49..000000000
--- a/apps/web/lib/web/live/rule_live/index_live.ex
+++ /dev/null
@@ -1,16 +0,0 @@
-defmodule Web.RuleLive.Index do
- @moduledoc """
- Handles Rule LiveViews.
- """
- use Web, :live_view
-
- @page_title "Egress Rules"
- @page_subtitle "Firewall rules to apply to the kernel's forward chain."
-
- def mount(_params, _session, socket) do
- {:ok,
- socket
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:page_title, @page_title)}
- end
-end
diff --git a/apps/web/lib/web/live/rule_live/rule_list_component.ex b/apps/web/lib/web/live/rule_live/rule_list_component.ex
deleted file mode 100644
index 408424e88..000000000
--- a/apps/web/lib/web/live/rule_live/rule_list_component.ex
+++ /dev/null
@@ -1,99 +0,0 @@
-defmodule Web.RuleLive.RuleListComponent do
- @moduledoc """
- Manages the Allowlist view.
- """
- use Web, :live_component
-
- alias Domain.Rules
- alias Domain.Users
-
- @impl Phoenix.LiveComponent
- def update(assigns, socket) do
- {:ok,
- socket
- |> assign(assigns)
- |> assign(
- action: action(assigns.id),
- rule_list: rule_list(assigns),
- users: users(assigns.subject),
- changeset: Rules.new_rule()
- )}
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("change", %{"rule" => attrs}, socket) do
- changeset = Rules.new_rule(attrs)
-
- socket =
- socket
- |> assign(:changeset, changeset)
-
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("add_rule", %{"rule" => attrs}, socket) do
- case Rules.create_rule(attrs, socket.assigns.subject) do
- {:ok, _rule} ->
- socket =
- socket
- |> assign(changeset: Rules.new_rule(), rule_list: rule_list(socket.assigns))
-
- {:noreply, socket}
-
- {:error, changeset} ->
- {:noreply, assign(socket, changeset: changeset)}
- end
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("delete_rule", %{"rule_id" => rule_id}, socket) do
- with {:ok, rule} <- Rules.fetch_rule_by_id(rule_id, socket.assigns.subject),
- {:ok, _rule} <- Rules.delete_rule(rule, socket.assigns.subject) do
- {:noreply, assign(socket, rule_list: rule_list(socket.assigns))}
- else
- {:error, msg} ->
- {:noreply, put_flash(socket, :error, "Couldn't delete rule. #{msg}")}
- end
- end
-
- def action(id) do
- case id do
- :allowlist ->
- :accept
-
- :denylist ->
- :drop
- end
- end
-
- defp rule_list(assigns) do
- case assigns.id do
- :allowlist ->
- Rules.allowlist()
-
- :denylist ->
- Rules.denylist()
- end
- end
-
- defp users(subject) do
- {:ok, users} = Users.list_users(subject)
-
- users
- |> Stream.map(&{&1.id, &1.email})
- |> Map.new()
- end
-
- defp user_options(users) do
- Enum.map(users, fn {id, email} -> {email, id} end)
- end
-
- defp port_type_options do
- %{TCP: :tcp, UDP: :udp}
- end
-
- defp port_type_display(nil), do: nil
- defp port_type_display(:tcp), do: "TCP"
- defp port_type_display(:udp), do: "UDP"
-end
diff --git a/apps/web/lib/web/live/rule_live/rule_list_component.html.heex b/apps/web/lib/web/live/rule_live/rule_list_component.html.heex
deleted file mode 100644
index ff3c8ecaf..000000000
--- a/apps/web/lib/web/live/rule_live/rule_list_component.html.heex
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
- <.form
- :let={f}
- for={@changeset}
- id={"#{@action}-form"}
- phx-change="change"
- phx-target={@myself}
- phx-submit="add_rule"
- >
- <%= hidden_input(f, :action, value: @action) %>
-
-
- <%= label(f, :destination, class: "label") %>
-
- <%= text_input(
- f,
- :destination,
- class: "input #{input_error_class(f, :destination)}",
- placeholder: "IPv4/6 CIDR range or address"
- ) %>
-
-
- <%= error_tag(f, :destination) %>
-
-
-
-
- <%= label(f, "User", class: "label") %>
-
- <%= select(
- f,
- :user_id,
- user_options(@users),
- prompt: "All users"
- ) %>
-
-
-
-
- <%= label(f, :port_type, class: "label") %>
-
- <%= select(
- f,
- :port_type,
- port_type_options(),
- prompt: "All protocols"
- ) %>
-
-
- <%= error_tag(f, :port_type) %>
-
-
-
-
- <%= label(f, :port_range, class: "label") %>
-
- <%= text_input(f, :port_range,
- class: "input #{input_error_class(f, :port_range)}",
- placeholder: "23000-24000",
- disabled: Domain.Validator.empty?(@changeset, :port_type)
- ) %>
-
-
- <%= error_tag(f, :port_range) %>
-
-
-
-
-
- <%= submit("Add", class: "button is-primary") %>
-
-
-
-
-
- <%= if length(@rule_list) > 0 do %>
-
-
- Destination
- User Scope
- Port Type
- Port Range
-
-
-
-
- <%= for rule <- @rule_list do %>
-
-
-
- <%= rule.destination %>
-
-
-
- <%= @users[rule.user_id] %>
-
-
- <%= port_type_display(rule.port_type) %>
-
-
- <%= rule.port_range %>
-
-
-
- Delete
-
-
-
- <% end %>
-
- <% end %>
-
-
-
diff --git a/apps/web/lib/web/live/setting_live/account.html.heex b/apps/web/lib/web/live/setting_live/account.html.heex
deleted file mode 100644
index eda8f2b31..000000000
--- a/apps/web/lib/web/live/setting_live/account.html.heex
+++ /dev/null
@@ -1,270 +0,0 @@
-<%= if @live_action == :edit do %>
- <%= live_modal(
- Web.SettingLive.AccountFormComponent,
- return_to: ~p"/settings/account",
- title: "Edit Account",
- id: "user-#{@current_user.id}",
- user: @current_user,
- subject: @subject,
- action: @live_action,
- form: "account-edit"
- ) %>
-<% end %>
-
-<%= if @live_action == :new_api_token do %>
- <%= live_modal(
- Web.SettingLive.NewApiTokenComponent,
- return_to: ~p"/settings/account",
- title: "Add API Token",
- id: "new_api_token",
- form: "api-token-form",
- user: @current_user,
- subject: @subject,
- changeset: Domain.ApiTokens.new_api_token()
- ) %>
-<% end %>
-
-<%= if @live_action == :show_api_token do %>
- <%= live_modal(
- Web.SettingLive.ShowApiTokenComponent,
- return_to: ~p"/settings/account",
- title: "API Token #{@api_token_id}",
- id: "show_api_token",
- hide_footer_content: true,
- user: @current_user,
- subject: @subject,
- api_token: @api_token
- ) %>
-<% end %>
-
-<%= if @live_action == :register_mfa do %>
- <.live_component
- module={Web.MFA.RegisterComponent}
- id="register-mfa"
- user={@current_user}
- return_to={~p"/settings/account"}
- subject={@subject}
- />
-<% end %>
-
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
-
-
Details
-
-
-
- <.link replace={true} patch={~p"/settings/account/edit"} class="button">
-
-
-
- Change Email or Password
-
-
-
-
- <%= render(Web.SharedView, "user_details.html",
- user: @current_user,
- rules_path: @rules_path,
- subject: @subject
- ) %>
-
-
-
-
- Active Sessions
-
-
-
-
- Your active Firezone web sessions. Each row corresponds to an open browser
- tab connected to Firezone.
-
-
-
-
-
-
-
- Came Online
- Last Signed In
- Remote IP
- User Agent
-
-
-
- <%= for {meta, index} <- Enum.with_index(@metas) do %>
-
-
- …
-
-
- …
-
- <%= meta.remote_ip || "-" %>
- <%= meta.user_agent %>
-
- <% end %>
-
-
-
-
-
-
-
- API Tokens
-
-
-
-
-
- <%= if Enum.any?(@api_tokens) do %>
-
-
-
- Created at
- Identifier
- Status
- Actions
-
-
-
- <%= for api_token <- @api_tokens do %>
-
-
- …
-
-
- <.link patch={~p"/settings/account/api_token/#{api_token}"}>
- <%= api_token.id %>
-
-
-
- <%= if ApiTokens.api_token_expired?(api_token) do %>
-
-
-
- Expired at
- <% else %>
-
-
-
- Expires at
- <% end %>
-
- …
-
-
-
- <.link
- data-confirm="Are you sure?"
- phx-click="delete_api_token"
- phx-value-id={api_token.id}
- >
- Delete
-
-
-
- <% end %>
-
-
- <% else %>
-
- No API tokens.
-
- <% end %>
-
-
- <%= if length(@api_tokens) < Domain.ApiTokens.ApiToken.Changeset.max_per_user() do %>
- <.link patch={~p"/settings/account/api_token"} class="button">
-
-
-
- Add API Token
-
- <% end %>
-
-
-
-
- Multi Factor Authentication
-
-
-
-
- Your MFA methods are invoked when login with username and password.
-
-
-
-
- <%= if length(@methods) > 0 do %>
- <%= render(Web.SharedView, "mfa_methods_table.html", methods: @methods) %>
- <% else %>
-
No MFA methods added.
- <% end %>
-
-
- <.link replace={true} patch={~p"/settings/account/register_mfa"} class="button">
-
-
-
- Add MFA Method
-
-
-
-
-
- Product and Security Updates
-
-
-
-
- <%= link("Click here", to: @subscribe_link, target: "_blank") %> to register for product and security updates.
-
-
-
-
-
-
- Danger Zone
-
-
- <%= form_for @changeset, ~p"/sign_out", [id: "delete-account", method: :delete], fn _f -> %>
- <%= submit(class: "button is-danger", data: if(@allow_delete, do: [confirm: "Are you sure?"], else: []), disabled: !@allow_delete) do %>
-
-
-
- Delete Your Account
- <% end %>
- <% end %>
-
diff --git a/apps/web/lib/web/live/setting_live/account_form_component.ex b/apps/web/lib/web/live/setting_live/account_form_component.ex
deleted file mode 100644
index 2331f55d5..000000000
--- a/apps/web/lib/web/live/setting_live/account_form_component.ex
+++ /dev/null
@@ -1,32 +0,0 @@
-defmodule Web.SettingLive.AccountFormComponent do
- @moduledoc """
- Handles the edit account form for admins.
- """
- use Web, :live_component
-
- alias Domain.Users
-
- def update(assigns, socket) do
- changeset = Users.change_user(assigns.user)
-
- {:ok,
- socket
- |> assign(assigns)
- |> assign(:changeset, changeset)}
- end
-
- def handle_event("save", %{"user" => user_params}, socket) do
- user = socket.assigns.user
-
- case Users.update_user(user, user_params) do
- {:ok, _user} ->
- {:noreply,
- socket
- |> put_flash(:info, "Account updated successfully.")
- |> redirect(to: socket.assigns.return_to)}
-
- {:error, changeset} ->
- {:noreply, assign(socket, :changeset, changeset)}
- end
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/account_form_component.html.heex b/apps/web/lib/web/live/setting_live/account_form_component.html.heex
deleted file mode 100644
index 421c64048..000000000
--- a/apps/web/lib/web/live/setting_live/account_form_component.html.heex
+++ /dev/null
@@ -1,43 +0,0 @@
-
- <.form
- :let={f}
- for={@changeset}
- id="account-edit"
- phx-target={@myself}
- phx-submit="save"
- x-autocomplete="off"
- >
-
-
Change email or enter new password below.
-
-
-
- <%= label(f, :email, class: "label") %>
-
-
- <%= text_input(f, :email, class: "input #{input_error_class(f, :email)}") %>
-
-
- <%= error_tag(f, :email) %>
-
-
-
- <%= render(
- Web.SharedView,
- "password_field.html",
- context: f,
- field: :password,
- autocomplete: "new-password",
- label: "Password"
- ) %>
-
- <%= render(
- Web.SharedView,
- "password_field.html",
- context: f,
- field: :password_confirmation,
- autocomplete: "new-password",
- label: "Password Confirmation"
- ) %>
-
-
diff --git a/apps/web/lib/web/live/setting_live/account_live.ex b/apps/web/lib/web/live/setting_live/account_live.ex
deleted file mode 100644
index f2a8f977c..000000000
--- a/apps/web/lib/web/live/setting_live/account_live.ex
+++ /dev/null
@@ -1,120 +0,0 @@
-defmodule Web.SettingLive.Account do
- @moduledoc """
- Handles Account-related things for admins.
- """
- use Web, :live_view
-
- alias Domain.{
- ApiTokens,
- Auth.MFA,
- Users
- }
-
- alias Web.{
- Endpoint,
- Presence
- }
-
- @live_sessions_topic "notification:session"
- @page_title "Account Settings"
- @page_subtitle "Configure settings related to your Firezone web portal account."
-
- @impl Phoenix.LiveView
- def mount(params, _session, socket) do
- Endpoint.subscribe(@live_sessions_topic)
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
-
- {:ok, api_tokens} =
- ApiTokens.list_api_tokens_by_user_id(socket.assigns.current_user.id, socket.assigns.subject)
-
- socket =
- socket
- |> assign(:api_token_id, params["api_token_id"])
- |> assign(:subscribe_link, subscribe_link())
- |> assign(:allow_delete, Users.count_by_role(:admin) > 1)
- |> assign(:api_tokens, api_tokens)
- |> assign(:changeset, Users.change_user(socket.assigns.current_user))
- |> assign(:methods, methods)
- |> assign(:page_title, @page_title)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:rules_path, ~p"/rules")
- |> assign(
- :metas,
- get_metas(Presence.list(@live_sessions_topic), socket.assigns.current_user.id)
- )
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_params(%{"api_token_id" => api_token_id}, _url, socket) do
- {:ok, api_token} = ApiTokens.fetch_unexpired_api_token_by_id(api_token_id)
- {:noreply, assign(socket, :api_token, api_token)}
- end
-
- @impl Phoenix.LiveView
- def handle_params(_params, _url, socket) do
- {:ok, api_tokens} =
- ApiTokens.list_api_tokens_by_user_id(socket.assigns.current_user.id, socket.assigns.subject)
-
- socket =
- socket
- |> assign(:allow_delete, Users.count_by_role(:admin) > 1)
- |> assign(:api_tokens, api_tokens)
-
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete_api_token", %{"id" => id}, socket) do
- case ApiTokens.delete_api_token_by_id(id, socket.assigns.subject) do
- {:ok, _api_token} ->
- {:ok, api_tokens} =
- ApiTokens.list_api_tokens_by_user_id(
- socket.assigns.current_user.id,
- socket.assigns.subject
- )
-
- {:noreply, assign(socket, :api_tokens, api_tokens)}
-
- {:error, :not_found} ->
- {:noreply, socket}
- end
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete_authenticator", %{"id" => id}, socket) do
- with {:ok, _method} <- MFA.delete_method_by_id(id, socket.assigns.current_user) do
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
- {:noreply, assign(socket, :methods, methods)}
- else
- {:error, :not_found} ->
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
- {:noreply, assign(socket, :methods, methods)}
-
- false ->
- {:noreply, socket}
- end
- end
-
- @impl Phoenix.LiveView
- def handle_info(
- %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}},
- %{assigns: %{metas: metas}} = socket
- ) do
- metas =
- (metas ++
- get_metas(joins, socket.assigns.current_user.id)) --
- get_metas(leaves, socket.assigns.current_user.id)
-
- {:noreply, assign(socket, :metas, metas)}
- end
-
- defp get_metas(presences, user_id) do
- get_in(presences, [user_id, :metas]) || []
- end
-
- defp subscribe_link do
- "https://www.firezone.dev/sales?utm_source=product&uid=#{Domain.Telemetry.id()}"
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/client_defaults.html.heex b/apps/web/lib/web/live/setting_live/client_defaults.html.heex
deleted file mode 100644
index 85e1b1bb3..000000000
--- a/apps/web/lib/web/live/setting_live/client_defaults.html.heex
+++ /dev/null
@@ -1,19 +0,0 @@
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
- Client Defaults
-
-
- <%= live_component(
- Web.SettingLive.ClientDefaultsFormComponent,
- subject: @subject,
- changeset: @changeset,
- id: :client_defaults_form_component
- ) %>
-
-
diff --git a/apps/web/lib/web/live/setting_live/client_defaults_form_component.ex b/apps/web/lib/web/live/setting_live/client_defaults_form_component.ex
deleted file mode 100644
index 20afe3127..000000000
--- a/apps/web/lib/web/live/setting_live/client_defaults_form_component.ex
+++ /dev/null
@@ -1,70 +0,0 @@
-defmodule Web.SettingLive.ClientDefaultsFormComponent do
- @moduledoc """
- Handles updating client defaults form.
- """
- use Web, :live_component
- alias Domain.Config
-
- @configs ~w[
- default_client_allowed_ips
- default_client_dns
- default_client_endpoint
- default_client_persistent_keepalive
- default_client_mtu
- ]a
-
- @impl Phoenix.LiveComponent
- def update(assigns, socket) do
- socket =
- socket
- |> assign(assigns)
- |> assign(:configs, Domain.Config.fetch_source_and_configs!(@configs))
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("save", %{"configuration" => configuration_params}, socket) do
- configuration_params =
- configuration_params
- |> Map.update("default_client_dns", nil, &binary_to_list/1)
- |> Map.update("default_client_allowed_ips", nil, &binary_to_list/1)
-
- configuration = Config.fetch_db_config!()
-
- socket =
- case Config.update_config(configuration, configuration_params) do
- {:ok, configuration} ->
- socket
- |> assign(:changeset, Config.change_config(configuration))
-
- {:error, changeset} ->
- socket
- |> assign(:changeset, changeset)
- end
-
- {:noreply, socket}
- end
-
- defp binary_to_list(binary) when is_binary(binary),
- do: binary |> String.trim() |> String.split(",")
-
- defp binary_to_list(list) when is_list(list),
- do: list
-
- def config_has_override?({{source, _source_key}, _key}) do
- source not in [:db]
- end
-
- def config_has_override?({_source, _key}) do
- false
- end
-
- def config_value({_source, value}) do
- value
- end
-
- def config_override_source({{:env, source_key}, _value}) do
- "environment variable #{source_key}"
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/client_defaults_form_component.html.heex b/apps/web/lib/web/live/setting_live/client_defaults_form_component.html.heex
deleted file mode 100644
index af72ec717..000000000
--- a/apps/web/lib/web/live/setting_live/client_defaults_form_component.html.heex
+++ /dev/null
@@ -1,152 +0,0 @@
-
- <.form :let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
-
- <% default_client_allowed_ips = Map.fetch!(@configs, :default_client_allowed_ips) %>
- <%= label(f, :default_client_allowed_ips, "Allowed IPs", class: "label") %>
-
-
- <%= textarea(
- f,
- :default_client_allowed_ips,
- placeholder: "0.0.0.0/0, ::/0",
- class: "textarea #{input_error_class(f, :default_client_allowed_ips)}",
- disabled: config_has_override?(default_client_allowed_ips),
- value:
- if config_has_override?(default_client_allowed_ips) do
- "Set in #{config_override_source(default_client_allowed_ips)}: #{Enum.join(config_value(default_client_allowed_ips), ", ")}"
- else
- list_value(f, :default_client_allowed_ips)
- end
- ) %>
-
-
-
- <%= error_tag(f, :default_client_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.
-
-
-
-
- <% default_client_dns = Map.fetch!(@configs, :default_client_dns) %>
- <%= label(f, :default_client_dns, "DNS Servers", class: "label") %>
-
-
- <%= text_input(
- f,
- :default_client_dns,
- placeholder: "1.1.1.1, 1.0.0.1",
- class: "input #{input_error_class(f, :default_client_dns)}",
- disabled: config_has_override?(default_client_dns),
- value:
- if config_has_override?(default_client_dns) do
- "Set in #{config_override_source(default_client_dns)}: #{Enum.join(config_value(default_client_dns), ", ")}"
- else
- list_value(f, :default_client_dns)
- end
- ) %>
-
-
-
- <%= error_tag(f, :default_client_dns) %>
-
-
- Comma-separated list of DNS servers to use for devices.
- Leave this blank to omit the DNS section from
- generated configs.
-
-
-
-
- <% default_client_endpoint = Map.fetch!(@configs, :default_client_endpoint) %>
- <%= label(f, :default_client_endpoint, "Endpoint", class: "label") %>
-
-
- <%= text_input(
- f,
- :default_client_endpoint,
- placeholder: "firezone.example.com",
- class: "input #{input_error_class(f, :default_client_endpoint)}",
- disabled: config_has_override?(default_client_endpoint),
- value:
- if config_has_override?(default_client_endpoint) do
- "Set in #{config_override_source(default_client_endpoint)}: #{config_value(default_client_endpoint)}"
- else
- input_value(f, :default_client_endpoint)
- end
- ) %>
-
-
- <%= error_tag(f, :default_client_endpoint) %>
-
-
- IPv4, IPv6 address, or FQDN that devices will be configured to connect
- to. Defaults to this server's FQDN.
-
-
-
-
- <% default_client_persistent_keepalive =
- Map.fetch!(@configs, :default_client_persistent_keepalive) %>
- <%= label(f, :default_client_persistent_keepalive, "Persistent Keepalive", class: "label") %>
-
-
- <%= text_input(
- f,
- :default_client_persistent_keepalive,
- placeholder: "25",
- class: "input #{input_error_class(f, :default_client_persistent_keepalive)}",
- disabled: config_has_override?(default_client_persistent_keepalive),
- value:
- if config_has_override?(default_client_persistent_keepalive) do
- "Set in #{config_override_source(default_client_persistent_keepalive)}: #{config_value(default_client_persistent_keepalive)}"
- else
- input_value(f, :default_client_persistent_keepalive)
- end
- ) %>
-
- <%= error_tag(f, :default_client_persistent_keepalive) %>
-
-
- Interval in seconds to send persistent keepalive packets from devices. Most users won't
- need to change this. Leave this blank to omit this field from generated configs.
-
-
-
-
-
- <% default_client_mtu = Map.fetch!(@configs, :default_client_mtu) %>
- <%= label(f, :default_client_mtu, "MTU", class: "label") %>
-
-
- <%= text_input(
- f,
- :default_client_mtu,
- placeholder: "1280",
- class: "input #{input_error_class(f, :default_client_mtu)}",
- disabled: config_has_override?(default_client_mtu),
- value:
- if config_has_override?(default_client_mtu) do
- "Set in #{config_override_source(default_client_mtu)}: #{config_value(default_client_mtu)}"
- else
- input_value(f, :default_client_mtu)
- end
- ) %>
-
-
- <%= error_tag(f, :default_client_mtu) %>
-
-
- WireGuard interface MTU for devices. 1280 is a safe bet for most networks.
- Leave this blank to omit this field from generated configs.
-
-
-
- <%= Phoenix.View.render(Web.SharedView, "submit_button.html", []) %>
-
-
diff --git a/apps/web/lib/web/live/setting_live/client_defaults_live.ex b/apps/web/lib/web/live/setting_live/client_defaults_live.ex
deleted file mode 100644
index fa21ea6a6..000000000
--- a/apps/web/lib/web/live/setting_live/client_defaults_live.ex
+++ /dev/null
@@ -1,21 +0,0 @@
-defmodule Web.SettingLive.ClientDefaults do
- @moduledoc """
- Manages the defaults view.
- """
- use Web, :live_view
- alias Domain.Config
-
- @page_title "Client Defaults"
- @page_subtitle "Configure default values for generating WireGuard client configurations."
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- socket =
- socket
- |> assign(:changeset, Config.change_config())
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:page_title, @page_title)
-
- {:ok, socket}
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/customization.html.heex b/apps/web/lib/web/live/setting_live/customization.html.heex
deleted file mode 100644
index 7db316bbf..000000000
--- a/apps/web/lib/web/live/setting_live/customization.html.heex
+++ /dev/null
@@ -1,94 +0,0 @@
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
- Logo
-
-
-
- Use a logo at least 300px wide with a 7:2 ratio for best results. GIF, JPEG, PNG, SVG, TIFF,
- WebP and AVIF images are supported.
-
- <%= if has_override?(@logo_source) do %>
-
- The logo was overridden by the LOGO
- environment variable; you cannot change it.
-
- <% end %>
-
-
-
- <%= unless has_override?(@logo_source) do %>
-
-
- <%= for type <- Domain.Config.Logo.__types__() do %>
-
-
- <%= type %>
-
- <% end %>
-
-
- <% end %>
-
-
- <%= if @logo_type == "Default" do %>
- <%= Web.LogoComponent.render(nil) %>
- <% else %>
- <%= Web.LogoComponent.render(@logo) %>
- <% end %>
-
-
- <%= unless has_override?(@logo_source) do %>
- <%= if @logo_type == "Default" do %>
-
- <% end %>
-
- <%= if @logo_type == "URL" do %>
-
- <% end %>
-
- <%= if @logo_type == "Upload" do %>
-
- <% end %>
- <% end %>
-
-
diff --git a/apps/web/lib/web/live/setting_live/customization_live.ex b/apps/web/lib/web/live/setting_live/customization_live.ex
deleted file mode 100644
index ba435f55a..000000000
--- a/apps/web/lib/web/live/setting_live/customization_live.ex
+++ /dev/null
@@ -1,90 +0,0 @@
-defmodule Web.SettingLive.Customization do
- @moduledoc """
- Manages the app customizations.
- """
- use Web, :live_view
- alias Domain.Config
-
- @max_logo_size 1024 ** 2
- @page_title "Customization"
- @page_subtitle "Customize the look and feel of your Firezone web portal."
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- {source, logo} = Domain.Config.fetch_source_and_config!(:logo)
- logo_type = Domain.Config.Logo.type(logo)
-
- socket =
- socket
- |> assign(:page_title, @page_title)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:logo, logo)
- |> assign(:logo_source, source)
- |> assign(:logo_type, logo_type)
- |> allow_upload(:logo,
- accept: ~w(.jpg .jpeg .png .gif .webp .avif .svg .tiff),
- max_file_size: @max_logo_size
- )
-
- {:ok, socket}
- end
-
- def has_override?({source, _source_key}), do: source not in [:db]
- def has_override?(_source), do: false
-
- @impl Phoenix.LiveView
- def handle_event("choose", %{"type" => type}, socket) do
- {:noreply, assign(socket, :logo_type, type)}
- end
-
- @impl Phoenix.LiveView
- def handle_event("validate", _params, socket) do
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_event("save", %{"default" => "true"}, socket) do
- {:ok, config} =
- Config.fetch_db_config!()
- |> Config.update_config(%{logo: nil}, socket.assigns.subject)
-
- {:noreply, assign(socket, :logo, config.logo)}
- end
-
- @impl Phoenix.LiveView
- def handle_event("save", %{"url" => url}, socket) do
- {:ok, config} =
- Config.fetch_db_config!()
- |> Config.update_config(%{logo: %{"url" => url}}, socket.assigns.subject)
-
- {:noreply, assign(socket, :logo, config.logo)}
- end
-
- @impl Phoenix.LiveView
- def handle_event("save", _params, socket) do
- {[entry], []} = uploaded_entries(socket, :logo)
-
- config =
- consume_uploaded_entry(socket, entry, fn %{path: path} ->
- data =
- path
- |> File.read!()
- |> Base.encode64()
-
- {:ok, config} =
- Config.fetch_db_config!()
- |> Config.update_config(
- %{logo: %{"data" => data, "type" => entry.client_type}},
- socket.assigns.subject
- )
-
- {:ok, config}
- end)
-
- {:noreply, assign(socket, :logo, config.logo)}
- end
-
- defp error_to_string(:too_large), do: "The file exceeds the maximum size of 1MB."
- defp error_to_string(:too_many_files), do: "You have selected too many files."
- defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type."
-end
diff --git a/apps/web/lib/web/live/setting_live/new_api_token_component.ex b/apps/web/lib/web/live/setting_live/new_api_token_component.ex
deleted file mode 100644
index 395467c9c..000000000
--- a/apps/web/lib/web/live/setting_live/new_api_token_component.ex
+++ /dev/null
@@ -1,69 +0,0 @@
-defmodule Web.SettingLive.NewApiTokenComponent do
- @moduledoc """
- Live component to manage creating API Tokens
- """
- use Web, :live_component
-
- alias Domain.ApiTokens
-
- def render(assigns) do
- ~H"""
-
- <.form
- :let={f}
- for={@changeset}
- autocomplete="off"
- id="api-token-form"
- phx-target={@myself}
- phx-submit="save"
- >
- <%= if @changeset.action do %>
-
-
- <%= error_tag(f, :base) %>
-
-
- <% end %>
-
-
- <%= label(f, :expires_in, class: "label") %>
-
-
-
-
-
- <%= text_input(f, :expires_in, class: "input #{input_error_class(f, :expires_in)}") %>
-
-
-
- days
-
-
-
-
-
- <%= error_tag(f, :expires_in) %>
-
-
-
-
-
- """
- end
-
- def handle_event("save", %{"api_token" => attrs}, socket) do
- subject = socket.assigns.subject
-
- case ApiTokens.create_api_token(attrs, subject) do
- {:ok, api_token} ->
- {:noreply,
- socket
- |> push_patch(to: ~p"/settings/account/api_token/#{api_token}")}
-
- {:error, changeset} ->
- {:noreply,
- socket
- |> assign(:changeset, changeset)}
- end
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/oidc_form_component.ex b/apps/web/lib/web/live/setting_live/oidc_form_component.ex
deleted file mode 100644
index f4ac6ac1c..000000000
--- a/apps/web/lib/web/live/setting_live/oidc_form_component.ex
+++ /dev/null
@@ -1,228 +0,0 @@
-defmodule Web.SettingLive.OIDCFormComponent do
- @moduledoc """
- Form for OIDC configs
- """
- use Web, :live_component
- alias Domain.Config
-
- def render(assigns) do
- ~H"""
-
- <.form
- :let={f}
- for={@changeset}
- autocomplete="off"
- id="oidc-form"
- phx-target={@myself}
- phx-submit="save"
- >
-
- <%= label(f, :id, "Config ID", class: "label") %>
-
-
- <%= text_input(f, :id, class: "input #{input_error_class(f, :id)}") %>
-
-
- <%= error_tag(f, :id) %>
-
-
- A unique ID that will be used to generate login URLs for this provider.
-
-
-
-
-
-
- <%= label(f, :label, class: "label") %>
-
-
- <%= text_input(f, :label, class: "input #{input_error_class(f, :label)}") %>
-
-
- <%= error_tag(f, :label) %>
-
-
- Text to display on the Login button.
-
-
-
-
-
-
- <%= label(f, :scope, class: "label") %>
-
-
- <%= text_input(f, :scope,
- placeholder: "openid email profile",
- class: "input #{input_error_class(f, :scope)}"
- ) %>
-
-
- <%= error_tag(f, :scope) %>
-
-
- Space-delimited list of OpenID scopes. openid
- and email
- are required in order for Firezone to work.
-
-
-
-
-
-
- <%= label(f, :response_type, class: "label") %>
-
-
- <%= text_input(f, :response_type,
- disabled: true,
- placeholder: "code",
- class: "input #{input_error_class(f, :response_type)}"
- ) %>
-
-
- <%= error_tag(f, :response_type) %>
-
-
-
-
-
-
- <%= label(f, :client_id, "Client ID", class: "label") %>
-
-
- <%= text_input(f, :client_id, class: "input #{input_error_class(f, :client_id)}") %>
-
-
- <%= error_tag(f, :client_id) %>
-
-
-
-
-
-
- <%= label(f, :client_secret, class: "label") %>
-
-
- <%= text_input(f, :client_secret, class: "input #{input_error_class(f, :client_secret)}") %>
-
-
- <%= error_tag(f, :client_secret) %>
-
-
-
-
-
-
- <%= label(f, :discovery_document_uri, "Discovery Document URI", class: "label") %>
-
-
- <%= text_input(f, :discovery_document_uri,
- placeholder: "https://accounts.google.com/.well-known/openid-configuration",
- class: "input #{input_error_class(f, :discovery_document_uri)}"
- ) %>
-
-
- <%= error_tag(f, :discovery_document_uri) %>
-
-
-
-
-
-
- <%= label(f, :redirect_uri, "Redirect URI", class: "label") %>
-
-
- <%= text_input(f, :redirect_uri,
- placeholder:
- "#{@external_url}auth/oidc/#{input_value(f, :id) || "{CONFIG_ID}"}/callback/",
- class: "input #{input_error_class(f, :redirect_uri)}"
- ) %>
-
-
- <%= error_tag(f, :redirect_uri) %>
-
-
- Optionally override the Redirect URI. Must match the redirect URI set in your IdP.
- In most cases you shouldn't change this. By default
-
- <%= "#{@external_url}auth/oidc/#{input_value(f, :id) || "{CONFIG_ID}"}/callback/" %>
-
- is used.
-
-
-
-
-
-
-
Auto-create users
-
-
-
-
- Automatically provision users when signing in for the first time.
-
-
- <%= error_tag(f, :auto_create_users) %>
-
-
-
- <%= label f, :auto_create_users, class: "switch is-medium" do %>
- <%= checkbox(f, :auto_create_users) %>
-
- <% end %>
-
-
-
-
-
- """
- end
-
- def update(assigns, socket) do
- changeset =
- assigns.provider
- |> Map.delete(:__struct__)
- |> Domain.Config.Configuration.OpenIDConnectProvider.create_changeset()
-
- socket =
- socket
- |> assign(assigns)
- |> assign(:external_url, Domain.Config.fetch_env!(:web, :external_url))
- |> assign(:changeset, changeset)
-
- {:ok, socket}
- end
-
- def handle_event("save", %{"open_id_connect_provider" => params}, socket) do
- changeset = Domain.Config.Configuration.OpenIDConnectProvider.create_changeset(params)
-
- if changeset.valid? do
- attrs = Ecto.Changeset.apply_changes(changeset)
-
- config = Config.fetch_db_config!()
-
- openid_connect_providers =
- config.openid_connect_providers
- |> Enum.reject(&(&1.id == socket.assigns.provider.id))
- |> Kernel.++([attrs])
- |> Enum.map(&Map.from_struct/1)
-
- {:ok, _config} =
- Config.update_config(
- config,
- %{openid_connect_providers: openid_connect_providers},
- socket.assigns.subject
- )
-
- socket =
- socket
- |> put_flash(:info, "Updated successfully.")
- |> redirect(to: socket.assigns.return_to)
-
- {:noreply, socket}
- else
- socket = assign(socket, :changeset, render_changeset_errors(changeset))
- {:noreply, socket}
- end
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/saml_form_component.ex b/apps/web/lib/web/live/setting_live/saml_form_component.ex
deleted file mode 100644
index 51432af42..000000000
--- a/apps/web/lib/web/live/setting_live/saml_form_component.ex
+++ /dev/null
@@ -1,244 +0,0 @@
-defmodule Web.SettingLive.SAMLFormComponent do
- @moduledoc """
- Form for SAML configs
- """
- use Web, :live_component
- alias Domain.Config
-
- def render(assigns) do
- ~H"""
-
- <.form
- :let={f}
- for={@changeset}
- autocomplete="off"
- id="saml-form"
- phx-target={@myself}
- phx-submit="save"
- >
-
- <%= label(f, :id, "Config ID", class: "label") %>
-
-
- <%= text_input(f, :id, class: "input #{input_error_class(f, :id)}") %>
-
-
- <%= error_tag(f, :id) %>
-
-
- ID used for generating auth URLs.
-
-
-
-
-
-
- <%= label(f, :label, class: "label") %>
-
-
- <%= text_input(f, :label, class: "input #{input_error_class(f, :label)}") %>
-
-
- <%= error_tag(f, :label) %>
-
-
- Sign in button text.
-
-
-
-
-
-
- <%= label(f, :base_url, "Base URL", class: "label") %>
-
-
- <%= text_input(f, :base_url, class: "input #{input_error_class(f, :base_url)}") %>
-
-
- <%= error_tag(f, :base_url) %>
-
-
- Base URL for the ACS URL. in most cases this shouldn't be changed.
-
-
-
-
-
-
- <%= label(f, :metadata, class: "label") %>
-
-
- <%= textarea(f, :metadata,
- rows: 8,
- class: "textarea #{input_error_class(f, :metadata)}"
- ) %>
-
-
- <%= error_tag(f, :metadata) %>
-
-
- IdP metadata XML.
-
-
-
-
-
-
-
Sign requests
-
-
-
-
Sign SAML requests with your SAML private key.
-
- <%= error_tag(f, :sign_requests) %>
-
-
-
- <%= label f, :sign_requests, class: "switch is-medium" do %>
- <%= checkbox(f, :sign_requests) %>
-
- <% end %>
-
-
-
-
-
-
-
-
Sign metadata
-
-
-
-
Sign SAML metadata with your SAML private key.
-
- <%= error_tag(f, :sign_metadata) %>
-
-
-
- <%= label f, :sign_metadata, class: "switch is-medium" do %>
- <%= checkbox(f, :sign_metadata) %>
-
- <% end %>
-
-
-
-
-
-
-
-
Require signed assertions
-
-
-
-
Require assertions from your IdP to be signed.
-
- <%= error_tag(f, :signed_assertion_in_resp) %>
-
-
-
- <%= label f, :signed_assertion_in_resp, class: "switch is-medium" do %>
- <%= checkbox(f, :signed_assertion_in_resp) %>
-
- <% end %>
-
-
-
-
-
-
-
-
Require signed envelopes
-
-
-
-
Require envelopes from your IdP to be signed.
-
- <%= error_tag(f, :signed_envelopes_in_resp) %>
-
-
-
- <%= label f, :signed_envelopes_in_resp, class: "switch is-medium" do %>
- <%= checkbox(f, :signed_envelopes_in_resp) %>
-
- <% end %>
-
-
-
-
-
-
-
-
Auto-create users
-
-
-
-
- Automatically provision users when signing in for the first time.
-
-
- <%= error_tag(f, :auto_create_users) %>
-
-
-
- <%= label f, :auto_create_users, class: "switch is-medium" do %>
- <%= checkbox(f, :auto_create_users) %>
-
- <% end %>
-
-
-
-
-
- """
- end
-
- def update(assigns, socket) do
- changeset =
- assigns.provider
- |> Map.delete(:__struct__)
- |> Domain.Config.Configuration.SAMLIdentityProvider.create_changeset()
-
- socket =
- socket
- |> assign(assigns)
- |> assign(:changeset, changeset)
-
- {:ok, socket}
- end
-
- def handle_event("save", %{"saml_identity_provider" => params}, socket) do
- changeset = Domain.Config.Configuration.SAMLIdentityProvider.create_changeset(params)
-
- if changeset.valid? do
- attrs = Ecto.Changeset.apply_changes(changeset)
-
- config = Config.fetch_db_config!()
-
- saml_identity_providers =
- config.saml_identity_providers
- |> Enum.reject(&(&1.id == socket.assigns.provider.id))
- |> Kernel.++([attrs])
- |> Enum.map(&Map.from_struct/1)
-
- {:ok, _config} =
- Config.update_config(
- config,
- %{saml_identity_providers: saml_identity_providers},
- socket.assigns.subject
- )
-
- socket =
- socket
- |> put_flash(:info, "Updated successfully.")
- |> redirect(to: socket.assigns.return_to)
-
- {:noreply, socket}
- else
- socket =
- socket
- |> assign(:changeset, render_changeset_errors(changeset))
-
- {:noreply, socket}
- end
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/security.html.heex b/apps/web/lib/web/live/setting_live/security.html.heex
deleted file mode 100644
index e11b363ac..000000000
--- a/apps/web/lib/web/live/setting_live/security.html.heex
+++ /dev/null
@@ -1,351 +0,0 @@
-<% openid_connect_providers = Map.fetch!(@configs, :openid_connect_providers) %>
-<% saml_identity_providers = Map.fetch!(@configs, :saml_identity_providers) %>
-
-<%= if @live_action == :edit_oidc do %>
- <%= live_modal(
- Web.SettingLive.OIDCFormComponent,
- subject: @subject,
- return_to: ~p"/settings/security",
- title: "OIDC Configuration",
- provider: get_provider(config_value(openid_connect_providers), @id) || %{id: @id},
- id: "oidc-form-component",
- form: "oidc-form"
- ) %>
-<% end %>
-
-<%= if @live_action == :edit_saml do %>
- <%= live_modal(
- Web.SettingLive.SAMLFormComponent,
- subject: @subject,
- return_to: ~p"/settings/security",
- title: "SAML Configuration",
- provider: get_provider(config_value(saml_identity_providers), @id) || %{id: @id},
- id: "saml-form-component",
- form: "saml-form"
- ) %>
-<% end %>
-
-<%= render(Web.SharedView, "heading.html",
- page_subtitle: @page_subtitle,
- page_title: @page_title
-) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
- Authentication
-
-
- <% vpn_session_duration = Map.fetch!(@configs, :vpn_session_duration) %>
- <.form
- :let={f}
- for={@configuration_changeset}
- phx-change="change"
- phx-submit="save_configuration"
- >
-
- <%= label(f, :vpn_session_duration, "Require Authentication For VPN Sessions",
- class: "label"
- ) %>
-
-
-
- <%= select(f, :vpn_session_duration, session_duration_options(vpn_session_duration),
- class: "input",
- disabled: config_has_override?(vpn_session_duration),
- selected: config_value(vpn_session_duration)
- ) %>
-
-
-
- <%= submit("Save",
- disabled: !(@form_changed and !config_has_override?(vpn_session_duration)),
- phx_disable_with: "Saving...",
- class: "button is-primary"
- ) %>
-
-
-
- <%= if config_has_override?(vpn_session_duration) do %>
- This field was overridden using <%= config_override_source(vpn_session_duration) %>; you cannot change it.
- <% else %>
- Optionally require users to periodically authenticate to the Firezone
- web UI in order to keep their VPN sessions active.
- <% end %>
-
-
-
-
-
-
- <% local_auth_enabled = Map.fetch!(@configs, :local_auth_enabled) %>
-
Local Auth
-
-
-
-
- Enable or disable authentication with email and password.
- <%= if config_has_override?(local_auth_enabled) do %>
-
-
- This value is overridden using <%= config_override_source(local_auth_enabled) %>; you cannot change it.
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
-
-
- <% allow_unprivileged_device_management =
- Map.fetch!(@configs, :allow_unprivileged_device_management) %>
-
Allow unprivileged device management
-
-
-
-
- Enable or disable management of devices on unprivileged accounts.
- <%= if config_has_override?(allow_unprivileged_device_management) do %>
-
-
- This value is overridden using <%= config_override_source(
- allow_unprivileged_device_management
- ) %>; you cannot change it.
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
-
-
- <% allow_unprivileged_device_configuration =
- Map.fetch!(@configs, :allow_unprivileged_device_configuration) %>
-
Allow unprivileged device configuration
-
-
-
-
- Enable or disable configuration of device network settings for unprivileged users.
- <%= if config_has_override?(allow_unprivileged_device_configuration) do %>
-
-
- This value is overridden using <%= config_override_source(
- allow_unprivileged_device_configuration
- ) %>; you cannot change it.
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
-
- Single Sign-On
-
-
-
-
- <% disable_vpn_on_oidc_error = Map.fetch!(@configs, :disable_vpn_on_oidc_error) %>
-
Auto disable VPN
-
-
-
-
Enable or disable auto disabling VPN connection on OIDC refresh error.
- <%= if config_has_override?(disable_vpn_on_oidc_error) do %>
-
-
- This value is overridden using <%= config_override_source(disable_vpn_on_oidc_error) %>; you cannot change it.
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
- OpenID Connect providers configuration
-
- <%= if config_has_override?(openid_connect_providers) do %>
-
-
- You cannot add new change providers because this value is overridden using <%= config_override_source(
- openid_connect_providers
- ) %>.
-
-
- <% end %>
-
-
-
-
- Config ID
- Label
- Client ID
- Discovery URI
- Scope
- <%= unless config_has_override?(openid_connect_providers) do %>
-
- <% end %>
-
-
-
- <%= for provider <- config_value(openid_connect_providers) do %>
-
- <%= provider.id %>
- <%= provider.label %>
- <%= provider.client_id %>
- <%= provider.discovery_document_uri %>
- <%= provider.scope %>
- <%= unless config_has_override?(openid_connect_providers) do %>
-
- <%= live_patch(to: ~p"/settings/security/oidc/#{provider.id}/edit",
- class: "button") do %>
- Edit
- <% end %>
-
- Delete
-
-
- <% end %>
-
- <% end %>
-
-
-
- <%= unless config_has_override?(openid_connect_providers) do %>
- <%= live_patch(
- to: ~p"/settings/security/oidc/#{rand_string(8)}/edit",
- class: "button mb-4") do %>
- Add OpenID Connect Provider
- <% end %>
- <% end %>
-
- SAML identity providers configuration
-
- <%= if config_has_override?(saml_identity_providers) do %>
-
-
- You cannot add new change providers because this value is overridden using <%= config_override_source(
- saml_identity_providers
- ) %>.
-
-
- <% end %>
-
-
-
-
- Config ID
- label
- Metadata
- <%= unless config_has_override?(saml_identity_providers) do %>
-
- <% end %>
-
-
-
- <%= for provider <- config_value(saml_identity_providers) do %>
-
- <%= provider.id %>
- <%= provider.label %>
-
- <%= provider.metadata %>
-
- <%= unless config_has_override?(saml_identity_providers) do %>
-
- <%= live_patch(to: ~p"/settings/security/saml/#{provider.id}/edit",
- class: "button") do %>
- Edit
- <% end %>
-
- Delete
-
-
- <% end %>
-
- <% end %>
-
-
-
- <%= unless config_has_override?(saml_identity_providers) do %>
- <%= live_patch(
- to: ~p"/settings/security/saml/#{rand_string(8)}/edit",
- class: "button mb-4") do %>
- Add SAML Identity Provider
- <% end %>
- <% end %>
-
diff --git a/apps/web/lib/web/live/setting_live/security_live.ex b/apps/web/lib/web/live/setting_live/security_live.ex
deleted file mode 100644
index 09615aa3d..000000000
--- a/apps/web/lib/web/live/setting_live/security_live.ex
+++ /dev/null
@@ -1,142 +0,0 @@
-defmodule Web.SettingLive.Security do
- use Web, :live_view
- import Domain.Crypto, only: [rand_string: 1]
- alias Domain.Config
-
- @page_title "Security Settings"
- @page_subtitle "Configure security-related settings."
-
- @hour 3_600
- @day 24 * @hour
-
- @configs ~w[
- local_auth_enabled
- disable_vpn_on_oidc_error
- allow_unprivileged_device_management
- allow_unprivileged_device_configuration
- vpn_session_duration
- openid_connect_providers
- saml_identity_providers
- ]a
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- socket =
- socket
- |> assign(:page_title, @page_title)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(:form_changed, false)
- |> assign(:configuration_changeset, configuration_changeset())
- |> assign(:configs, Domain.Config.fetch_source_and_configs!(@configs))
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_params(params, _uri, socket) do
- {:noreply, assign(socket, :id, params["id"])}
- end
-
- @impl Phoenix.LiveView
- def handle_event("change", %{"configuration" => attrs}, socket) do
- changeset = configuration_changeset(attrs)
- {:noreply, assign(socket, :form_changed, changeset.changes != %{})}
- end
-
- @impl Phoenix.LiveView
- def handle_event("save_configuration", %{"configuration" => attrs}, socket) do
- configuration = Config.fetch_db_config!()
-
- socket =
- case Config.update_config(configuration, attrs) do
- {:ok, configuration} ->
- socket
- |> assign(:form_changed, false)
- |> assign(:configuration_changeset, Config.change_config(configuration))
-
- {:error, configuration_changeset} ->
- socket
- |> assign(:configuration_changeset, configuration_changeset)
- end
-
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_event("toggle", %{"config" => key} = params, socket) do
- {:ok, _config} =
- Config.fetch_db_config!()
- |> Config.update_config(%{key => !!params["value"]}, socket.assigns.subject)
-
- configs = Domain.Config.fetch_source_and_configs!(@configs)
- {:noreply, assign(socket, :configs, configs)}
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete", %{"type" => type, "key" => key}, socket) do
- field_key = String.to_existing_atom(type)
-
- config = Config.fetch_db_config!()
-
- providers =
- config
- |> Map.fetch!(field_key)
- |> Enum.reject(&(&1.id == key))
- |> Enum.map(&Map.from_struct/1)
-
- {:ok, _config} =
- Config.update_config(config, %{field_key => providers}, socket.assigns.subject)
-
- configs = Domain.Config.fetch_source_and_configs!(@configs)
-
- {:noreply, assign(socket, :configs, configs)}
- end
-
- def config_has_override?({{source, _source_key}, _key}), do: source not in [:db]
- def config_has_override?({_source, _key}), do: false
-
- def config_value({_source, value}) do
- value
- end
-
- def get_provider(providers, id) do
- Enum.find(providers, &(&1.id == id))
- end
-
- def config_toggle_status({_source, value}) do
- if(!value, do: "on")
- end
-
- def config_override_source({{:env, source_key}, _value}) do
- "environment variable #{source_key}"
- end
-
- def session_duration_options(vpn_session_duration) do
- options = [
- {"Never", 0},
- {"Once", Domain.Config.Configuration.Changeset.max_vpn_session_duration()},
- {"Every Hour", @hour},
- {"Every Day", @day},
- {"Every Week", 7 * @day},
- {"Every 30 Days", 30 * @day},
- {"Every 90 Days", 90 * @day}
- ]
-
- values = Enum.map(options, fn {_, value} -> value end)
-
- if config_value(vpn_session_duration) in values do
- options
- else
- options ++
- [
- {"Every #{config_value(vpn_session_duration)} seconds",
- config_value(vpn_session_duration)}
- ]
- end
- end
-
- defp configuration_changeset(attrs \\ %{}) do
- Config.fetch_db_config!()
- |> Config.change_config(attrs)
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/show_api_token_component.ex b/apps/web/lib/web/live/setting_live/show_api_token_component.ex
deleted file mode 100644
index 2f660424c..000000000
--- a/apps/web/lib/web/live/setting_live/show_api_token_component.ex
+++ /dev/null
@@ -1,63 +0,0 @@
-defmodule Web.SettingLive.ShowApiTokenComponent do
- use Web, :live_component
-
- alias Phoenix.LiveView.JS
- alias Web.Auth.JSON.Authentication
-
- def update(assigns, socket) do
- if connected?(socket) do
- {:ok, secret, _claims} = Authentication.fz_encode_and_sign(assigns.api_token)
-
- {:ok,
- socket
- |> assign(:secret, secret)}
- else
- {:ok, socket}
- end
- end
-
- def render(assigns) do
- ~H"""
-
- <%= if assigns[:secret] do %>
-
-
-
- API token secret:
-
-
-
-
-
-
-
-
-
-
-
-
-
Warning! This token is sensitive data. Store it somewhere safe.
-
-
-
-
cURL example:
-
# List all users
- curl -H 'Content-Type: application/json' \
- -H 'Authorization: Bearer <%= @secret %>' \
- <%= Domain.Config.fetch_env!(:web, :external_url) %>/v0/users
-
-
- <% end %>
-
- """
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/unprivileged/account.html.heex b/apps/web/lib/web/live/setting_live/unprivileged/account.html.heex
deleted file mode 100644
index e5943c51b..000000000
--- a/apps/web/lib/web/live/setting_live/unprivileged/account.html.heex
+++ /dev/null
@@ -1,132 +0,0 @@
-<%= if @live_action == :change_password do %>
- <%= live_modal(
- Web.SettingLive.Unprivileged.AccountFormComponent,
- return_to: ~p"/user_account",
- title: "Change password",
- id: "account-form-component",
- current_user: @current_user,
- subject: @subject,
- form: "account-edit"
- ) %>
-<% end %>
-
-<%= if @live_action == :register_mfa do %>
- <.live_component
- module={Web.MFA.RegisterComponent}
- id="register-mfa"
- user={@current_user}
- return_to={~p"/user_account"}
- />
-<% end %>
-
-<.link navigate={~p"/user_devices"}>
- <- Back to devices
-
-
-<%= render(Web.SharedView, "heading.html", page_title: @page_title) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
- <%= @page_subtitle %>
-
-
-
-
-
Details
-
-
-
- <%= if @local_auth_enabled do %>
- <.link replace={true} patch={~p"/user_account/change_password"} class="button">
-
-
-
- Change Password
-
- <% end %>
-
-
-
- <%= render(Web.SharedView, "user_details.html",
- user: @current_user,
- rules_path: nil,
- subject: @subject
- ) %>
-
-
-
-
- Active Sessions
-
-
-
-
- Your active Firezone web sessions. Each row corresponds to an open browser
- tab connected to Firezone.
-
-
-
-
-
-
-
- Came Online
- Last Signed In
- Remote IP
- User Agent
-
-
-
- <%= for {meta, index} <- Enum.with_index(@metas) do %>
-
-
- …
-
-
- …
-
- <%= meta.remote_ip || "-" %>
- <%= meta.user_agent %>
-
- <% end %>
-
-
-
-
-
-
-
- Multi Factor Authentication
-
-
-
-
- Your MFA methods are invoked when you login with email and password.
-
-
-
-
- <%= if length(@methods) > 0 do %>
- <%= render(Web.SharedView, "mfa_methods_table.html", methods: @methods) %>
- <% else %>
-
No MFA methods added.
- <% end %>
-
-
- <.link replace={true} patch={~p"/user_account/register_mfa"} class="button">
-
-
-
- Add MFA Method
-
-
diff --git a/apps/web/lib/web/live/setting_live/unprivileged/account_form_component.ex b/apps/web/lib/web/live/setting_live/unprivileged/account_form_component.ex
deleted file mode 100644
index 0759b1e9d..000000000
--- a/apps/web/lib/web/live/setting_live/unprivileged/account_form_component.ex
+++ /dev/null
@@ -1,30 +0,0 @@
-defmodule Web.SettingLive.Unprivileged.AccountFormComponent do
- @moduledoc """
- Handles the edit account form for unprivileged users.
- """
- use Web, :live_component
-
- alias Domain.Users
-
- def update(assigns, socket) do
- changeset = Users.change_user(assigns.current_user)
-
- {:ok,
- socket
- |> assign(assigns)
- |> assign(:changeset, changeset)}
- end
-
- def handle_event("save", %{"user" => attrs}, socket) do
- case Users.update_self(attrs, socket.assigns.subject) do
- {:ok, _user} ->
- {:noreply,
- socket
- |> put_flash(:info, "Password updated successfully.")
- |> redirect(to: socket.assigns.return_to)}
-
- {:error, changeset} ->
- {:noreply, assign(socket, :changeset, changeset)}
- end
- end
-end
diff --git a/apps/web/lib/web/live/setting_live/unprivileged/account_form_component.html.heex b/apps/web/lib/web/live/setting_live/unprivileged/account_form_component.html.heex
deleted file mode 100644
index 8df9240e6..000000000
--- a/apps/web/lib/web/live/setting_live/unprivileged/account_form_component.html.heex
+++ /dev/null
@@ -1,32 +0,0 @@
-
- <.form
- :let={f}
- for={@changeset}
- x-autocomplete="off"
- id="account-edit"
- phx-target={@myself}
- phx-submit="save"
- >
-
-
Enter new password below.
-
-
- <%= render(
- Web.SharedView,
- "password_field.html",
- context: f,
- field: :password,
- autocomplete: "new-password",
- label: "Password"
- ) %>
-
- <%= render(
- Web.SharedView,
- "password_field.html",
- context: f,
- field: :password_confirmation,
- autocomplete: "new-password",
- label: "Password Confirmation"
- ) %>
-
-
diff --git a/apps/web/lib/web/live/setting_live/unprivileged/account_live.ex b/apps/web/lib/web/live/setting_live/unprivileged/account_live.ex
deleted file mode 100644
index 2e33bb42f..000000000
--- a/apps/web/lib/web/live/setting_live/unprivileged/account_live.ex
+++ /dev/null
@@ -1,75 +0,0 @@
-defmodule Web.SettingLive.Unprivileged.Account do
- @moduledoc """
- Handles Account-related things for unprivileged users.
-
- XXX: At this moment, this is a carbon copy of the admin account live view.
- Only the html is going to be different. This serves its purpose until a
- redesign happens.
- """
- use Web, :live_view
-
- alias Domain.{Auth.MFA, Users}
- alias Web.{Endpoint, Presence}
-
- @live_sessions_topic "notification:session"
- @page_title "Account Settings"
- @page_subtitle "Configure account settings."
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- Endpoint.subscribe(@live_sessions_topic)
-
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
-
- socket =
- socket
- |> assign(:local_auth_enabled, Domain.Config.fetch_config!(:local_auth_enabled))
- |> assign(:changeset, Users.change_user(socket.assigns.current_user))
- |> assign(:methods, methods)
- |> assign(:page_title, @page_title)
- |> assign(:page_subtitle, @page_subtitle)
- |> assign(
- :metas,
- get_metas(Presence.list(@live_sessions_topic), socket.assigns.current_user.id)
- )
-
- {:ok, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_params(_params, _url, socket) do
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete_authenticator", %{"id" => id}, socket) do
- with {:ok, _method} <- MFA.delete_method_by_id(id, socket.assigns.current_user) do
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
- {:noreply, assign(socket, :methods, methods)}
- else
- {:error, :not_found} ->
- {:ok, methods} = MFA.list_methods_for_user(socket.assigns.current_user)
- {:noreply, assign(socket, :methods, methods)}
-
- false ->
- {:noreply, socket}
- end
- end
-
- @impl Phoenix.LiveView
- def handle_info(
- %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}},
- %{assigns: %{metas: metas}} = socket
- ) do
- metas =
- (metas ++
- get_metas(joins, socket.assigns.current_user.id)) --
- get_metas(leaves, socket.assigns.current_user.id)
-
- {:noreply, assign(socket, :metas, metas)}
- end
-
- defp get_metas(presences, user_id) do
- get_in(presences, [user_id, :metas]) || []
- end
-end
diff --git a/apps/web/lib/web/live/sidebar_component.ex b/apps/web/lib/web/live/sidebar_component.ex
deleted file mode 100644
index 7639812ef..000000000
--- a/apps/web/lib/web/live/sidebar_component.ex
+++ /dev/null
@@ -1,85 +0,0 @@
-defmodule Web.SidebarComponent do
- @moduledoc """
- Admin Sidebar
- """
- use Web, :live_component
-
- def render(assigns) do
- ~H"""
-
- """
- end
-
- def nav_class(path, prefix) do
- if String.starts_with?(path, prefix) do
- "is-active has-icon"
- else
- "has-icon"
- end
- end
-end
diff --git a/apps/web/lib/web/live/user_live/form_component.ex b/apps/web/lib/web/live/user_live/form_component.ex
deleted file mode 100644
index 76b57b8ef..000000000
--- a/apps/web/lib/web/live/user_live/form_component.ex
+++ /dev/null
@@ -1,62 +0,0 @@
-defmodule Web.UserLive.FormComponent do
- @moduledoc """
- Handles user form for admins.
- """
- use Web, :live_component
-
- alias Domain.Users
-
- @impl Phoenix.LiveComponent
- def update(%{action: :new} = assigns, socket) do
- changeset = Users.change_user()
-
- {:ok,
- socket
- |> assign(assigns)
- |> assign(:changeset, changeset)}
- end
-
- @impl Phoenix.LiveComponent
- def update(%{action: :edit} = assigns, socket) do
- changeset = Users.change_user(assigns.user)
-
- {:ok,
- socket
- |> assign(assigns)
- |> assign(:changeset, changeset)}
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("save", %{"user" => attrs}, %{assigns: %{action: :new}} = socket) do
- case Users.create_user(:unprivileged, attrs, socket.assigns.subject) do
- {:ok, user} ->
- {:noreply,
- socket
- |> assign(:user, user)
- |> put_flash(:info, "User created successfully.")
- |> push_redirect(to: ~p"/users/#{user}")}
-
- {:error, changeset} ->
- {:noreply,
- socket
- |> assign(:changeset, changeset)}
- end
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("save", %{"user" => user_params}, %{assigns: %{action: :edit}} = socket) do
- user = socket.assigns.user
-
- case Users.update_user(user, user_params) do
- {:ok, user} ->
- {:noreply,
- socket
- |> assign(:user, user)
- |> put_flash(:info, "User updated successfully.")
- |> push_redirect(to: socket.assigns.return_to)}
-
- {:error, changeset} ->
- {:noreply, assign(socket, :changeset, changeset)}
- end
- end
-end
diff --git a/apps/web/lib/web/live/user_live/form_component.html.heex b/apps/web/lib/web/live/user_live/form_component.html.heex
deleted file mode 100644
index 931767ce2..000000000
--- a/apps/web/lib/web/live/user_live/form_component.html.heex
+++ /dev/null
@@ -1,45 +0,0 @@
-
- <.form
- :let={f}
- for={@changeset}
- x-autocomplete="off"
- id="user-form"
- phx-target={@myself}
- phx-submit="save"
- >
- <%= if @action == :edit do %>
-
-
Change user email or enter new password below.
-
- <% end %>
-
-
- <%= label(f, :email, class: "label") %>
-
-
- <%= text_input(f, :email, class: "input #{input_error_class(f, :email)}") %>
-
-
- <%= error_tag(f, :email) %>
-
-
-
- <%= render(
- Web.SharedView,
- "password_field.html",
- context: f,
- field: :password,
- autocomplete: "new-password",
- label: "New Password"
- ) %>
-
- <%= render(
- Web.SharedView,
- "password_field.html",
- context: f,
- field: :password_confirmation,
- autocomplete: "new-password",
- label: "New Password Confirmation"
- ) %>
-
-
diff --git a/apps/web/lib/web/live/user_live/index.html.heex b/apps/web/lib/web/live/user_live/index.html.heex
deleted file mode 100644
index a39073b47..000000000
--- a/apps/web/lib/web/live/user_live/index.html.heex
+++ /dev/null
@@ -1,76 +0,0 @@
-<%= if @live_action == :new do %>
- <%= live_modal(
- Web.UserLive.FormComponent,
- return_to: ~p"/users",
- title: "Add User",
- id: "user-form-component",
- user: nil,
- current_user: @current_user,
- subject: @subject,
- action: @live_action,
- form: "user-form"
- ) %>
-<% end %>
-
-<%= render(Web.SharedView, "heading.html", page_title: @page_title) %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
-
-
-
- Email
- Devices
- VPN Connection
- Last Signed In
- Last Signed In Method
- Created
- Updated
-
-
-
- <%= for user <- @users do %>
-
-
- <.link navigate={~p"/users/#{user}"}>
- <%= user.email %>
-
-
- <%= user.device_count %>
-
-
-
-
- …
-
- <%= user.last_signed_in_method %>
-
- …
-
-
- …
-
-
- <% end %>
-
-
-
-
- <.link replace={true} patch={~p"/users/new"} class="button">
- Add User
-
-
diff --git a/apps/web/lib/web/live/user_live/index_live.ex b/apps/web/lib/web/live/user_live/index_live.ex
deleted file mode 100644
index 5e4ff128d..000000000
--- a/apps/web/lib/web/live/user_live/index_live.ex
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule Web.UserLive.Index do
- @moduledoc """
- Handles User LiveViews.
- """
- use Web, :live_view
-
- alias Domain.Users
-
- @page_title "Users"
-
- @impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- with {:ok, users} <- Users.list_users(socket.assigns.subject, hydrate: [:device_count]) do
- socket =
- socket
- |> assign(:users, users)
- |> assign(:changeset, Users.change_user())
- |> assign(:page_title, @page_title)
-
- {:ok, socket}
- end
- end
-
- @impl Phoenix.LiveView
- def handle_params(_params, _url, socket) do
- {:noreply, socket}
- end
-end
diff --git a/apps/web/lib/web/live/user_live/show.html.heex b/apps/web/lib/web/live/user_live/show.html.heex
deleted file mode 100644
index 21596eab7..000000000
--- a/apps/web/lib/web/live/user_live/show.html.heex
+++ /dev/null
@@ -1,143 +0,0 @@
-<%= if @live_action == :edit do %>
- <%= live_modal(
- Web.UserLive.FormComponent,
- return_to: ~p"/users/#{@user}",
- title: "Edit #{@user.email}",
- id: "user-form-component",
- user: @user,
- current_user: @current_user,
- subject: @subject,
- action: @live_action,
- form: "user-form"
- ) %>
-<% end %>
-<%= if @live_action == :new_device do %>
- <%= live_modal(
- Web.DeviceLive.NewFormComponent,
- return_to: ~p"/users/#{@user}",
- title: "Add Device",
- current_user: @current_user,
- subject: @subject,
- user: @user,
- id: "create-device-component",
- form: "create-device",
- button_text: "Generate Configuration"
- ) %>
-<% end %>
-
-<%= render(Web.SharedView, "heading.html", page_title: "Users |> #{@user.email}") %>
-
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
-
-
-
Details
-
-
- <.link patch={~p"/users/#{@user}/edit"} replace={true} class="button">
-
-
-
- Change Email or Password
-
-
-
-
- <%= render(Web.SharedView, "user_details.html",
- user: @user,
- rules_path: @rules_path,
- subject: @subject
- ) %>
-
-
-<%= if length(@connections) > 0 do %>
- <.live_component
- id="connections-table"
- module={Web.OIDCLive.ConnectionsTableComponent}
- connections={@connections}
- user={@user}
- />
-<% end %>
-
-
- Devices
-
-
- <%= if length(@devices) > 0 do %>
- <%= render(Web.SharedView, "devices_table.html",
- devices: @devices,
- show_user: false,
- socket: @socket
- ) %>
- <% else %>
- No devices.
- <% end %>
-
-
- <.link
- patch={~p"/users/#{@user}/new_device"}
- replace={true}
- id="add-device-button"
- class="button"
- >
- Add Device
-
-
-
-
- Danger Zone
- VPN Connection
-
-
-
Enable or disable this user's VPN connection. Applies to all their devices.
-
-
- <.live_component
- id="allowed-to-connect"
- module={Web.UserLive.VPNConnectionComponent}
- user={@user}
- />
-
-
-
- Promote or Demote User
-
-
-
Promote a user to admin or demote a user to unprivileged role.
-
-
-
-
-
-
- <%= mote(@user) %>
-
-
-
-
- Delete User
-
-
-
Permanently delete a user and all their devices.
-
-
-
-
-
-
- Delete User
-
-
-
-
diff --git a/apps/web/lib/web/live/user_live/show_live.ex b/apps/web/lib/web/live/user_live/show_live.ex
deleted file mode 100644
index 03929b4ad..000000000
--- a/apps/web/lib/web/live/user_live/show_live.ex
+++ /dev/null
@@ -1,122 +0,0 @@
-defmodule Web.UserLive.Show do
- @moduledoc """
- Handles showing users.
- XXX: Admin only
- """
- use Web, :live_view
-
- alias Domain.{Devices, Auth.OIDC, Users}
- alias Web.ErrorHelpers
-
- @impl Phoenix.LiveView
-
- def mount(%{"id" => user_id} = _params, _session, socket) do
- {:ok, user} = Users.fetch_user_by_id(user_id, socket.assigns.subject)
- {:ok, devices} = Devices.list_devices_for_user(user, socket.assigns.subject)
- connections = OIDC.list_connections(user)
-
- {:ok,
- socket
- |> assign(:devices, devices)
- |> assign(:device_config, socket.assigns[:device_config])
- |> assign(:connections, connections)
- |> assign(:user, user)
- |> assign(:page_title, "Users")
- |> assign(:rules_path, ~p"/rules")}
- end
-
- @doc """
- Called when a modal is dismissed; reload devices.
- """
- @impl Phoenix.LiveView
- def handle_params(%{"id" => user_id} = _params, _url, socket) do
- {:ok, user} = Users.fetch_user_by_id(user_id, socket.assigns.subject)
- {:ok, devices} = Devices.list_devices_for_user(user, socket.assigns.subject)
-
- socket =
- socket
- |> assign(:devices, devices)
-
- {:noreply, socket}
- end
-
- @impl Phoenix.LiveView
- def handle_event("delete_user", %{"user_id" => user_id}, socket) do
- if user_id == "#{socket.assigns.current_user.id}" do
- {:noreply,
- socket
- |> put_flash(:error, "Use the account section to delete your account.")}
- else
- {:ok, user} = Users.fetch_user_by_id(user_id, socket.assigns.subject)
-
- case Users.delete_user(user, socket.assigns.subject) do
- {:ok, _} ->
- Web.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
-
- {:noreply,
- socket
- |> put_flash(:info, "User deleted successfully.")
- |> push_redirect(to: ~p"/users")}
-
- {:error, changeset} ->
- {:noreply,
- socket
- |> put_flash(
- :error,
- "Error deleting user: #{ErrorHelpers.aggregated_errors(changeset)}"
- )}
- end
- end
- end
-
- @impl Phoenix.LiveView
- def handle_event(action, %{"user_id" => user_id}, socket) when action in ~w(promote demote) do
- role =
- case action do
- "promote" -> :admin
- "demote" -> :unprivileged
- end
-
- with {:ok, user} <- Users.fetch_user_by_id(user_id, socket.assigns.subject),
- {:ok, user} <- Users.update_user(user, %{role: role}, socket.assigns.subject) do
- # Force reconnect with new role
- Web.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
-
- socket =
- socket
- |> assign(:user, user)
- |> put_flash(:info, "User updated successfully.")
-
- {:noreply, socket}
- else
- {:error, %Ecto.Changeset{} = changeset} ->
- message = "Error, #{ErrorHelpers.aggregated_errors(changeset)}"
- socket = put_flash(socket, :error, message)
- {:noreply, socket}
-
- {:error, reason} ->
- message = "Error updating user: #{inspect(reason)}"
- socket = put_flash(socket, :error, message)
- {:noreply, socket}
- end
- end
-
- @action_and_message %{
- admin: %{
- action: "demote",
- message: "This will remove admin permissions from the user."
- },
- unprivileged: %{
- action: "promote",
- message: "This will give admin permissions to the user."
- }
- }
-
- defp mote(%{role: role}) do
- @action_and_message[role].action
- end
-
- defp mote_message(%{role: role}) do
- @action_and_message[role].message
- end
-end
diff --git a/apps/web/lib/web/live/user_live/vpn_connection_component.ex b/apps/web/lib/web/live/user_live/vpn_connection_component.ex
deleted file mode 100644
index efba8b68f..000000000
--- a/apps/web/lib/web/live/user_live/vpn_connection_component.ex
+++ /dev/null
@@ -1,53 +0,0 @@
-defmodule Web.UserLive.VPNConnectionComponent do
- @moduledoc """
- Handles user form.
- """
- use Web, :live_component
-
- import Ecto.Changeset
- alias Domain.Repo
-
- @impl Phoenix.LiveComponent
- def render(assigns) do
- ~H"""
-
-
-
-
- """
- end
-
- @impl Phoenix.LiveComponent
- def handle_event("toggle_disabled_at", params, socket) do
- to_disable = !params["value"]
-
- user =
- socket.assigns.user
- |> change()
- |> put_change(
- :disabled_at,
- if(to_disable, do: DateTime.utc_now(), else: nil)
- )
- |> prepare_changes(fn
- %{changes: %{disabled_at: nil}} = changeset ->
- changeset
-
- %{data: user} = changeset ->
- Domain.Telemetry.disable_user()
- Web.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
- changeset
- end)
- |> Repo.update!()
-
- {:noreply, assign(socket, :user, user)}
- end
-end
diff --git a/apps/web/lib/web/live/user_live/vpn_status_component.ex b/apps/web/lib/web/live/user_live/vpn_status_component.ex
deleted file mode 100644
index c2e02236e..000000000
--- a/apps/web/lib/web/live/user_live/vpn_status_component.ex
+++ /dev/null
@@ -1,54 +0,0 @@
-defmodule Web.UserLive.VPNStatusComponent do
- @moduledoc """
- Handles VPN status tag.
- """
- use Phoenix.Component
-
- def status(assigns) do
- user = assigns.user
- expired = assigns.expired
-
- cond do
- user.disabled_at -> disabled_tag(assigns)
- expired && user.last_signed_in_at -> expired_tag_sign_in(assigns)
- expired && is_nil(user.last_signed_in_at) -> expired_tag_auth(assigns)
- !expired -> enabled_tag(assigns)
- end
- end
-
- defp disabled_tag(assigns) do
- ~H"""
-
- DISABLED
-
- """
- end
-
- defp enabled_tag(assigns) do
- ~H"""
-
- ENABLED
-
- """
- end
-
- defp expired_tag_sign_in(assigns) do
- ~H"""
-
- EXPIRED
-
- """
- end
-
- defp expired_tag_auth(assigns) do
- ~H"""
- EXPIRED
- """
- end
-end
diff --git a/apps/web/lib/web/live_helpers.ex b/apps/web/lib/web/live_helpers.ex
deleted file mode 100644
index 28c96acd9..000000000
--- a/apps/web/lib/web/live_helpers.ex
+++ /dev/null
@@ -1,81 +0,0 @@
-defmodule Web.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
- """
- use Phoenix.Component
- alias Domain.{Config, Users}
-
- def live_modal(component, opts) do
- path = Keyword.fetch!(opts, :return_to)
-
- live_component(%{
- module: Web.ModalComponent,
- id: :modal,
- return_to: path,
- component: component,
- opts: 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
-
- def admin_email do
- Domain.Config.fetch_env!(:domain, :admin_email)
- end
-
- def vpn_sessions_expire? do
- Config.vpn_sessions_expire?()
- end
-
- def vpn_expires_at(user) do
- Users.vpn_session_expires_at(user)
- end
-
- def vpn_expired?(user) do
- Users.vpn_session_expired?(user)
- 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
-
- def do_not_render_changeset_errors(%Ecto.Changeset{} = changeset) do
- %{changeset | action: nil}
- end
-
- def render_changeset_errors(%Ecto.Changeset{} = changeset) do
- %{changeset | action: :validate}
- end
-
- def list_value(form, field) do
- case Phoenix.HTML.Form.input_value(form, field) do
- value when is_list(value) -> Enum.join(value, ", ")
- value -> value
- end
- end
-end
diff --git a/apps/web/lib/web/mailer.ex b/apps/web/lib/web/mailer.ex
index 72a9770de..fbc565eee 100644
--- a/apps/web/lib/web/mailer.ex
+++ b/apps/web/lib/web/mailer.ex
@@ -1,7 +1,4 @@
defmodule Web.Mailer do
- @moduledoc """
- Outbound Email Sender.
- """
use Swoosh.Mailer, otp_app: :web
alias Swoosh.Email
diff --git a/apps/web/lib/web/mailer/auth_email.ex b/apps/web/lib/web/mailer/auth_email.ex
index 30e6f4e8c..4d6f1938f 100644
--- a/apps/web/lib/web/mailer/auth_email.ex
+++ b/apps/web/lib/web/mailer/auth_email.ex
@@ -1,21 +1,9 @@
defmodule Web.Mailer.AuthEmail do
- @moduledoc """
- This module generates emails that are Auth related.
- """
- use Web, :helper
+ use Web, :verified_routes
use Phoenix.Swoosh,
template_root: Path.join(__DIR__, "templates"),
template_path: "auth_email"
alias Web.Mailer
-
- def magic_link(%Domain.Users.User{} = user) do
- Mailer.default_email()
- |> subject("Firezone Magic Link")
- |> to(user.email)
- |> render_body(:magic_link,
- link: url(~p"/auth/magic/#{user.id}/#{user.sign_in_token}")
- )
- end
end
diff --git a/apps/web/lib/web/mailer/noop_adapter.ex b/apps/web/lib/web/mailer/noop_adapter.ex
index 465139fa5..70c1342b2 100644
--- a/apps/web/lib/web/mailer/noop_adapter.ex
+++ b/apps/web/lib/web/mailer/noop_adapter.ex
@@ -4,9 +4,7 @@ defmodule Web.Mailer.NoopAdapter do
so that we don't have to add conditional logic to every single call to
`Web.Mailer.deliver/2`.
"""
-
use Swoosh.Adapter
-
require Logger
@impl true
diff --git a/apps/web/lib/web/mailer/templates/auth_email/magic_link.html.heex b/apps/web/lib/web/mailer/templates/auth_email/magic_link.html.heex
deleted file mode 100644
index 86538ffe3..000000000
--- a/apps/web/lib/web/mailer/templates/auth_email/magic_link.html.heex
+++ /dev/null
@@ -1,14 +0,0 @@
-Magic sign-in link
-
-
- Dear Firezone user,
-
-
-
- Here is the magic sign-in link . It is valid for 1 hour.
- If you didn't request for this, you can safely discard this email.
-
-
-
- If the link didn't work, please copy this link and open it in your browser. <%= @link %>
-
diff --git a/apps/web/lib/web/mailer/templates/auth_email/magic_link.text.eex b/apps/web/lib/web/mailer/templates/auth_email/magic_link.text.eex
deleted file mode 100644
index 6787b9414..000000000
--- a/apps/web/lib/web/mailer/templates/auth_email/magic_link.text.eex
+++ /dev/null
@@ -1,9 +0,0 @@
-Magic sign-in link
-
-Dear Firezone user,
-
-Here is the magic sign-in link: <%= @link %> ,
-please copy this link and open it in your browser.
-
-It is valid for 1 hour.
-If you didn't request for this, you can safely discard this email.
diff --git a/apps/web/lib/web/oauth/pkce.ex b/apps/web/lib/web/oauth/pkce.ex
deleted file mode 100644
index 0a33e5321..000000000
--- a/apps/web/lib/web/oauth/pkce.ex
+++ /dev/null
@@ -1,49 +0,0 @@
-defmodule Web.OAuth.PKCE do
- @moduledoc """
- Helpers related to PKCE for OAuth2.
- """
-
- @pkce_key "fz_pkce_code_verifier"
- @pkce_valid_duration 60
- @code_challenge_method :S256
-
- import Plug.Conn
-
- def put_cookie(conn, verifier) do
- put_resp_cookie(conn, @pkce_key, verifier, cookie_opts())
- end
-
- def token_params(conn) do
- conn
- |> fetch_cookies(signed: [@pkce_key])
- |> then(fn
- %{cookies: %{@pkce_key => verifier}} ->
- %{code_verifier: verifier}
-
- _ ->
- %{}
- end)
- end
-
- def code_challenge_method do
- @code_challenge_method
- end
-
- def code_verifier do
- :crypto.strong_rand_bytes(32)
- |> Base.url_encode64(padding: false)
- end
-
- def code_challenge(verifier) do
- :crypto.hash(:sha256, verifier) |> Base.url_encode64(padding: false)
- end
-
- defp cookie_opts do
- [
- max_age: @pkce_valid_duration,
- sign: true,
- same_site: "Lax",
- secure: Domain.Config.fetch_env!(:web, :cookie_secure)
- ]
- end
-end
diff --git a/apps/web/lib/web/oidc/state.ex b/apps/web/lib/web/oidc/state.ex
deleted file mode 100644
index ee5504781..000000000
--- a/apps/web/lib/web/oidc/state.ex
+++ /dev/null
@@ -1,39 +0,0 @@
-defmodule Web.OIDC.State do
- @moduledoc """
- Helpers to manage the OIDC CSRF token, otherwise known as the state param,
- throughout the login flow.
- """
- @oidc_state_key "fz_oidc_state"
- @oidc_state_valid_duration 300
-
- import Plug.Conn
-
- def put_cookie(conn, state) do
- put_resp_cookie(conn, @oidc_state_key, state, cookie_opts())
- end
-
- def verify_state(conn, state) do
- conn
- |> fetch_cookies(signed: [@oidc_state_key])
- |> then(fn
- %{cookies: %{@oidc_state_key => ^state}} ->
- :ok
-
- _ ->
- {:error, "Cannot verify state"}
- end)
- end
-
- def new do
- Domain.Crypto.rand_string()
- end
-
- defp cookie_opts do
- [
- max_age: @oidc_state_valid_duration,
- sign: true,
- same_site: "Lax",
- secure: Domain.Config.fetch_env!(:web, :cookie_secure)
- ]
- end
-end
diff --git a/apps/web/lib/web/plug/path_prefix.ex b/apps/web/lib/web/plug/path_prefix.ex
deleted file mode 100644
index f31faccdd..000000000
--- a/apps/web/lib/web/plug/path_prefix.ex
+++ /dev/null
@@ -1,38 +0,0 @@
-defmodule Web.Plug.PathPrefix do
- @moduledoc """
- This Plug removes prefix from Plug.Conn path fields which allows to run Firezone
- under non root directory without recompiling it.
- """
- @behaviour Plug
-
- def init(opts), do: opts
-
- def call(%Plug.Conn{request_path: request_path} = conn, _opts) do
- if path_prefix = get_path_prefix() do
- request_path_info = String.split(request_path, "/")
- trim_prefix(conn, request_path_info, path_prefix)
- else
- conn
- end
- end
-
- defp get_path_prefix do
- case Domain.Config.fetch_env!(:web, :path_prefix) do
- "/" -> nil
- nil -> nil
- prefix when is_binary(prefix) -> String.trim(prefix, "/")
- end
- end
-
- defp trim_prefix(
- %Plug.Conn{path_info: [prefix | path_info]} = conn,
- ["", prefix | request_path_info],
- prefix
- ) do
- %{conn | path_info: path_info, request_path: Enum.join([""] ++ request_path_info, "/")}
- end
-
- defp trim_prefix(%Plug.Conn{} = conn, _request_path_info, prefix) do
- Phoenix.Controller.redirect(conn, to: "/" <> Path.join(prefix, conn.request_path))
- end
-end
diff --git a/apps/web/lib/web/plug/require_local_authentication.ex b/apps/web/lib/web/plug/require_local_authentication.ex
deleted file mode 100644
index 6faa14322..000000000
--- a/apps/web/lib/web/plug/require_local_authentication.ex
+++ /dev/null
@@ -1,17 +0,0 @@
-defmodule Web.Plug.RequireLocalAuthentication do
- use Web, :controller
-
- def init(opts), do: opts
-
- def call(conn, _opts) do
- # XXX: This should be moved to Auth
- if Domain.Config.fetch_config!(:local_auth_enabled) do
- conn
- else
- conn
- |> put_resp_content_type("text/plain")
- |> send_resp(404, "Local auth disabled")
- |> halt()
- end
- end
-end
diff --git a/apps/web/lib/web/plug/samly_target_url.ex b/apps/web/lib/web/plug/samly_target_url.ex
deleted file mode 100644
index d5039c4a7..000000000
--- a/apps/web/lib/web/plug/samly_target_url.ex
+++ /dev/null
@@ -1,13 +0,0 @@
-defmodule Web.Plug.SamlyTargetUrl do
- @moduledoc """
- Plug to set target url for samly to later on redirect to after auth success
- """
-
- import Plug.Conn
-
- def init(opts), do: opts
-
- def call(conn, _opt) do
- put_session(conn, "target_url", "/auth/saml/callback")
- end
-end
diff --git a/apps/web/lib/web/presence.ex b/apps/web/lib/web/presence.ex
deleted file mode 100644
index 4e0c299d1..000000000
--- a/apps/web/lib/web/presence.ex
+++ /dev/null
@@ -1,5 +0,0 @@
-defmodule Web.Presence do
- use Phoenix.Presence,
- otp_app: :domain,
- pubsub_server: Domain.PubSub
-end
diff --git a/apps/web/lib/web/proxy_headers.ex b/apps/web/lib/web/proxy_headers.ex
deleted file mode 100644
index 6ef764738..000000000
--- a/apps/web/lib/web/proxy_headers.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-defmodule Web.ProxyHeaders do
- @moduledoc """
- Loads proxy-related headers when it corresponds using runtime config
- """
- alias Web.HeaderHelpers
-
- @behaviour Plug
-
- def init(opts), do: opts
-
- def call(conn, _opts) do
- conn
- |> RemoteIp.call(RemoteIp.init(HeaderHelpers.remote_ip_opts()))
- |> Plug.RewriteOn.call(rewrite_opts())
- end
-
- defp rewrite_opts, do: Plug.RewriteOn.init([:x_forwarded_proto])
-end
diff --git a/apps/web/lib/web/router.ex b/apps/web/lib/web/router.ex
index 743c2ed5d..a30c45a6b 100644
--- a/apps/web/lib/web/router.ex
+++ b/apps/web/lib/web/router.ex
@@ -1,207 +1,28 @@
defmodule Web.Router do
- @moduledoc """
- Main Application Router
- """
-
use Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
- plug :put_root_layout, {Web.LayoutView, :root}
+ plug :put_root_layout, {Web.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
- plug Web.Auth.JSON.Pipeline
+ # TODO: auth
end
pipeline :browser_static do
plug :accepts, ["html", "xml"]
end
- pipeline :require_authenticated do
- plug Guardian.Plug.EnsureAuthenticated
- end
-
- pipeline :require_unauthenticated do
- plug Guardian.Plug.EnsureNotAuthenticated
- end
-
- pipeline :html_auth do
- plug Web.Auth.HTML.Pipeline
- end
-
- pipeline :require_local_auth do
- plug Web.Plug.RequireLocalAuthentication
- end
-
- pipeline :samly do
- plug :fetch_session
- plug Web.Plug.SamlyTargetUrl
- end
-
- # Local auth routes
- scope "/auth", Web do
- pipe_through [
- :browser,
- :html_auth,
- :require_unauthenticated,
- :require_local_auth
- ]
-
- get "/reset_password", AuthController, :reset_password
- post "/magic_link", AuthController, :magic_link
- get "/magic/:user_id/:token", AuthController, :magic_sign_in
-
- get "/identity", AuthController, :request
- get "/identity/callback", AuthController, :callback
- post "/identity/callback", AuthController, :callback
- end
-
- # OIDC auth routes
- scope "/auth", Web do
- scope "/oidc" do
- pipe_through [
- :browser,
- :require_unauthenticated
- ]
-
- get "/", AuthController, :request
- get "/:provider/callback", AuthController, :oidc_callback
- get "/:provider", AuthController, :redirect_oidc_auth_uri
- end
- end
-
- # SAML auth routes
- scope "/auth/saml", Web do
- pipe_through [
- :browser,
- :require_unauthenticated
- ]
-
- get "/", AuthController, :request
- get "/callback", AuthController, :saml_callback
- end
-
- scope "/auth/saml" do
- pipe_through [
- :samly,
- :require_unauthenticated
- ]
-
- forward "/", Samly.Router
- end
-
- # Unauthenticated routes
scope "/", Web do
- pipe_through [
- :browser,
- :html_auth,
- :require_unauthenticated
- ]
+ pipe_through :browser
- get "/", RootController, :index
- end
-
- scope "/mfa", Web do
- pipe_through([
- :browser,
- :html_auth
- ])
-
- live_session(
- :authenticated,
- on_mount: [
- Web.Hooks.AllowEctoSandbox,
- {Web.LiveAuth, :any},
- {Web.LiveNav, nil}
- ],
- root_layout: {Web.LayoutView, :root}
- ) do
- live "/auth", MFALive.Auth, :auth
- live "/auth/:id", MFALive.Auth, :auth
- live "/types", MFALive.Auth, :types
- end
- end
-
- # Authenticated routes
- scope "/", Web do
- pipe_through [
- :browser,
- :html_auth,
- :require_authenticated
- ]
-
- delete "/sign_out", AuthController, :delete
- delete "/user", UserController, :delete
-
- # Unprivileged Live routes
- live_session(
- :unprivileged,
- on_mount: [
- Web.Hooks.AllowEctoSandbox,
- {Web.LiveAuth, :unprivileged},
- {Web.LiveNav, nil},
- Web.LiveMFA
- ],
- root_layout: {Web.LayoutView, :unprivileged}
- ) do
- live "/user_devices", DeviceLive.Unprivileged.Index, :index
- live "/user_devices/new", DeviceLive.Unprivileged.Index, :new
- live "/user_devices/:id", DeviceLive.Unprivileged.Show, :show
-
- live "/user_account", SettingLive.Unprivileged.Account, :show
- live "/user_account/change_password", SettingLive.Unprivileged.Account, :change_password
- live "/user_account/register_mfa", SettingLive.Unprivileged.Account, :register_mfa
- end
-
- # Admin Live routes
- live_session(
- :admin,
- on_mount: [
- Web.Hooks.AllowEctoSandbox,
- {Web.LiveAuth, :admin},
- Web.LiveNav,
- Web.LiveMFA
- ],
- root_layout: {Web.LayoutView, :admin}
- ) do
- live "/users", UserLive.Index, :index
- live "/users/new", UserLive.Index, :new
- live "/users/:id", UserLive.Show, :show
- live "/users/:id/edit", UserLive.Show, :edit
- live "/users/:id/new_device", UserLive.Show, :new_device
- live "/rules", RuleLive.Index, :index
- live "/devices", DeviceLive.Admin.Index, :index
- live "/devices/:id", DeviceLive.Admin.Show, :show
- live "/settings/client_defaults", SettingLive.ClientDefaults, :show
-
- live "/settings/security", SettingLive.Security, :show
- live "/settings/security/oidc/:id/edit", SettingLive.Security, :edit_oidc
- live "/settings/security/saml/:id/edit", SettingLive.Security, :edit_saml
-
- live "/settings/account", SettingLive.Account, :show
- live "/settings/account/edit", SettingLive.Account, :edit
- live "/settings/account/register_mfa", SettingLive.Account, :register_mfa
- live "/settings/account/api_token", SettingLive.Account, :new_api_token
- live "/settings/account/api_token/:api_token_id", SettingLive.Account, :show_api_token
- live "/settings/customization", SettingLive.Customization, :show
- live "/diagnostics/connectivity_checks", ConnectivityCheckLive.Index, :index
- live "/notifications", NotificationsLive.Index, :index
- end
- end
-
- scope "/v0", Web.JSON do
- pipe_through :api
-
- resources "/configuration", ConfigurationController, singleton: true, only: [:show, :update]
- resources "/users", UserController, except: [:new, :edit]
- resources "/devices", DeviceController, except: [:new, :edit]
- resources "/rules", RuleController, except: [:new, :edit]
+ get "/", PageController, :home
end
scope "/browser", Web do
@@ -209,18 +30,4 @@ defmodule Web.Router do
get "/config.xml", BrowserController, :config
end
-
- if Mix.env() in [:dev, :test] do
- import Phoenix.LiveDashboard.Router
-
- scope "/dev" do
- pipe_through [:browser]
-
- forward "/mailbox", Plug.Swoosh.MailboxPreview
- live_dashboard "/dashboard"
-
- get "/samly", Web.DebugController, :samly
- get "/session", Web.DebugController, :session
- end
- end
end
diff --git a/apps/web/lib/web/sandbox.ex b/apps/web/lib/web/sandbox.ex
deleted file mode 100644
index e953f7f06..000000000
--- a/apps/web/lib/web/sandbox.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-defmodule Web.Sandbox do
- @moduledoc """
- A set of helpers that allow Phoenix components (Channels and LiveView) to access SQL sandbox in test environment.
- """
-
- def allow_channel_sql_sandbox(socket) do
- if Map.has_key?(socket.assigns, :user_agent) do
- allow(socket.assigns.user_agent)
- end
-
- socket
- end
-
- def allow_live_ecto_sandbox(socket) do
- if Phoenix.LiveView.connected?(socket) do
- socket
- |> Phoenix.LiveView.get_connect_info(:user_agent)
- |> allow()
- end
-
- socket
- end
-
- if Mix.env() in [:test, :dev] do
- defp allow(metadata) do
- # We notify the test process that there is someone trying to access the sandbox,
- # so that it can optionally await after test has passed for the sandbox to be
- # closed gracefully
- case Phoenix.Ecto.SQL.Sandbox.decode_metadata(metadata) do
- %{owner: owner_pid} -> send(owner_pid, {:sandbox_access, self()})
- _ -> :ok
- end
-
- Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
- end
- else
- defp allow(_metadata) do
- :ok
- end
- end
-end
diff --git a/apps/web/lib/web/session.ex b/apps/web/lib/web/session.ex
index bc6771146..f760d5a46 100644
--- a/apps/web/lib/web/session.ex
+++ b/apps/web/lib/web/session.ex
@@ -1,8 +1,4 @@
defmodule Web.Session do
- @moduledoc """
- Dynamically configures session.
- """
-
# 4 hours
@max_cookie_age 14_400
@@ -21,7 +17,11 @@ defmodule Web.Session do
def options do
@session_options ++
- [secure: cookie_secure(), signing_salt: signing_salt(), encryption_salt: encryption_salt()]
+ [
+ secure: cookie_secure(),
+ signing_salt: signing_salt(),
+ encryption_salt: encryption_salt()
+ ]
end
defp cookie_secure do
diff --git a/apps/web/lib/web/sockets/user_socket.ex b/apps/web/lib/web/sockets/user_socket.ex
deleted file mode 100644
index ee0daefcd..000000000
--- a/apps/web/lib/web/sockets/user_socket.ex
+++ /dev/null
@@ -1,87 +0,0 @@
-defmodule Web.UserSocket do
- use Phoenix.Socket
- alias Web.HeaderHelpers
-
- @blank_ip_warning """
- Client IP couldn't be determined! Check to ensure your reverse proxy is properly sending the \
- X-Forwarded-For header. Read more in our reverse proxy docs: \
- https://docs.firezone.dev/deploy/reverse-proxies?utm_source=code \
- """
-
- # 1 day channel tokens
- @token_verify_opts [max_age: 86_400]
-
- require Logger
-
- ## Channels
- # channel "room:*", Web.RoomChannel
- channel("notification:session", Web.NotificationChannel)
-
- # Socket params are passed from the client and can
- # be used to verify and authenticate a user. After
- # verification, you can put default assigns into
- # the socket that will be set for all channels, ie
- #
- # {:ok, assign(socket, :user_id, verified_user_id)}
- #
- # To deny connection, return `:error`.
- #
- # See `Phoenix.Token` documentation for examples in
- # performing token verification on connect.
- def connect(%{"token" => token}, socket, connect_info) do
- socket = assign(socket, :user_agent, connect_info[:user_agent])
-
- parse_ip(connect_info)
- |> verify_token_and_assign_remote_ip(token, socket)
- end
-
- defp parse_ip(connect_info) do
- case get_ip_address(connect_info) do
- ip when ip in ["", nil] ->
- Logger.warn(@blank_ip_warning)
- Logger.warn(connect_info)
- :x_forward_for_header_issue
-
- ip when is_tuple(ip) ->
- :inet.ntoa(ip) |> List.to_string()
- end
- end
-
- defp verify_token_and_assign_remote_ip(ip, token, socket) do
- case Phoenix.Token.verify(socket, "user auth", token, @token_verify_opts) do
- {:ok, user_id} ->
- socket =
- socket
- |> assign(:current_user_id, user_id)
- |> assign(:remote_ip, ip)
-
- {:ok, socket}
-
- {:error, reason} ->
- {:error, reason}
- end
- end
-
- # No proxy
- defp get_ip_address(%{peer_data: %{address: address}, x_headers: []}) do
- address
- end
-
- # Proxied
- defp get_ip_address(%{x_headers: x_headers}) do
- RemoteIp.from(x_headers, HeaderHelpers.remote_ip_opts())
- end
-
- # Socket id's are topics that allow you to identify all sockets for a given user:
- #
- # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
- #
- # Would allow you to broadcast a "disconnect" event and terminate
- # all active sockets and channels for a given user:
- #
- # Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
- #
- # Returning `nil` makes this socket anonymous.
- # def id(_socket), do: nil
- def id(socket), do: "user_socket:#{socket.assigns.current_user_id}"
-end
diff --git a/apps/web/lib/web/telemetry.ex b/apps/web/lib/web/telemetry.ex
new file mode 100644
index 000000000..a22b5e33c
--- /dev/null
+++ b/apps/web/lib/web/telemetry.ex
@@ -0,0 +1,69 @@
+defmodule Web.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ # Telemetry poller will execute the given period measurements
+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.start.system_time",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.start.system_time",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.exception.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.socket_connected.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_join.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_handled_in.duration",
+ tags: [:event],
+ unit: {:native, :millisecond}
+ ),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {Web, :count_users, []}
+ ]
+ end
+end
diff --git a/apps/web/lib/web/templates/auth/request.html.heex b/apps/web/lib/web/templates/auth/request.html.heex
deleted file mode 100644
index 1929f5100..000000000
--- a/apps/web/lib/web/templates/auth/request.html.heex
+++ /dev/null
@@ -1,48 +0,0 @@
-Sign In
-
-
-
-
- <%= link("<- Back to sign in methods", to: ~p"/") %>
-
-
-
- <%= form_tag @callback_path, method: "post" do %>
-
-
-
-
-
-
-
-
- <%= submit("Sign In", class: "button") %>
-
-
- <%= link("Forgot password",
- to: ~p"/auth/reset_password",
- class: "forgot-password"
- ) %>
-
-
-
-
- <% end %>
-
diff --git a/apps/web/lib/web/templates/auth/reset_password.html.heex b/apps/web/lib/web/templates/auth/reset_password.html.heex
deleted file mode 100644
index f5e54863d..000000000
--- a/apps/web/lib/web/templates/auth/reset_password.html.heex
+++ /dev/null
@@ -1,25 +0,0 @@
-Reset Password
-
-
-
-
- <%= link("<- Back to sign in methods", to: ~p"/") %>
-
-
-
- <%= form_tag ~p"/auth/magic_link", method: "post" do %>
-
-
Email
-
-
-
-
We will send you a single-use magic link for signing in.
-
-
-
-
- <%= submit("Send", class: "button") %>
-
-
- <% end %>
-
diff --git a/apps/web/lib/web/templates/browser/browserconfig.xml.eex b/apps/web/lib/web/templates/browser/browserconfig.xml.eex
deleted file mode 100644
index 3fbc28e6f..000000000
--- a/apps/web/lib/web/templates/browser/browserconfig.xml.eex
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- " />
- 331700
-
-
-
diff --git a/apps/web/lib/web/templates/layout/admin.html.heex b/apps/web/lib/web/templates/layout/admin.html.heex
deleted file mode 100644
index c9ede37cd..000000000
--- a/apps/web/lib/web/templates/layout/admin.html.heex
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
- <.live_title prefix="Firezone • ">
- <%= assigns[:page_title] %>
-
-
-
- <%= render(Web.SharedView, "head.html", assigns) %>
- <%= render(Web.SharedView, "socket_token_headers.html",
- conn: @conn,
- current_user: @current_user
- ) %>
-
-
-
-
-
-
-
-
-
-
-
- <%= @inner_content %>
-
-
-
-
-
diff --git a/apps/web/lib/web/templates/layout/app.html.heex b/apps/web/lib/web/templates/layout/app.html.heex
deleted file mode 100644
index f29474f2c..000000000
--- a/apps/web/lib/web/templates/layout/app.html.heex
+++ /dev/null
@@ -1,23 +0,0 @@
-<%= if !is_nil(Phoenix.Flash.get(@flash, :info)) or !is_nil(Phoenix.Flash.get(@flash, :error)) do %>
-
- <%= if Phoenix.Flash.get(@flash, :info) do %>
-
-
-
- <%= Phoenix.Flash.get(@flash, :info) %>
-
-
- <% end %>
- <%= if Phoenix.Flash.get(@flash, :error) do %>
-
-
-
- <%= Phoenix.Flash.get(@flash, :error) %>
-
-
- <% end %>
-
-<% end %>
-
- <%= @inner_content %>
-
diff --git a/apps/web/lib/web/templates/layout/email.html.heex b/apps/web/lib/web/templates/layout/email.html.heex
deleted file mode 100644
index a7cc3a991..000000000
--- a/apps/web/lib/web/templates/layout/email.html.heex
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
- <%= @inner_content %>
-
-
diff --git a/apps/web/lib/web/templates/layout/live.html.heex b/apps/web/lib/web/templates/layout/live.html.heex
deleted file mode 100644
index 62607db07..000000000
--- a/apps/web/lib/web/templates/layout/live.html.heex
+++ /dev/null
@@ -1,7 +0,0 @@
-<%= if @path do %>
-
-<% end %>
-
-
- <%= @inner_content %>
-
diff --git a/apps/web/lib/web/templates/layout/root.html.heex b/apps/web/lib/web/templates/layout/root.html.heex
deleted file mode 100644
index 48cc6f434..000000000
--- a/apps/web/lib/web/templates/layout/root.html.heex
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
- <%= csrf_meta_tag() %>
- <.live_title>
- <%= assigns[:page_title] || "Firezone" %>
-
-
-
- <%= render(Web.SharedView, "head.html", assigns) %>
-
- <%= csrf_meta_tag() %>
-
-
-
-
-
-
-
-
-
- <%= Web.LogoComponent.render(Domain.Config.fetch_config!(:logo)) %>
-
-
- <%= @inner_content %>
-
-
-
-
-
-
-
diff --git a/apps/web/lib/web/templates/layout/unprivileged.html.heex b/apps/web/lib/web/templates/layout/unprivileged.html.heex
deleted file mode 100644
index 8f8400608..000000000
--- a/apps/web/lib/web/templates/layout/unprivileged.html.heex
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- <.live_title>
- <%= assigns[:page_title] || "Firezone" %>
-
-
-
- <%= render(Web.SharedView, "socket_token_headers.html",
- current_user: @current_user,
- conn: @conn
- ) %>
- <%= render(Web.SharedView, "head.html", assigns) %>
-
-
-
-
-
-
-
-
-
- <%= Web.LogoComponent.render(Domain.Config.fetch_config!(:logo)) %>
-
-
-
- <%= @inner_content %>
-
-
-
-
-
-
-
diff --git a/apps/web/lib/web/templates/root/auth.html.heex b/apps/web/lib/web/templates/root/auth.html.heex
deleted file mode 100644
index add1b9559..000000000
--- a/apps/web/lib/web/templates/root/auth.html.heex
+++ /dev/null
@@ -1,39 +0,0 @@
-Sign In
-
-
-
-
-
- Please sign in via one of the methods below.
-
-
- <%= for provider <- @openid_connect_providers do %>
-
- <%= link(
- "Sign in with #{provider.label}",
- to: ~p"/auth/oidc/#{provider.id}",
- class: "button"
- ) %>
-
- <% end %>
-
- <%= for provider <- @saml_identity_providers do %>
-
- <%= link(
- "Sign in with #{provider.label}",
- to: "/auth/saml/auth/signin/#{provider.id}/",
- class: "button"
- ) %>
-
- <% end %>
-
- <%= if @local_enabled do %>
-
- <%= link(
- "Sign in with email",
- to: ~p"/auth/identity",
- class: "button"
- ) %>
-
- <% end %>
-
diff --git a/apps/web/lib/web/templates/shared/device_details.html.heex b/apps/web/lib/web/templates/shared/device_details.html.heex
deleted file mode 100644
index fc5614942..000000000
--- a/apps/web/lib/web/templates/shared/device_details.html.heex
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
- <%= if Web.LiveAuth.has_role?(@current_user, :admin) do %>
-
- User
-
- <%= live_redirect(@user.email, to: ~p"/users/#{@user}") %>
-
-
- <% end %>
-
-
- Name
- <%= @device.name %>
-
-
-
- Description
- <%= @device.description %>
-
-
- <%= if Domain.Config.fetch_env!(:domain, :wireguard_ipv4_enabled) do %>
-
- Tunnel IPv4
- <%= @device.ipv4 %>
-
- <% end %>
-
- <%= if Domain.Config.fetch_env!(:domain, :wireguard_ipv6_enabled) do %>
-
- Tunnel IPv6
- <%= @device.ipv6 %>
-
- <% end %>
-
-
- Remote IP
- <%= @device.remote_ip %>
-
-
-
- Latest Handshake
-
- …
-
-
-
-
- Received
- <%= to_human_bytes(@device.rx_bytes) %>
-
-
-
- Sent
- <%= to_human_bytes(@device.tx_bytes) %>
-
-
-
- Allowed IPs
- <%= list_to_string(@allowed_ips) || "None" %>
-
-
-
- DNS Servers
- <%= list_to_string(@dns) || "None" %>
-
-
-
- Endpoint
- <%= @endpoint %>:<%= @port %>
-
-
-
- Persistent Keepalive
-
- <%= if @persistent_keepalive == 0 do %>
- Disabled
- <% else %>
- Every <%= @persistent_keepalive %> seconds
- <% end %>
-
-
-
-
- MTU
- <%= @mtu %>
-
-
-
- Public key
- <%= @device.public_key %>
-
-
-
- Preshared Key
- <%= @device.preshared_key %>
-
-
-
diff --git a/apps/web/lib/web/templates/shared/devices_table.html.heex b/apps/web/lib/web/templates/shared/devices_table.html.heex
deleted file mode 100644
index e0511de72..000000000
--- a/apps/web/lib/web/templates/shared/devices_table.html.heex
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
- Name
- <%= if @show_user do %>
- User
- <% end %>
- Tunnel IPv4
- Tunnel IPv6
- Remote IP
- Latest Handshake
- Transfer
- Public key
- Created
- Updated
-
-
-
- <%= for device <- @devices do %>
-
-
- <.link navigate={~p"/devices/#{device}"}>
- <%= device.name %>
-
-
- <%= if @show_user do %>
-
- <%= live_redirect(device.user.email,
- to: ~p"/users/#{device.user}"
- ) %>
-
- <% end %>
- <%= device.ipv4 %>
- <%= device.ipv6 %>
-
- <%= device.remote_ip %>
-
-
- …
-
-
- <%= to_human_bytes(device.rx_bytes) %> received
- <%= to_human_bytes(device.tx_bytes) %> sent
-
- <%= device.public_key %>
-
- …
-
-
- …
-
-
- <% end %>
-
-
diff --git a/apps/web/lib/web/templates/shared/flash.html.heex b/apps/web/lib/web/templates/shared/flash.html.heex
deleted file mode 100644
index 2b21ab81e..000000000
--- a/apps/web/lib/web/templates/shared/flash.html.heex
+++ /dev/null
@@ -1,28 +0,0 @@
-<%= if !is_nil(live_flash(@flash, :info)) or !is_nil(live_flash(@flash, :error)) do %>
-
- <%= if live_flash(@flash, :info) do %>
-
-
-
-
<%= live_flash(@flash, :info) %>
-
- <% end %>
- <%= if live_flash(@flash, :error) do %>
-
-
-
-
<%= live_flash(@flash, :error) %>
-
- <% end %>
-
-<% end %>
diff --git a/apps/web/lib/web/templates/shared/head.html.heex b/apps/web/lib/web/templates/shared/head.html.heex
deleted file mode 100644
index 012eb0bb0..000000000
--- a/apps/web/lib/web/templates/shared/head.html.heex
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/web/lib/web/templates/shared/heading.html.heex b/apps/web/lib/web/templates/shared/heading.html.heex
deleted file mode 100644
index afa3de055..000000000
--- a/apps/web/lib/web/templates/shared/heading.html.heex
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- <%= @page_title %>
-
-
- <%= if assigns[:page_subtitle] do %>
-
-
<%= @page_subtitle %>
-
- <% end %>
-
-
diff --git a/apps/web/lib/web/templates/shared/mfa_methods_table.html.heex b/apps/web/lib/web/templates/shared/mfa_methods_table.html.heex
deleted file mode 100644
index 188483ba1..000000000
--- a/apps/web/lib/web/templates/shared/mfa_methods_table.html.heex
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- Name
- Type
- Last Used At
-
-
-
-
- <%= for method <- @methods do %>
-
-
- <%= method.name %>
-
-
- <%= method.type %>
-
-
- …
-
-
- ?"}
- phx-click="delete_authenticator"
- phx-value-id={method.id}
- >
-
-
-
- Delete
-
-
-
- <% end %>
-
-
diff --git a/apps/web/lib/web/templates/shared/password_field.html.heex b/apps/web/lib/web/templates/shared/password_field.html.heex
deleted file mode 100644
index 17af60ee1..000000000
--- a/apps/web/lib/web/templates/shared/password_field.html.heex
+++ /dev/null
@@ -1,20 +0,0 @@
-
- <%= label(@context, @field, @label, class: "label") %>
-
-
- <%= password_input(
- @context,
- @field,
- class: "input password",
- id: "#{@field}-field",
- autocomplete: "new-password",
- data_target: "#{@field}-progress",
- phx_hook: "PasswordStrength"
- ) %>
-
-
- <%= error_tag(@context, @field) %>
-
-
-
0%
-
diff --git a/apps/web/lib/web/templates/shared/show_device.html.heex b/apps/web/lib/web/templates/shared/show_device.html.heex
deleted file mode 100644
index 8069312f1..000000000
--- a/apps/web/lib/web/templates/shared/show_device.html.heex
+++ /dev/null
@@ -1,28 +0,0 @@
-
- <%= render(Web.SharedView, "flash.html", assigns) %>
-
- Details
-
- <%= render(Web.SharedView, "device_details.html", assigns) %>
-
-
-<%= if Domain.Devices.authorize_user_device_management(@current_user.id, @subject) == :ok do %>
-
-
- Danger Zone
-
-
-
-
-
-
- Delete Device <%= @device.name %>
-
-
-<% end %>
diff --git a/apps/web/lib/web/templates/shared/socket_token_headers.html.heex b/apps/web/lib/web/templates/shared/socket_token_headers.html.heex
deleted file mode 100644
index a69137bd2..000000000
--- a/apps/web/lib/web/templates/shared/socket_token_headers.html.heex
+++ /dev/null
@@ -1,12 +0,0 @@
-
-<%= tag(:meta,
- name: "user-token",
- content: Phoenix.Token.sign(@conn, "user auth", @current_user.id)
-) %>
-
-<%= tag(:meta,
- name: "channel-token",
- content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id)
-) %>
-
-<%= csrf_meta_tag() %>
diff --git a/apps/web/lib/web/templates/shared/submit_button.html.heex b/apps/web/lib/web/templates/shared/submit_button.html.heex
deleted file mode 100644
index d5e78fa80..000000000
--- a/apps/web/lib/web/templates/shared/submit_button.html.heex
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- <%= submit(assigns[:button_text] || "Save",
- phx_disable_with: "Saving...",
- form: assigns[:form],
- class: "button is-primary"
- ) %>
-
-
-
-
diff --git a/apps/web/lib/web/templates/shared/user_details.html.heex b/apps/web/lib/web/templates/shared/user_details.html.heex
deleted file mode 100644
index 9175feba0..000000000
--- a/apps/web/lib/web/templates/shared/user_details.html.heex
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
- Email
- <%= @user.email %>
-
-
-
- Role
- <%= @user.role %>
-
-
-
- Last Signed In
-
- …
-
-
-
-
- Created
-
- …
-
-
-
-
- Updated
-
- …
-
-
-
-
- Number of Devices
- <%= Domain.Devices.count_by_user_id(@user.id) %>
-
-
- <%= if @rules_path do %>
- <%= with {:ok, rules_count} <- Domain.Rules.fetch_count_by_user_id(@user.id, @subject) do %>
-
- Number of Rules
-
- <%= rules_count %>
-
-
- <% end %>
- <% end %>
-
-
diff --git a/apps/web/lib/web/user_from_auth.ex b/apps/web/lib/web/user_from_auth.ex
deleted file mode 100644
index 4ad9dc7ad..000000000
--- a/apps/web/lib/web/user_from_auth.ex
+++ /dev/null
@@ -1,46 +0,0 @@
-defmodule Web.UserFromAuth do
- @moduledoc """
- Authenticates users.
- """
- alias Domain.{Auth, Users}
- alias Web.Auth.HTML.Authentication
-
- # Local auth
- def find_or_create(
- %Ueberauth.Auth{
- provider: :identity,
- info: %Ueberauth.Auth.Info{email: email},
- credentials: %Ueberauth.Auth.Credentials{other: %{password: password}}
- } = _auth
- ) do
- with {:ok, user} <- Users.fetch_user_by_email(email) do
- Authentication.authenticate(user, password)
- end
- end
-
- # SAML
- def find_or_create(:saml, provider_id, %{"email" => email}) do
- with {:ok, user} <- Users.fetch_user_by_email(email) do
- {:ok, user}
- else
- {:error, :not_found} -> maybe_create_user(:saml_identity_providers, provider_id, email)
- end
- end
-
- # OIDC
- def find_or_create(provider_id, %{"email" => email, "sub" => _sub}) do
- with {:ok, user} <- Users.fetch_user_by_email(email) do
- {:ok, user}
- else
- {:error, :not_found} -> maybe_create_user(:openid_connect_providers, provider_id, email)
- end
- end
-
- defp maybe_create_user(idp_field, provider_id, email) do
- if Auth.auto_create_users?(idp_field, provider_id) do
- Users.create_user(:unprivileged, %{email: email})
- else
- {:error, "user not found and auto_create_users disabled"}
- end
- end
-end
diff --git a/apps/web/lib/web/views/auth_view.ex b/apps/web/lib/web/views/auth_view.ex
deleted file mode 100644
index a1e7a1954..000000000
--- a/apps/web/lib/web/views/auth_view.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Web.AuthView do
- use Web, :view
-end
diff --git a/apps/web/lib/web/views/browser_view.ex b/apps/web/lib/web/views/browser_view.ex
deleted file mode 100644
index 13c042551..000000000
--- a/apps/web/lib/web/views/browser_view.ex
+++ /dev/null
@@ -1,4 +0,0 @@
-defmodule Web.BrowserView do
- use Web, :view
- import Web.Endpoint, only: [static_path: 1]
-end
diff --git a/apps/web/lib/web/views/error_view.ex b/apps/web/lib/web/views/error_view.ex
deleted file mode 100644
index 2766d4bb9..000000000
--- a/apps/web/lib/web/views/error_view.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Web.ErrorView do
- use Web, :view
-
- # If you want to customize a particular status code
- # for a certain format, you may uncomment below.
- # def render("500.html", _assigns) do
- # "Internal Server Error"
- # end
-
- def render("404.json", _assigns) do
- %{"error" => "not_found"}
- end
-
- # By default, Phoenix returns the status message from
- # the template name. For example, "404.html" becomes
- # "Not Found".
- def template_not_found(template, assigns) do
- default_reason = Phoenix.Controller.status_message_from_template(template)
- reason = assigns[:reason] || default_reason
-
- if String.ends_with?(template, ".json") do
- %{"error" => reason}
- else
- reason
- end
- end
-end
diff --git a/apps/web/lib/web/views/json/changeset_view.ex b/apps/web/lib/web/views/json/changeset_view.ex
deleted file mode 100644
index 907188595..000000000
--- a/apps/web/lib/web/views/json/changeset_view.ex
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule Web.JSON.ChangesetView do
- use Web, :view
-
- @doc """
- Traverses and translates changeset errors.
-
- See `Ecto.Changeset.traverse_errors/2` and
- `Web.ErrorHelpers.translate_error/1` for more details.
- """
- def translate_errors(changeset) do
- Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
- end
-
- def render("error.json", %{changeset: changeset}) do
- # When encoded, the changeset returns its errors
- # as a JSON object. So we just pass it forward.
- %{errors: translate_errors(changeset)}
- end
-end
diff --git a/apps/web/lib/web/views/json/configuration_view.ex b/apps/web/lib/web/views/json/configuration_view.ex
deleted file mode 100644
index 72d70802f..000000000
--- a/apps/web/lib/web/views/json/configuration_view.ex
+++ /dev/null
@@ -1,54 +0,0 @@
-defmodule Web.JSON.ConfigurationView do
- @moduledoc """
- Handles JSON rendering of Configuration records.
- """
- use Web, :view
-
- def render("show.json", %{configuration: configuration}) do
- %{data: render_one(configuration, __MODULE__, "configuration.json")}
- end
-
- @keys_to_render ~w[
- id
- local_auth_enabled
- allow_unprivileged_device_management
- allow_unprivileged_device_configuration
- disable_vpn_on_oidc_error
- vpn_session_duration
- default_client_persistent_keepalive
- default_client_mtu
- default_client_endpoint
- default_client_dns
- default_client_allowed_ips
- inserted_at
- updated_at
- ]a
- def render("configuration.json", %{configuration: configuration}) do
- Map.merge(
- Map.take(configuration, @keys_to_render),
- %{
- openid_connect_providers:
- render_many(
- configuration.openid_connect_providers,
- Web.JSON.OpenIDConnectProviderView,
- "openid_connect_provider.json"
- ),
- saml_identity_providers:
- render_many(
- configuration.saml_identity_providers,
- Web.JSON.SAMLIdentityProviderView,
- "saml_identity_provider.json"
- ),
- logo: render("logo.json", %{logo: configuration.logo})
- }
- )
- end
-
- def render("logo.json", %{logo: nil}) do
- %{}
- end
-
- def render("logo.json", %{logo: logo}) do
- Map.take(logo, ~w[url data type]a)
- end
-end
diff --git a/apps/web/lib/web/views/json/device_view.ex b/apps/web/lib/web/views/json/device_view.ex
deleted file mode 100644
index 2b443dd8e..000000000
--- a/apps/web/lib/web/views/json/device_view.ex
+++ /dev/null
@@ -1,56 +0,0 @@
-defmodule Web.JSON.DeviceView do
- @moduledoc """
- Handles JSON rendering of Device records.
- """
- use Web, :view
-
- alias Domain.Devices
-
- def render("index.json", %{devices: devices, defaults: defaults}) do
- %{data: render_many(devices, __MODULE__, "device.json", defaults: defaults)}
- end
-
- def render("show.json", %{device: device, defaults: defaults}) do
- %{data: render_one(device, __MODULE__, "device.json", defaults: defaults)}
- end
-
- @keys_to_render ~w[
- id
- rx_bytes
- tx_bytes
- name
- description
- public_key
- preshared_key
- use_default_allowed_ips
- use_default_dns
- use_default_endpoint
- use_default_mtu
- use_default_persistent_keepalive
- endpoint
- mtu
- persistent_keepalive
- allowed_ips
- dns
- remote_ip
- ipv4
- ipv6
- latest_handshake
- updated_at
- inserted_at
- user_id
- ]a
- def render("device.json", %{device: device, defaults: defaults}) do
- Map.merge(
- Map.take(device, @keys_to_render),
- %{
- server_public_key: Application.get_env(:domain, :wireguard_public_key),
- endpoint: Devices.get_endpoint(device, defaults),
- allowed_ips: Devices.get_allowed_ips(device, defaults),
- dns: Devices.get_dns(device, defaults),
- persistent_keepalive: Devices.get_persistent_keepalive(device, defaults),
- mtu: Devices.get_mtu(device, defaults)
- }
- )
- end
-end
diff --git a/apps/web/lib/web/views/json/openid_connect_provider_view.ex b/apps/web/lib/web/views/json/openid_connect_provider_view.ex
deleted file mode 100644
index 270e4bf1f..000000000
--- a/apps/web/lib/web/views/json/openid_connect_provider_view.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-defmodule Web.JSON.OpenIDConnectProviderView do
- use Web, :view
-
- @keys_to_render ~w[
- id
- label
- scope
- response_type
- client_id
- client_secret
- discovery_document_uri
- redirect_uri
- auto_create_users
- ]a
- def render("openid_connect_provider.json", %{open_id_connect_provider: openid_connect_provider}) do
- Map.take(openid_connect_provider, @keys_to_render)
- end
-end
diff --git a/apps/web/lib/web/views/json/rule_view.ex b/apps/web/lib/web/views/json/rule_view.ex
deleted file mode 100644
index d1d3c1cb0..000000000
--- a/apps/web/lib/web/views/json/rule_view.ex
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule Web.JSON.RuleView do
- @moduledoc """
- Handles JSON rendering of Rule records.
- """
- use Web, :view
-
- def render("index.json", %{rules: rules}) do
- %{data: render_many(rules, __MODULE__, "rule.json")}
- end
-
- def render("show.json", %{rule: rule}) do
- %{data: render_one(rule, __MODULE__, "rule.json")}
- end
-
- @keys_to_render ~w[
- id
- destination
- action
- port_type
- port_range
- user_id
- inserted_at
- updated_at
- ]a
- def render("rule.json", %{rule: rule}) do
- Map.take(rule, @keys_to_render)
- end
-end
diff --git a/apps/web/lib/web/views/json/saml_identity_provider_view.ex b/apps/web/lib/web/views/json/saml_identity_provider_view.ex
deleted file mode 100644
index d13994588..000000000
--- a/apps/web/lib/web/views/json/saml_identity_provider_view.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-defmodule Web.JSON.SAMLIdentityProviderView do
- use Web, :view
-
- @keys_to_render ~w[
- id
- label
- base_url
- metadata
- sign_requests
- sign_metadata
- signed_assertion_in_resp
- signed_envelopes_in_resp
- auto_create_users
- ]a
- def render("saml_identity_provider.json", %{saml_identity_provider: saml_identity_provider}) do
- Map.take(saml_identity_provider, @keys_to_render)
- end
-end
diff --git a/apps/web/lib/web/views/json/user_view.ex b/apps/web/lib/web/views/json/user_view.ex
deleted file mode 100644
index b16e2fb8c..000000000
--- a/apps/web/lib/web/views/json/user_view.ex
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule Web.JSON.UserView do
- @moduledoc """
- Handles JSON rendering of User records.
- """
- use Web, :view
-
- def render("index.json", %{users: users}) do
- %{data: render_many(users, __MODULE__, "user.json")}
- end
-
- def render("show.json", %{user: user}) do
- %{data: render_one(user, __MODULE__, "user.json")}
- end
-
- @keys_to_render ~w[
- id
- role
- email
- last_signed_in_at
- last_signed_in_method
- disabled_at
- inserted_at
- updated_at
- ]a
- def render("user.json", %{user: user}) do
- Map.take(user, @keys_to_render)
- end
-end
diff --git a/apps/web/lib/web/views/layout_view.ex b/apps/web/lib/web/views/layout_view.ex
deleted file mode 100644
index 9fabe3237..000000000
--- a/apps/web/lib/web/views/layout_view.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-defmodule Web.LayoutView do
- use Web, :view
- import Web.Endpoint, only: [static_path: 1]
-
- @doc """
- Generate a random feedback email to avoid spam.
- """
- def feedback_recipient do
- "feedback@firezone.dev"
- end
-
- @doc """
- The application version from mix.exs.
- """
- def application_version do
- Application.spec(:domain, :vsn)
- end
-end
diff --git a/apps/web/lib/web/views/root_view.ex b/apps/web/lib/web/views/root_view.ex
deleted file mode 100644
index 1f368bd36..000000000
--- a/apps/web/lib/web/views/root_view.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Web.RootView do
- use Web, :view
-end
diff --git a/apps/web/lib/web/views/rule_view.ex b/apps/web/lib/web/views/rule_view.ex
deleted file mode 100644
index 8a710b9b9..000000000
--- a/apps/web/lib/web/views/rule_view.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Web.RuleView do
- use Web, :view
-end
diff --git a/apps/web/lib/web/views/shared_view.ex b/apps/web/lib/web/views/shared_view.ex
deleted file mode 100644
index 918a802d8..000000000
--- a/apps/web/lib/web/views/shared_view.ex
+++ /dev/null
@@ -1,23 +0,0 @@
-defmodule Web.SharedView do
- use Web, :view
- import Web.Endpoint, only: [static_path: 1]
-
- @byte_size_opts [
- precision: 2,
- delimiter: ""
- ]
-
- def list_to_string(list, separator \\ ", ") do
- case Enum.join(list, separator) do
- "" -> nil
- binary -> binary
- end
- end
-
- def to_human_bytes(nil), do: to_human_bytes(0)
-
- def to_human_bytes(bytes) when is_integer(bytes) do
- FileSize.from_bytes(bytes, scale: :iec)
- |> FileSize.format(@byte_size_opts)
- end
-end
diff --git a/apps/web/lib/web/views/user_view.ex b/apps/web/lib/web/views/user_view.ex
deleted file mode 100644
index dfd11d92b..000000000
--- a/apps/web/lib/web/views/user_view.ex
+++ /dev/null
@@ -1,6 +0,0 @@
-defmodule Web.UserView do
- @moduledoc """
- Helper functions for User views.
- """
- use Web, :view
-end
diff --git a/apps/web/lib/web/views/wireguard_config_view.ex b/apps/web/lib/web/views/wireguard_config_view.ex
deleted file mode 100644
index 63eef5ada..000000000
--- a/apps/web/lib/web/views/wireguard_config_view.ex
+++ /dev/null
@@ -1,133 +0,0 @@
-defmodule Web.WireguardConfigView do
- use Web, :view
- alias Domain.Config
- alias Domain.Devices
- require Logger
-
- def render("base64_device.conf", %{device: device}) do
- render("device.conf", %{device: device})
- |> Base.encode64()
- end
-
- def render("device.conf", %{device: device}) do
- server_public_key = Application.get_env(:domain, :wireguard_public_key)
- defaults = Devices.defaults()
-
- if is_nil(server_public_key) do
- Logger.error(
- "No server public key found! This will break device config generation. Is fz_vpn alive?"
- )
- end
-
- """
- [Interface]
- PrivateKey = REPLACE_ME
- Address = #{Devices.inet(device)}
- #{mtu_config(device, defaults)}
- #{dns_config(device, defaults)}
-
- [Peer]
- #{psk_config(device)}
- PublicKey = #{server_public_key}
- #{allowed_ips_config(device, defaults)}
- #{endpoint_config(device, defaults)}
- #{persistent_keepalive_config(device, defaults)}
- """
- end
-
- defp psk_config(device) do
- if device.preshared_key do
- "PresharedKey = #{device.preshared_key}"
- else
- ""
- end
- end
-
- defp mtu_config(device, defaults) do
- m = Devices.get_mtu(device, defaults)
-
- if field_empty?(m) do
- ""
- else
- "MTU = #{m}"
- end
- end
-
- defp allowed_ips_config(device, defaults) do
- allowed_ips = Devices.get_allowed_ips(device, defaults)
-
- if field_empty?(allowed_ips) do
- ""
- else
- "AllowedIPs = #{Enum.join(allowed_ips, ",")}"
- end
- end
-
- defp persistent_keepalive_config(device, defaults) do
- pk = Devices.get_persistent_keepalive(device, defaults)
-
- if field_empty?(pk) do
- ""
- else
- "PersistentKeepalive = #{pk}"
- end
- end
-
- defp dns_config(device, defaults) when is_struct(device) do
- dns = Devices.get_dns(device, defaults)
-
- if field_empty?(dns) do
- ""
- else
- "DNS = #{Enum.join(dns, ",")}"
- end
- end
-
- defp endpoint_config(device, defaults) do
- ep = Devices.get_endpoint(device, defaults)
-
- if field_empty?(ep) do
- ""
- else
- "Endpoint = #{maybe_add_port(ep)}"
- end
- end
-
- defp maybe_add_port(%Domain.Types.IPPort{port: nil} = ip_port) do
- wireguard_port = Config.fetch_env!(:domain, :wireguard_port)
- Domain.Types.IPPort.to_string(%{ip_port | port: wireguard_port})
- end
-
- defp maybe_add_port(%Domain.Types.IPPort{} = ip_port) do
- Domain.Types.IPPort.to_string(ip_port)
- end
-
- # Finds a port in IPv6-formatted address, e.g. [2001::1]:51820
- @capture_port ~r/\[.*]:(?[\d]+)/
- defp maybe_add_port(endpoint) do
- wireguard_port = Domain.Config.fetch_env!(:domain, :wireguard_port)
- colon_count = endpoint |> String.graphemes() |> Enum.count(&(&1 == ":"))
-
- if colon_count == 1 or !is_nil(Regex.named_captures(@capture_port, endpoint)) do
- endpoint
- else
- # No port found
- "#{endpoint}:#{wireguard_port}"
- end
- end
-
- defp field_empty?(nil), do: true
- defp field_empty?(0), do: true
- defp field_empty?([]), do: true
-
- defp field_empty?(field) when is_binary(field) do
- len =
- field
- |> String.trim()
- |> String.length()
-
- len == 0
- end
-
- defp field_empty?(_), do: false
-end
diff --git a/apps/web/mix.exs b/apps/web/mix.exs
index e63af4046..686677f94 100644
--- a/apps/web/mix.exs
+++ b/apps/web/mix.exs
@@ -9,38 +9,26 @@ defmodule Web.MixProject do
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
- elixir: "~> 1.12",
+ elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
- test_coverage: [tool: ExCoveralls],
- preferred_cli_env: [
- coveralls: :test,
- "coveralls.detail": :test,
- "coveralls.post": :test,
- "coveralls.html": :test
- ],
aliases: aliases(),
deps: deps()
]
end
def version do
- # Use dummy version for dev and test
System.get_env("VERSION", "0.0.0+git.0.deadbeef")
end
def application do
[
mod: {Web.Application, []},
- extra_applications: [
- :logger,
- :runtime_tools
- ]
+ extra_applications: [:logger, :runtime_tools]
]
end
- # Specifies which paths to compile per environment.
- defp elixirc_paths(:test), do: ["test/support", "lib"]
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp deps do
@@ -49,45 +37,48 @@ defmodule Web.MixProject do
{:domain, in_umbrella: true},
# Phoenix/Plug deps
- {:plug, "~> 1.13"},
- {:plug_cowboy, "~> 2.5"},
{:phoenix, "~> 1.7.0"},
+ {:phoenix_html, "~> 3.3"},
{:phoenix_ecto, "~> 4.4"},
- {:phoenix_html, "~> 3.2"},
- {:phoenix_live_view, "~> 0.18.8"},
- {:phoenix_live_dashboard, "~> 0.7.2"},
- {:phoenix_live_reload, "~> 1.3", only: :dev},
- {:phoenix_swoosh, "~> 1.0"},
- {:gettext, "~> 0.18"},
- {:file_size, "~> 3.0.1"},
-
- # Auth-related deps
- {:guardian, "~> 2.0"},
- {:guardian_db, "~> 2.0"},
- {:openid_connect, github: "firezone/openid_connect", branch: "master"},
- # XXX: All github deps should use ref instead of always updating from master branch
- {:esaml, github: "firezone/esaml", override: true},
- {:samly, github: "firezone/samly"},
- {:ueberauth, "~> 0.7"},
- {:ueberauth_identity, "~> 0.4"},
-
- # Other deps
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_view, "~> 0.18.16"},
+ {:plug_cowboy, "~> 2.5"},
+ {:gettext, "~> 0.20"},
{:remote_ip, "~> 1.0"},
- {:telemetry, "~> 1.0"},
- # Used in Swoosh SMTP adapter
+
+ # Asset pipeline deps
+ {:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
+ {:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
+ {:heroicons, "~> 0.5"},
+
+ # Observability and debugging deps
+ {:telemetry_metrics, "~> 0.6"},
+ {:telemetry_poller, "~> 1.0"},
+ {:recon, "~> 2.5"},
+ {:observer_cli, "~> 1.7"},
+
+ # Mailer deps
+ {:phoenix_swoosh, "~> 1.0"},
{:gen_smtp, "~> 1.0"},
- # Test and dev deps
+ # Other deps
+ {:jason, "~> 1.2"},
+ {:file_size, "~> 3.0.1"},
+
+ # Test deps
+ {:floki, ">= 0.30.0", only: :test},
{:bypass, "~> 2.1", only: :test},
- {:wallaby, "~> 0.30.0", only: :test},
{:bureaucrat, "~> 0.2.9", only: :test},
- {:floki, "~> 0.34.0"}
+ {:wallaby, "~> 0.30.0", only: :test}
]
end
defp aliases do
[
- "assets.build": ["cmd cd assets && yarn install --frozen-lockfile && node esbuild.js prod"],
+ setup: ["deps.get", "assets.setup", "assets.build"],
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
+ "assets.build": ["tailwind default", "esbuild default"],
+ "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"],
"ecto.seed": ["ecto.create", "ecto.migrate", "run ../domain/priv/repo/seeds.exs"],
"ecto.setup": ["ecto.create", "ecto.migrate"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
diff --git a/apps/web/priv/cert/saml_selfsigned.pem b/apps/web/priv/cert/saml_selfsigned.pem
deleted file mode 100644
index 22b9f2aed..000000000
--- a/apps/web/priv/cert/saml_selfsigned.pem
+++ /dev/null
@@ -1,21 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDfTCCAmWgAwIBAgIJAJYYUPJ1xkGhMA0GCSqGSIb3DQEBCwUAMEMxGjAYBgNV
-BAoMEVBob2VuaXggRnJhbWV3b3JrMSUwIwYDVQQDDBxTZWxmLXNpZ25lZCB0ZXN0
-IGNlcnRpZmljYXRlMB4XDTIyMTAwNTAwMDAwMFoXDTIzMTAwNTAwMDAwMFowQzEa
-MBgGA1UECgwRUGhvZW5peCBGcmFtZXdvcmsxJTAjBgNVBAMMHFNlbGYtc2lnbmVk
-IHRlc3QgY2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
-AQC8FSQs7KYacs5lTg0+7/NmbSnwJHZc6W7di9tnzWJPiReBwEVWLj82Bn4mbQIZ
-nQMgbckQUA3V8LLGHC3nBxqy6xqt0h/69OhpvKFWHcakmzv/+eOXj7ruQ42uzaba
-AGXkTWqyRHpqbqYfF45XQEMQau2Fw+9AQZuBtU+Sz98Im5n5DV6S/BTaLNIbszlg
-MEhJYmG19hI/XZ45Dj439M1Hg9D1U1N5vMxcLcnpgBSBLAoBupyq98wme3OU/eAt
-BkmQDzNKESESNSj6fw+8CI9V26TXXrf1ELmJRFLv34ZAj4edLBLoU/z0n/vT3xTd
-r558NZVTG2IvkBKF7BUjrcFFAgMBAAGjdDByMAwGA1UdEwEB/wQCMAAwDgYDVR0P
-AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4E
-FgQU1xmmLeuXQFix8LaBuXlhEGshBJIwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0G
-CSqGSIb3DQEBCwUAA4IBAQCYOy6x0+M4lkl5c4GDBDyQc3CXsyqKBGu92LH2ybaU
-uIX6C6I329nkP7PRvJIewYQhuiAoL/KiaQJItZRkObRkXrrtsnnbZiEvnCvinMK0
-Tueq5/eixQqZFuOkKaLEf8PpJAEqSjiyZy8etWbGd53/OPY0Swwsd9V2+fx48cFL
-0MlaBFQR6qervAO84a2Las0dAHhXOYSQObOokGt/8b5eairi1w/dT6+iHyKM2dUM
-QQfNt3QOTKJUf6Xq31ytLmlnfsGe5rKsBUqJuUH5NBeGu4pOoywG+9U9Bc5GHBMq
-r3Pk9vaGkdEtmZehscgdD2+dYA+FBs0eCzOrzT08h7MW
------END CERTIFICATE-----
diff --git a/apps/web/priv/cert/saml_selfsigned_key.pem b/apps/web/priv/cert/saml_selfsigned_key.pem
deleted file mode 100644
index aa4df94e3..000000000
--- a/apps/web/priv/cert/saml_selfsigned_key.pem
+++ /dev/null
@@ -1,27 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIIEpAIBAAKCAQEAvBUkLOymGnLOZU4NPu/zZm0p8CR2XOlu3YvbZ81iT4kXgcBF
-Vi4/NgZ+Jm0CGZ0DIG3JEFAN1fCyxhwt5wcasusardIf+vToabyhVh3GpJs7//nj
-l4+67kONrs2m2gBl5E1qskR6am6mHxeOV0BDEGrthcPvQEGbgbVPks/fCJuZ+Q1e
-kvwU2izSG7M5YDBISWJhtfYSP12eOQ4+N/TNR4PQ9VNTebzMXC3J6YAUgSwKAbqc
-qvfMJntzlP3gLQZJkA8zShEhEjUo+n8PvAiPVduk11639RC5iURS79+GQI+HnSwS
-6FP89J/7098U3a+efDWVUxtiL5AShewVI63BRQIDAQABAoIBAB+N4HLVBQz849ml
-HZ3IfepaOCX8yArQcvQiSZ4BnBPB6TqwejF6MsqqjjF+KlMHv4WKRahB9gBFkIii
-I6VV0Mnhnak5znm46uEKb3rWJgRpsshAMUm1KGRe2v9Pq0V5uZ5yyoq76FnA1If0
-2MGUm2u+tLizZYk/OIqrU31K+J0lzCaiSxIji9QVD68eIQ9M3+wNC7IZr3LLtwSl
-xuJNDl1aQXyz6fB7mGiasjlYcmZgRgyXfnCQimd3EuF7hDYpGD8Qo8ZyZRCN5KQh
-V8dWowezzmmmjeabcJzDOWgpZihUtEdycM4iRfLxpEvnCoZZVF/urhyLgzsq6FRq
-qybAE4ECgYEA4k0u/X48615SkJ72UdaR73VNczDZ0HN+UX7SdbDgXXb41UNjOuxC
-R7FeQuNT+Eldcx4bssYbHRM7rfVqvWlYd266VveE7nNYE46WjFuUYiZr+zNZwyot
-J3t2Ks3egGlZHWYAvIMlaMwC0LAM3qbxl6rcMhJVXh8QvblRqhd5CqECgYEA1MP0
-4LXIc+3fnc0prhB6HOwWbYD1AwliCjLYiZdPSA98jR+jtjHYp7os2deFLMDWPV4x
-2phyWYg8kvgcp3amkfsgUF1M8sdmnRfDQxNgWpRxZ/6RpPk88zXhMtWLzqRpXiys
-z2Kec//Uw3JZDwrPYQ7K5JAznZofLtEM45NcOCUCgYEAisKa8pKKViQS6lyeWtX/
-y92YbO5iUH/Qz7W85K9dE9JUh6f3W3TsuzsVulvb7B1IMMMgZsE0dOKLMIKQPa4v
-saPynErPdsrBEdTXmR66YGiAw5ncC2B8KX55mYt8SC7Qlscp4m1j7dtSSpX4fjnN
-X5tDw2wcbkcMI9lTKsGT1aECgYEAp+R1oLhxlGF52qjhofRol9gInqJrNNk7nvae
-fnyC2Ec4Lphv9D6DS1+TMtdpxHXq2QQybN9tJI9n1UWqPs9XA8zZo/Dr3oxQwdfV
-gmGQ4AlRMBHm1frDCNxUd2uhZg/BAcpZF1En3jtbplreQgtyt5EXs6LCyDOtNaFK
-/W30EG0CgYAdHjgvmXMsUo52w0E4Qer7eKJiZxxsxW3ucelPW1PYyD3GEWBeZ0IE
-UECGyYjhh1Igb4bPRWD6OIxZfF32swMEiM1wpx6gW5GS2mMTNrpXOChNscBKZrtd
-eccM/L8vdwYw5INerYkKlo4hNhqwsc4rXOQ31mT/nPJ0DT3cL292AA==
------END RSA PRIVATE KEY-----
diff --git a/apps/web/priv/gettext/en/LC_MESSAGES/errors.po b/apps/web/priv/gettext/en/LC_MESSAGES/errors.po
index a589998cf..cdec3a113 100644
--- a/apps/web/priv/gettext/en/LC_MESSAGES/errors.po
+++ b/apps/web/priv/gettext/en/LC_MESSAGES/errors.po
@@ -9,89 +9,3 @@
msgid ""
msgstr ""
"Language: en\n"
-
-## From Ecto.Changeset.cast/4
-msgid "can't be blank"
-msgstr ""
-
-## From Ecto.Changeset.unique_constraint/3
-msgid "has already been taken"
-msgstr ""
-
-## From Ecto.Changeset.put_change/3
-msgid "is invalid"
-msgstr ""
-
-## From Ecto.Changeset.validate_acceptance/3
-msgid "must be accepted"
-msgstr ""
-
-## From Ecto.Changeset.validate_format/3
-msgid "has invalid format"
-msgstr ""
-
-## From Ecto.Changeset.validate_subset/3
-msgid "has an invalid entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_exclusion/3
-msgid "is reserved"
-msgstr ""
-
-## From Ecto.Changeset.validate_confirmation/3
-msgid "does not match confirmation"
-msgstr ""
-
-## From Ecto.Changeset.no_assoc_constraint/3
-msgid "is still associated with this entry"
-msgstr ""
-
-msgid "are still associated with this entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_length/3
-msgid "should be %{count} character(s)"
-msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have %{count} item(s)"
-msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} character(s)"
-msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at least %{count} item(s)"
-msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} character(s)"
-msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at most %{count} item(s)"
-msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-## From Ecto.Changeset.validate_number/3
-msgid "must be less than %{number}"
-msgstr ""
-
-msgid "must be greater than %{number}"
-msgstr ""
-
-msgid "must be less than or equal to %{number}"
-msgstr ""
-
-msgid "must be greater than or equal to %{number}"
-msgstr ""
-
-msgid "must be equal to %{number}"
-msgstr ""
diff --git a/apps/web/priv/gettext/errors.pot b/apps/web/priv/gettext/errors.pot
index 39a220be3..d6f47fa87 100644
--- a/apps/web/priv/gettext/errors.pot
+++ b/apps/web/priv/gettext/errors.pot
@@ -8,88 +8,3 @@
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.
-## From Ecto.Changeset.cast/4
-msgid "can't be blank"
-msgstr ""
-
-## From Ecto.Changeset.unique_constraint/3
-msgid "has already been taken"
-msgstr ""
-
-## From Ecto.Changeset.put_change/3
-msgid "is invalid"
-msgstr ""
-
-## From Ecto.Changeset.validate_acceptance/3
-msgid "must be accepted"
-msgstr ""
-
-## From Ecto.Changeset.validate_format/3
-msgid "has invalid format"
-msgstr ""
-
-## From Ecto.Changeset.validate_subset/3
-msgid "has an invalid entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_exclusion/3
-msgid "is reserved"
-msgstr ""
-
-## From Ecto.Changeset.validate_confirmation/3
-msgid "does not match confirmation"
-msgstr ""
-
-## From Ecto.Changeset.no_assoc_constraint/3
-msgid "is still associated with this entry"
-msgstr ""
-
-msgid "are still associated with this entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_length/3
-msgid "should be %{count} character(s)"
-msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have %{count} item(s)"
-msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} character(s)"
-msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at least %{count} item(s)"
-msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} character(s)"
-msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at most %{count} item(s)"
-msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-## From Ecto.Changeset.validate_number/3
-msgid "must be less than %{number}"
-msgstr ""
-
-msgid "must be greater than %{number}"
-msgstr ""
-
-msgid "must be less than or equal to %{number}"
-msgstr ""
-
-msgid "must be greater than or equal to %{number}"
-msgstr ""
-
-msgid "must be equal to %{number}"
-msgstr ""
diff --git a/apps/web/test/support/acceptance_case.ex b/apps/web/test/support/acceptance_case.ex
index 420d6888f..5ec5848c6 100644
--- a/apps/web/test/support/acceptance_case.ex
+++ b/apps/web/test/support/acceptance_case.ex
@@ -1,5 +1,6 @@
defmodule Web.AcceptanceCase do
use ExUnit.CaseTemplate
+ use Domain.CaseTemplate
alias Wallaby.Query
import Wallaby.Browser
diff --git a/apps/web/test/support/acceptance_case/auth.ex b/apps/web/test/support/acceptance_case/auth.ex
index a8fc2f9af..2a26a27da 100644
--- a/apps/web/test/support/acceptance_case/auth.ex
+++ b/apps/web/test/support/acceptance_case/auth.ex
@@ -32,66 +32,70 @@ defmodule Web.AcceptanceCase.Auth do
end
end
- def authenticate(session, %Domain.Users.User{} = user) do
- subject = Domain.Auth.fetch_subject!(user, "127.0.0.1", "AcceptanceCase")
- authenticate(session, subject)
- end
+ # def authenticate(session, %Domain.Users.User{} = user) do
+ # subject = Domain.Auth.fetch_subject!(user, "127.0.0.1", "AcceptanceCase")
+ # authenticate(session, subject)
+ # end
- def authenticate(session, %Domain.Auth.Subject{} = subject) do
- options = Web.Session.options()
+ # def authenticate(session, %Domain.Auth.Subject{} = subject) do
+ # options = Web.Session.options()
- key = Keyword.fetch!(options, :key)
- encryption_salt = Keyword.fetch!(options, :encryption_salt)
- signing_salt = Keyword.fetch!(options, :signing_salt)
- secret_key_base = Web.Endpoint.config(:secret_key_base)
+ # key = Keyword.fetch!(options, :key)
+ # encryption_salt = Keyword.fetch!(options, :encryption_salt)
+ # signing_salt = Keyword.fetch!(options, :signing_salt)
+ # secret_key_base = Web.Endpoint.config(:secret_key_base)
- with {:ok, token, _claims} <- Web.Auth.HTML.Authentication.encode_and_sign(subject) do
- encryption_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, encryption_salt, [])
- signing_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt, [])
+ # with {:ok, token, _claims} <- Web.Auth.HTML.Authentication.encode_and_sign(subject) do
+ # encryption_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, encryption_salt, [])
+ # signing_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt, [])
- cookie =
- %{
- "guardian_default_token" => token,
- "login_method" => "identity",
- "logged_in_at" => DateTime.utc_now()
- }
- |> :erlang.term_to_binary()
+ # cookie =
+ # %{
+ # "guardian_default_token" => token,
+ # "login_method" => "identity",
+ # "logged_in_at" => DateTime.utc_now()
+ # }
+ # |> :erlang.term_to_binary()
- encrypted =
- Plug.Crypto.MessageEncryptor.encrypt(
- cookie,
- encryption_key,
- signing_key
- )
+ # encrypted =
+ # Plug.Crypto.MessageEncryptor.encrypt(
+ # cookie,
+ # encryption_key,
+ # signing_key
+ # )
- Wallaby.Browser.set_cookie(session, key, encrypted)
- end
- end
+ # Wallaby.Browser.set_cookie(session, key, encrypted)
+ # end
+ # end
- def assert_unauthenticated(session) do
- with {:ok, cookie} <- fetch_session_cookie(session) do
- if token = cookie["guardian_default_token"] do
- {:ok, claims} = Web.Auth.HTML.Authentication.decode_and_verify(token)
- flunk("User is authenticated, claims: #{inspect(claims)}")
- else
- session
- end
- else
- :error -> session
- end
- end
+ # TODO
+ # def assert_unauthenticated(session) do
+ # with {:ok, cookie} <- fetch_session_cookie(session) do
+ # if token = cookie["guardian_default_token"] do
+ # # TODO
+ # # {:ok, claims} = Web.Auth.HTML.Authentication.decode_and_verify(token)
+ # # flunk("User is authenticated, claims: #{inspect(claims)}")
+ # :ok
+ # else
+ # session
+ # end
+ # else
+ # :error -> session
+ # end
+ # end
- def assert_authenticated(session, user) do
- with {:ok, cookie} <- fetch_session_cookie(session),
- {:ok, claims} <-
- Web.Auth.HTML.Authentication.decode_and_verify(cookie["guardian_default_token"]),
- {:ok, subject} <-
- Web.Auth.HTML.Authentication.resource_from_claims(claims) do
- assert elem(subject.actor, 1).id == user.id
- session
- else
- :error -> flunk("No session cookie found")
- other -> flunk("User is not authenticated: #{inspect(other)}")
- end
- end
+ # def assert_authenticated(session, user) do
+ # with {:ok, cookie} <- fetch_session_cookie(session) do
+ # # TODO
+ # # {:ok, claims} <-
+ # # Web.Auth.HTML.Authentication.decode_and_verify(cookie["guardian_default_token"]),
+ # # {:ok, subject} <-
+ # # Web.Auth.HTML.Authentication.resource_from_claims(claims) do
+ # # assert elem(subject.actor, 1).id == user.id
+ # session
+ # else
+ # :error -> flunk("No session cookie found")
+ # other -> flunk("User is not authenticated: #{inspect(other)}")
+ # end
+ # end
end
diff --git a/apps/web/test/support/api_case.ex b/apps/web/test/support/api_case.ex
deleted file mode 100644
index 97f2f00a9..000000000
--- a/apps/web/test/support/api_case.ex
+++ /dev/null
@@ -1,63 +0,0 @@
-defmodule Web.ApiCase do
- @moduledoc """
- This module defines the test case to be used by
- tests that require setting up a connection.
-
- Such tests rely on `Phoenix.ConnTest` and also
- import other functionality to make it easier
- to build common data structures and query the data layer.
-
- Finally, if the test case interacts with the database,
- we enable the SQL sandbox, so changes done to the database
- are reverted at the end of every test. If you are using
- PostgreSQL, you can even run database tests asynchronously
- by setting `use Web.ConnCase, async: true`, although
- this option is not recommended for other databases.
- """
- use ExUnit.CaseTemplate
- use Domain.CaseTemplate
-
- alias Domain.{
- ApiTokensFixtures,
- UsersFixtures
- }
-
- using do
- quote do
- use Web, :verified_routes
- # Import conveniences for testing with connections
- import Plug.Conn
- import Phoenix.ConnTest
- import Web.ApiCase
- import Domain.TestHelpers
- import Bureaucrat.Helpers
- import Web.ApiCase
- alias Domain.Repo
-
- # The default endpoint for testing
- @endpoint Web.Endpoint
- end
- end
-
- def new_conn do
- Phoenix.ConnTest.build_conn()
- end
-
- def api_conn do
- new_conn()
- |> Plug.Conn.put_req_header("accept", "application/json")
- end
-
- def unauthed_conn, do: api_conn()
-
- def authed_conn do
- user = UsersFixtures.create_user_with_role(:admin)
- api_token = ApiTokensFixtures.create_api_token(user: user)
-
- {:ok, token, _claims} = Web.Auth.JSON.Authentication.fz_encode_and_sign(api_token)
-
- api_conn()
- |> Plug.Conn.put_req_header("authorization", "bearer #{token}")
- |> Web.Auth.JSON.Pipeline.call([])
- end
-end
diff --git a/apps/web/test/support/channel_case.ex b/apps/web/test/support/channel_case.ex
deleted file mode 100644
index 0c8e40e35..000000000
--- a/apps/web/test/support/channel_case.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-defmodule Web.ChannelCase do
- @moduledoc """
- This module defines the test case to be used by
- channel tests.
-
- Such tests rely on `Phoenix.ChannelTest` and also
- import other functionality to make it easier
- to build common data structures and query the data layer.
-
- Finally, if the test case interacts with the database,
- we enable the SQL sandbox, so changes done to the database
- are reverted at the end of every test. If you are using
- PostgreSQL, you can even run database tests asynchronously
- by setting `use Web.ChannelCase, async: true`, although
- this option is not recommended for other databases.
- """
-
- use ExUnit.CaseTemplate
- use Domain.CaseTemplate
-
- using do
- quote do
- # Import conveniences for testing with channels
- import Phoenix.ChannelTest
- import Domain.TestHelpers
-
- # The default endpoint for testing
- @endpoint Web.Endpoint
- end
- end
-end
diff --git a/apps/web/test/support/conn_case.ex b/apps/web/test/support/conn_case.ex
index 6f53fb454..08ab641e1 100644
--- a/apps/web/test/support/conn_case.ex
+++ b/apps/web/test/support/conn_case.ex
@@ -1,98 +1,25 @@
defmodule Web.ConnCase do
- @moduledoc """
- This module defines the test case to be used by
- tests that require setting up a connection.
-
- Such tests rely on `Phoenix.ConnTest` and also
- import other functionality to make it easier
- to build common data structures and query the data layer.
-
- Finally, if the test case interacts with the database,
- we enable the SQL sandbox, so changes done to the database
- are reverted at the end of every test. If you are using
- PostgreSQL, you can even run database tests asynchronously
- by setting `use Web.ConnCase, async: true`, although
- this option is not recommended for other databases.
- """
use ExUnit.CaseTemplate
use Domain.CaseTemplate
- alias Domain.UsersFixtures
- alias Web.Auth.HTML.Authentication
-
using do
quote do
- # Import conveniences for testing with connections
- alias Domain.Repo
- import Plug.Conn
- import Phoenix.ConnTest
- import Phoenix.LiveViewTest
- import Domain.TestHelpers
- import Web.ConnCase
-
# The default endpoint for testing
@endpoint Web.Endpoint
use Web, :verified_routes
- def current_user(test_conn) do
- %{actor: {:user, user}} =
- test_conn
- |> get_session()
- |> Authentication.get_current_subject()
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import Phoenix.LiveViewTest
+ import Web.ConnCase
- user
- end
+ alias Domain.Repo
end
end
- def new_conn do
- Phoenix.ConnTest.build_conn()
- end
-
- def admin_conn(tags) do
- authed_conn(:admin, tags)
- end
-
- def unprivileged_conn(tags) do
- authed_conn(:unprivileged, tags)
- end
-
- defp authed_conn(role, tags) do
- user = UsersFixtures.create_user_with_role(role)
-
- conn =
- new_conn()
- |> Plug.Test.init_test_session(%{})
- |> Authentication.sign_in(user, %{provider: :identity})
- |> maybe_put_session(tags)
-
- {user,
- conn
- |> Plug.Conn.put_session("guardian_default_token", conn.private.guardian_default_token)}
- end
-
- defp maybe_put_session(conn, %{session: session}) do
- conn
- |> Plug.Test.init_test_session(session)
- end
-
- defp maybe_put_session(conn, _tags) do
- conn
- end
-
- setup tags do
- {unprivileged_user, unprivileged_conn} = unprivileged_conn(tags)
- {admin_user, admin_conn} = admin_conn(tags)
-
- conns = [
- unauthed_conn: new_conn(),
- admin_user: admin_user,
- unprivileged_user: unprivileged_user,
- admin_conn: admin_conn,
- unprivileged_conn: unprivileged_conn
- ]
-
- {:ok, conns}
+ setup _tags do
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
diff --git a/apps/web/test/support/docs_generator.ex b/apps/web/test/support/documentation/docs_generator.ex
similarity index 99%
rename from apps/web/test/support/docs_generator.ex
rename to apps/web/test/support/documentation/docs_generator.ex
index 07a08aba5..0b2209173 100644
--- a/apps/web/test/support/docs_generator.ex
+++ b/apps/web/test/support/documentation/docs_generator.ex
@@ -1,4 +1,4 @@
-defmodule DocsGenerator do
+defmodule Web.Documentation.Generator do
alias Domain.Config.Definition
@keep_req_headers ["authorization"]
diff --git a/apps/web/test/support/mailer_case.ex b/apps/web/test/support/mailer_case.ex
deleted file mode 100644
index 0d356a247..000000000
--- a/apps/web/test/support/mailer_case.ex
+++ /dev/null
@@ -1,21 +0,0 @@
-defmodule Web.MailerCase do
- @moduledoc """
- A case template for Mailers.
- """
- use ExUnit.CaseTemplate
- use Domain.CaseTemplate
-
- using do
- quote do
- alias Domain.Repo
-
- import Ecto
- import Ecto.Changeset
- import Ecto.Query
- import Domain.DataCase
- import Domain.TestHelpers
-
- use Web, :verified_routes
- end
- end
-end
diff --git a/apps/web/test/support/mailer_test_adapter.ex b/apps/web/test/support/mailer_test_adapter.ex
deleted file mode 100644
index 8ca8237fa..000000000
--- a/apps/web/test/support/mailer_test_adapter.ex
+++ /dev/null
@@ -1,15 +0,0 @@
-defmodule Web.MailerTestAdapter do
- use Swoosh.Adapter
-
- @impl true
- def deliver(email, config) do
- Swoosh.Adapters.Local.deliver(email, config)
- Swoosh.Adapters.Test.deliver(email, config)
- end
-
- @impl true
- def deliver_many(emails, config) do
- Swoosh.Adapters.Local.deliver_many(emails, config)
- Swoosh.Adapters.Test.deliver_many(emails, config)
- end
-end
diff --git a/apps/web/test/support/test_helpers.ex b/apps/web/test/support/test_helpers.ex
deleted file mode 100644
index 21ad4ce76..000000000
--- a/apps/web/test/support/test_helpers.ex
+++ /dev/null
@@ -1,175 +0,0 @@
-# Removeme
-defmodule Web.TestHelpers do
- @moduledoc """
- Test setup helpers
- """
-
- alias Domain.{
- ConnectivityChecksFixtures,
- DevicesFixtures,
- NotificationsFixtures,
- Repo,
- RulesFixtures,
- Users.User,
- UsersFixtures
- }
-
- def clear_users do
- Repo.delete_all(User)
- end
-
- def create_unprivileged_device(%{unprivileged_user: user}) do
- {:ok, device: DevicesFixtures.create_device(user: user)}
- end
-
- def create_device(tags) do
- device =
- if tags[:unauthed] || is_nil(tags[:user_id]) do
- DevicesFixtures.create_device()
- else
- DevicesFixtures.create_device(%{user_id: tags[:user_id]})
- end
-
- {:ok, device: device}
- end
-
- def create_other_user_device(_) do
- user = UsersFixtures.create_user_with_role(:unprivileged, %{email: "other_user@test"})
-
- device =
- DevicesFixtures.create_device(%{
- user: user,
- name: "other device"
- })
-
- {:ok, other_device: device}
- end
-
- def create_connectivity_checks(_tags) do
- connectivity_checks =
- Enum.map(1..5, fn _i ->
- ConnectivityChecksFixtures.create_connectivity_check()
- end)
-
- {:ok, connectivity_checks: connectivity_checks}
- end
-
- def create_devices(tags) do
- user =
- if tags[:unathed] || is_nil(tags[:user_id]) do
- UsersFixtures.create_user_with_role(:admin)
- else
- Repo.get!(User, tags[:user_id])
- end
-
- devices =
- Enum.map(1..5, fn num ->
- DevicesFixtures.create_device(%{
- name: "device #{num}",
- user: user
- })
- end)
-
- {:ok, devices: devices}
- end
-
- def create_user(tags) do
- role = tags[:role] || :admin
- user = UsersFixtures.create_user_with_role(role)
-
- {:ok, user: user}
- end
-
- def create_accept_rule(_) do
- rule = RulesFixtures.create_rule(%{action: :accept})
- {:ok, rule: rule}
- end
-
- def create_drop_rule(_) do
- rule = RulesFixtures.create_rule(%{action: :drop})
- {:ok, rule: rule}
- end
-
- def create_rule(_) do
- rule = RulesFixtures.create_rule(%{})
- {:ok, rule: rule}
- end
-
- def create_rule_accept(_) do
- rule = RulesFixtures.create_rule(%{action: :accept})
- {:ok, rule: rule}
- end
-
- def create_rule_with_user_and_device(_) do
- user = UsersFixtures.create_user_with_role(:admin)
- rule = RulesFixtures.create_rule(user_id: user.id, destination: "10.20.30.0/24")
-
- device =
- DevicesFixtures.create_device(
- user: user,
- name: "device"
- )
-
- {:ok, rule: rule, user: user, device: device}
- end
-
- def create_rule_with_user(opts \\ %{}) do
- user = UsersFixtures.create_user_with_role(:admin)
- rule = RulesFixtures.create_rule(Map.merge(%{user_id: user.id}, opts))
-
- {:ok, rule: rule, user: user}
- end
-
- def create_rule_with_ports(opts \\ %{}) do
- rule = RulesFixtures.create_rule(Map.merge(%{port_range: "10 - 20", port_type: :udp}, opts))
-
- {:ok, rule: rule}
- end
-
- def create_user_with_valid_sign_in_token(_) do
- {:ok, user: %User{}} = UsersFixtures.create_user_with_role(:admin)
- end
-
- def create_user_with_expired_sign_in_token(_) do
- expired_at = DateTime.add(DateTime.utc_now(), -1 * 86_401)
-
- {:ok,
- user:
- UsersFixtures.create_user_with_role(:admin, %{
- sign_in_token: "EXPIRED_TOKEN",
- sign_in_token_created_at: expired_at
- })}
- end
-
- def create_users(tags) do
- count = tags[:count] || 5
- role = tags[:role] || :admin
-
- users =
- Enum.map(1..count, fn i ->
- UsersFixtures.create_user_with_role(role, %{email: "userlist#{i}@test"})
- end)
-
- {:ok, users: users}
- end
-
- def clear_users(_) do
- {count, _result} = Repo.delete_all(User)
- {:ok, count: count}
- end
-
- def create_notifications(opts \\ []) do
- count = opts[:count] || 5
-
- notifications =
- for i <- 1..count do
- NotificationsFixtures.notification_fixture(user: "test#{i}@localhost")
- end
-
- {:ok, notifications: notifications}
- end
-
- def create_notification(attrs \\ []) do
- {:ok, notification: NotificationsFixtures.notification_fixture(attrs)}
- end
-end
diff --git a/apps/web/test/test_helper.exs b/apps/web/test/test_helper.exs
index 459141307..7c3ecf7a2 100644
--- a/apps/web/test/test_helper.exs
+++ b/apps/web/test/test_helper.exs
@@ -2,7 +2,7 @@
Path.join(File.cwd!(), "screenshots") |> File.rm_rf!()
Bureaucrat.start(
- writer: DocsGenerator,
+ writer: Web.Documentation.Generator,
default_path: "../../www/docs/reference/rest-api"
)
diff --git a/apps/web/test/web/acceptance/admin_test.exs b/apps/web/test/web/acceptance/admin_test.exs
deleted file mode 100644
index 6d73646e4..000000000
--- a/apps/web/test/web/acceptance/admin_test.exs
+++ /dev/null
@@ -1,702 +0,0 @@
-defmodule Web.Acceptance.AdminTest do
- use Web.AcceptanceCase, async: true
- alias Domain.UsersFixtures
- alias Domain.DevicesFixtures
-
- setup tags do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session =
- tags.session
- |> visit(~p"/")
- |> Auth.authenticate(user)
-
- tags
- |> Map.put(:session, session)
- |> Map.put(:user, user)
- end
-
- describe "user management" do
- feature "create new unprivileged users without password", %{session: session, user: user} do
- attrs = UsersFixtures.user_attrs()
-
- session
- |> visit(~p"/users/new")
- |> assert_el(Query.text("Add User", minimum: 1))
- |> fill_in(Query.fillable_field("user[email]"), with: "xxx")
- |> click(Query.button("Save"))
- |> assert_el(Query.text("is invalid email address"))
- |> fill_in(Query.fillable_field("user[email]"), with: user.email)
- |> click(Query.button("Save"))
- |> assert_el(Query.text("has already been taken"))
- |> fill_in(Query.fillable_field("user[email]"), with: attrs.email)
- |> click(Query.button("Save"))
- |> assert_el(Query.text("User created successfully."))
- |> assert_el(Query.text(attrs.email, minimum: 1))
-
- assert Repo.get_by(Domain.Users.User, email: attrs.email)
- end
-
- feature "create new unprivileged users with password auth", %{session: session, user: user} do
- attrs = UsersFixtures.user_attrs()
-
- session
- |> visit(~p"/users/new")
- |> assert_el(Query.text("Add User", minimum: 1))
- |> fill_form(%{
- "user[email]" => "xxx",
- "user[password]" => "yyy",
- "user[password_confirmation]" => "zzz"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("is invalid email address"))
- |> assert_el(Query.text("should be at least 12 character(s)"))
- |> assert_el(Query.text("does not match confirmation"))
- |> fill_form(%{
- "user[email]" => user.email,
- "user[password]" => "firezone1234",
- "user[password_confirmation]" => "firezone1234"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("has already been taken"))
- # XXX: for some reason form rests when email has already been taken
- |> fill_form(%{
- "user[email]" => attrs.email,
- "user[password]" => attrs.password,
- "user[password_confirmation]" => attrs.password_confirmation
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("User created successfully."))
- |> assert_el(Query.text("unprivileged", minimum: 1))
- |> assert_el(Query.text(attrs.email, minimum: 1))
-
- assert user = Repo.get_by(Domain.Users.User, email: attrs.email)
- assert user.role == :unprivileged
- assert Domain.Crypto.equal?(attrs.password, user.password_hash)
- end
-
- feature "change user email and password", %{session: session} do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session
- |> visit(~p"/users/#{user.id}")
- |> assert_el(Query.link("Change Email or Password"))
- |> click(Query.link("Change Email or Password"))
- |> assert_el(Query.text("Change user email or enter new password below."))
- |> fill_form(%{
- "user[email]" => "foo",
- "user[password]" => "123",
- "user[password_confirmation]" => "1234"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("is invalid email address"))
- |> assert_el(Query.text("should be at least 12 character(s)"))
- |> assert_el(Query.text("does not match confirmation"))
- |> fill_form(%{
- "user[email]" => "foo@xample.com",
- "user[password]" => "mynewpassword",
- "user[password_confirmation]" => "mynewpassword"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("User updated successfully."))
-
- assert updated_user = Repo.get(Domain.Users.User, user.id)
- assert updated_user.password_hash != user.password_hash
- assert updated_user.email == "foo@xample.com"
- end
-
- feature "promote and demote users", %{session: session} do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session =
- session
- |> visit(~p"/users/#{user.id}")
- |> assert_el(Query.link("Change Email or Password"))
-
- accept_confirm(session, fn session ->
- session
- |> click(Query.button("demote"))
- |> assert_el(Query.text("User updated successfully."))
- end)
-
- assert updated_user = Repo.get(Domain.Users.User, user.id)
- assert updated_user.role == :unprivileged
-
- accept_confirm(session, fn session ->
- session
- |> click(Query.button("promote"))
- |> assert_el(Query.text("User updated successfully."))
- end)
-
- assert updated_user = Repo.get(Domain.Users.User, user.id)
- assert updated_user.role == :admin
- end
-
- feature "disable and enable user VPN connection", %{session: session} do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session =
- session
- |> visit(~p"/users/#{user.id}")
- |> assert_el(Query.link("Change Email or Password"))
-
- accept_confirm(session, fn session ->
- session
- |> toggle("toggle_disabled_at")
- end)
-
- wait_for(fn ->
- assert updated_user = Repo.get(Domain.Users.User, user.id)
- refute is_nil(updated_user.disabled_at)
- end)
-
- accept_confirm(session, fn session ->
- session
- |> toggle("toggle_disabled_at")
- end)
-
- wait_for(fn ->
- assert updated_user = Repo.get(Domain.Users.User, user.id)
- assert is_nil(updated_user.disabled_at)
- end)
- end
-
- feature "delete user", %{session: session, user: user} do
- unprivileged_user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session =
- session
- |> visit(~p"/users/#{user.id}")
- |> assert_el(Query.button("Delete User"))
-
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete User"))
- end)
-
- assert_el(session, Query.text("Use the account section to delete your account."))
-
- assert Repo.get(Domain.Users.User, user.id)
-
- session
- |> visit(~p"/users/#{unprivileged_user.id}")
- |> assert_el(Query.button("Delete User"))
-
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete User"))
- end)
-
- assert_el(session, Query.text("User deleted successfully."))
-
- refute Repo.get(Domain.Users.User, unprivileged_user.id)
- end
- end
-
- describe "device management" do
- feature "can add devices for users", %{session: session} do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session
- |> visit(~p"/users/#{user.id}")
- |> assert_el(Query.text("No devices."))
- |> assert_el(Query.link("Add Device"))
- |> click(Query.link("Add Device"))
- |> assert_el(Query.button("Generate Configuration"))
- |> set_value(Query.css("#create-device_use_default_allowed_ips_false"), :selected)
- |> set_value(Query.css("#create-device_use_default_dns_false"), :selected)
- |> set_value(Query.css("#create-device_use_default_endpoint_false"), :selected)
- |> set_value(Query.css("#create-device_use_default_mtu_false"), :selected)
- |> set_value(
- Query.css("#create-device_use_default_persistent_keepalive_false"),
- :selected
- )
- |> fill_form(%{
- "device[allowed_ips]" => "127.0.0.1",
- "device[name]" => "big-leg-007",
- "device[description]" => "Dummy description",
- "device[dns]" => "1.1.1.1,2.2.2.2",
- "device[endpoint]" => "example.com:51820",
- "device[mtu]" => "1400",
- "device[persistent_keepalive]" => "10",
- "device[ipv4]" => "100.64.255.110",
- "device[ipv6]" => "fd00::1e:3f96"
- })
- |> click(Query.button("Generate Configuration"))
- |> assert_el(Query.text("Device added!"))
- |> click(Query.css("#download-config"))
- |> click(Query.css("button[phx-click=\"close\"]"))
- |> assert_el(Query.link("Add Device"))
- |> assert_el(Query.link("big-leg-007"))
- |> assert_path(~p"/users/#{user.id}")
-
- assert device = Repo.one(Domain.Devices.Device)
- assert device.name == "big-leg-007"
- assert device.description == "Dummy description"
- assert device.allowed_ips == [%Postgrex.INET{address: {127, 0, 0, 1}, netmask: nil}]
- assert device.dns == ["1.1.1.1", "2.2.2.2"]
- assert device.endpoint == "example.com:51820"
- assert device.mtu == 1400
- assert device.persistent_keepalive == 10
- assert device.ipv4 == %Postgrex.INET{address: {100, 64, 255, 110}}
- assert device.ipv6 == %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 0, 30, 16_278}}
- end
-
- feature "can see devices, their details and delete them", %{session: session} do
- device1 = DevicesFixtures.create_device()
- device2 = DevicesFixtures.create_device()
-
- session =
- session
- |> visit(~p"/devices")
- |> assert_el(Query.text("All Devices"))
- |> assert_el(Query.link(device1.name))
- |> click(Query.link(device2.name))
- |> assert_el(Query.text("Danger Zone"))
- |> assert_el(Query.text(device2.public_key))
-
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete Device #{device2.name}"))
- end)
-
- assert_el(session, Query.text("All Devices"))
-
- assert Repo.aggregate(Domain.Devices.Device, :count) == 1
- end
- end
-
- describe "rules" do
- feature "manage allow rules", %{session: session, user: user} do
- session =
- session
- |> visit(~p"/rules")
- |> assert_has(Query.text("Egress Rules"))
- |> find(Query.css("#accept-form"), fn parent ->
- parent
- |> set_value(Query.select("rule[port_type]"), "tcp")
- |> set_value(Query.select("rule[user_id]"), user.email)
- |> fill_form(%{
- "rule[destination]" => "8.8.4.4",
- "rule[port_range]" => "1-8000"
- })
- |> click(Query.button("Add"))
- end)
- |> assert_has(Query.text("8.8.4.4"))
- |> assert_has(Query.link("Delete"))
-
- assert rule = Repo.one(Domain.Rules.Rule)
- assert rule.destination == %Postgrex.INET{address: {8, 8, 4, 4}}
- assert rule.port_range == "1 - 8000"
- assert rule.port_type == :tcp
-
- click(session, Query.link("Delete"))
-
- # XXX: We need to show a confirmation dialog on delete,
- # and message once record was saved or deleted.
- wait_for(fn ->
- assert is_nil(Repo.one(Domain.Rules.Rule))
- end)
- end
- end
-
- describe "settings" do
- feature "change default settings", %{session: session} do
- session
- |> visit(~p"/settings/client_defaults")
- |> assert_el(Query.text("Client Defaults", count: 2))
- |> fill_form(%{
- "configuration[default_client_allowed_ips]" => "192.0.0.0/0,::/0",
- "configuration[default_client_dns]" => "1.1.1.1,2.2.2.2",
- "configuration[default_client_endpoint]" => "example.com:8123",
- "configuration[default_client_persistent_keepalive]" => "10",
- "configuration[default_client_mtu]" => "1234"
- })
- |> click(Query.button("Save"))
- # XXX: We need to show a flash that settings are saved
- |> visit(~p"/settings/client_defaults")
- |> assert_el(Query.text("Client Defaults", count: 2))
-
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.default_client_persistent_keepalive == 10
- assert configuration.default_client_mtu == 1234
- assert configuration.default_client_endpoint == "example.com:8123"
- assert configuration.default_client_dns == ["1.1.1.1", "2.2.2.2"]
-
- assert configuration.default_client_allowed_ips == [
- %Postgrex.INET{address: {192, 0, 0, 0}, netmask: 0},
- %Postgrex.INET{address: {0, 0, 0, 0, 0, 0, 0, 0}, netmask: 0}
- ]
- end
-
- feature "can use IP with a port in default client endpoint and host in DNS", %{
- session: session
- } do
- session
- |> visit(~p"/settings/client_defaults")
- |> assert_el(Query.text("Client Defaults", count: 2))
- |> fill_in(Query.fillable_field("configuration[default_client_dns]"),
- with: "dns.example.com"
- )
- |> fill_in(Query.fillable_field("configuration[default_client_endpoint]"),
- with: "1.2.3.4:8123"
- )
- |> click(Query.button("Save"))
- # XXX: We need to show a flash that settings are saved
- |> visit(~p"/settings/client_defaults")
- |> assert_el(Query.text("Client Defaults", count: 2))
-
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.default_client_endpoint == "1.2.3.4:8123"
- assert configuration.default_client_dns == ["dns.example.com"]
- end
- end
-
- describe "customization" do
- feature "allows to change logo using a URL", %{session: session} do
- session
- |> visit(~p"/settings/customization")
- |> assert_el(Query.text("Customization", count: 2))
- |> set_value(Query.css("input[value=\"URL\"]"), :selected)
- |> fill_in(Query.fillable_field("url"), with: "https://http.cat/200")
- |> click(Query.button("Save"))
- |> assert_el(Query.css("img[src=\"https://http.cat/200\"]"))
-
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.logo.url == "https://http.cat/200"
- end
- end
-
- describe "security" do
- feature "change security settings", %{
- session: session
- } do
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.local_auth_enabled == true
- assert configuration.allow_unprivileged_device_management == true
- assert configuration.allow_unprivileged_device_configuration == true
- assert configuration.disable_vpn_on_oidc_error == false
-
- session
- |> visit(~p"/settings/security")
- |> assert_el(Query.text("Security Settings"))
- |> toggle("local_auth_enabled")
- |> toggle("allow_unprivileged_device_management")
- |> toggle("allow_unprivileged_device_configuration")
- |> toggle("disable_vpn_on_oidc_error")
- |> assert_el(Query.text("Security Settings"))
-
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.local_auth_enabled == false
- assert configuration.allow_unprivileged_device_management == false
- assert configuration.allow_unprivileged_device_configuration == false
- assert configuration.disable_vpn_on_oidc_error == true
- end
-
- feature "change required authentication timeout", %{session: session} do
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.vpn_session_duration == 0
-
- session
- |> visit(~p"/settings/security")
- |> assert_el(Query.text("Security Settings"))
- |> find(Query.select("configuration[vpn_session_duration]"), fn select ->
- click(select, Query.option("Every Week"))
- end)
- |> click(Query.css("[type=\"submit\""))
- |> assert_el(Query.text("Security Settings"))
-
- # XXX: We need to show a flash that settings are saved
- wait_for(fn ->
- assert configuration = Domain.Config.fetch_db_config!()
- assert configuration.vpn_session_duration == 604_800
- end)
- end
-
- feature "manage OpenIDConnect providers", %{session: session} do
- {_bypass, uri} = Domain.ConfigFixtures.discovery_document_server()
-
- # Create
- session =
- session
- |> visit(~p"/settings/security")
- |> assert_el(Query.text("Security Settings"))
- |> click(Query.link("Add OpenID Connect Provider"))
- |> assert_el(Query.text("OIDC Configuration"))
- |> fill_in(Query.fillable_field("open_id_connect_provider[id]"), with: "oidc-foo-bar")
- |> fill_in(Query.fillable_field("open_id_connect_provider[label]"), with: "Firebook")
- |> fill_in(Query.fillable_field("open_id_connect_provider[scope]"),
- with: "openid email eyes_color"
- )
- |> fill_in(Query.fillable_field("open_id_connect_provider[client_id]"), with: "CLIENT_ID")
- |> fill_in(Query.fillable_field("open_id_connect_provider[client_secret]"),
- with: "CLIENT_SECRET"
- )
- |> fill_in(Query.fillable_field("open_id_connect_provider[discovery_document_uri]"),
- with: uri
- )
- |> fill_in(Query.fillable_field("open_id_connect_provider[redirect_uri]"),
- with: "http://localhost/redirect"
- )
- |> toggle("open_id_connect_provider[auto_create_users]")
- |> click(Query.css("button[form=\"oidc-form\"]"))
- |> assert_el(Query.text("Updated successfully."))
- |> assert_el(Query.text("oidc-foo-bar"))
- |> assert_el(Query.text("Firebook"))
-
- assert [open_id_connect_provider] = Domain.Config.fetch_config!(:openid_connect_providers)
-
- assert open_id_connect_provider ==
- %Domain.Config.Configuration.OpenIDConnectProvider{
- id: "oidc-foo-bar",
- label: "Firebook",
- scope: "openid email eyes_color",
- response_type: "code",
- client_id: "CLIENT_ID",
- client_secret: "CLIENT_SECRET",
- discovery_document_uri: uri,
- redirect_uri: "http://localhost/redirect",
- auto_create_users: true
- }
-
- # Edit
- session =
- session
- |> click(Query.link("Edit"))
- |> assert_el(Query.text("OIDC Configuration"))
- |> fill_in(Query.fillable_field("open_id_connect_provider[label]"), with: "Metabook")
- |> click(Query.css("button[form=\"oidc-form\"]"))
- |> assert_el(Query.text("Updated successfully."))
- |> assert_el(Query.text("Metabook"))
-
- assert [open_id_connect_provider] = Domain.Config.fetch_config!(:openid_connect_providers)
- assert open_id_connect_provider.label == "Metabook"
-
- # Delete
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete"))
- end)
-
- assert_el(session, Query.text("Updated successfully."))
-
- assert Domain.Config.fetch_config!(:openid_connect_providers) == []
- end
-
- feature "manage SAML providers", %{session: session} do
- saml_metadata = Domain.ConfigFixtures.saml_metadata()
-
- # Create
- session =
- session
- |> visit(~p"/settings/security")
- |> assert_el(Query.text("Security Settings"))
- |> click(Query.link("Add SAML Identity Provider"))
- |> assert_el(Query.text("SAML Configuration"))
- |> toggle("saml_identity_provider[sign_requests]")
- |> toggle("saml_identity_provider[sign_metadata]")
- |> toggle("saml_identity_provider[signed_assertion_in_resp]")
- |> toggle("saml_identity_provider[signed_envelopes_in_resp]")
- |> toggle("saml_identity_provider[auto_create_users]")
- |> fill_in(Query.fillable_field("saml_identity_provider[id]"), with: "foo-bar-buz")
- |> fill_in(Query.fillable_field("saml_identity_provider[label]"), with: "Sneaky ID")
- |> fill_in(Query.fillable_field("saml_identity_provider[base_url]"),
- with: "http://localhost:13000/autX/saml#foo"
- )
- |> fill_in(Query.fillable_field("saml_identity_provider[metadata]"),
- with: saml_metadata
- )
- |> click(Query.css("button[form=\"saml-form\"]"))
- |> assert_el(Query.text("Updated successfully."))
- |> assert_el(Query.text("foo-bar-buz"))
- |> assert_el(Query.text("Sneaky ID"))
-
- assert [saml_identity_provider] = Domain.Config.fetch_config!(:saml_identity_providers)
-
- assert saml_identity_provider ==
- %Domain.Config.Configuration.SAMLIdentityProvider{
- id: "foo-bar-buz",
- label: "Sneaky ID",
- base_url: "http://localhost:13000/autX/saml#foo",
- metadata: saml_metadata,
- sign_requests: false,
- sign_metadata: false,
- signed_assertion_in_resp: false,
- signed_envelopes_in_resp: false,
- auto_create_users: true
- }
-
- # Edit
- session =
- session
- |> click(Query.link("Edit"))
- |> assert_el(Query.text("SAML Configuration"))
- |> fill_in(Query.fillable_field("saml_identity_provider[label]"), with: "Sneaky XID")
- |> click(Query.css("button[form=\"saml-form\"]"))
- |> assert_el(Query.text("Updated successfully."))
- |> assert_el(Query.text("Sneaky XID"))
-
- assert [saml_identity_provider] = Domain.Config.fetch_config!(:saml_identity_providers)
- assert saml_identity_provider.label == "Sneaky XID"
-
- # Delete
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete"))
- end)
-
- assert_el(session, Query.text("Updated successfully."))
-
- assert Domain.Config.fetch_config!(:saml_identity_providers) == []
- end
- end
-
- describe "profile" do
- feature "edit profile", %{
- session: session,
- user: user
- } do
- session
- |> visit(~p"/settings/account")
- |> assert_el(Query.link("Change Email or Password"))
- |> click(Query.link("Change Email or Password"))
- |> assert_el(Query.text("Edit Account"))
- |> fill_form(%{
- "user[email]" => "foo",
- "user[password]" => "123",
- "user[password_confirmation]" => "1234"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("is invalid email address"))
- |> assert_el(Query.text("should be at least 12 character(s)"))
- |> assert_el(Query.text("does not match confirmation"))
- |> fill_form(%{
- "user[email]" => "foo@xample.com",
- "user[password]" => "mynewpassword",
- "user[password_confirmation]" => "mynewpassword"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("Account updated successfully."))
-
- assert updated_user = Repo.one(Domain.Users.User)
- assert updated_user.password_hash != user.password_hash
- assert updated_user.email == "foo@xample.com"
- end
-
- feature "can see active user sessions", %{
- session: session,
- user_agent: user_agent
- } do
- session
- |> visit(~p"/settings/account")
- |> assert_el(Query.text("Active Sessions"))
- |> assert_el(Query.text(user_agent))
- end
-
- feature "can delete own account if there are other admins", %{session: session} do
- session =
- session
- |> visit(~p"/settings/account")
- |> assert_el(Query.text("Danger Zone"))
-
- assert attr(session, Query.button("Delete Your Account"), "disabled") ==
- "true"
-
- UsersFixtures.create_user_with_role(:admin)
-
- session =
- session
- |> visit(~p"/settings/account")
- |> assert_el(Query.text("Danger Zone"))
-
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete Your Account"))
- end)
-
- session
- |> Auth.assert_unauthenticated()
- |> assert_path("/")
- end
- end
-
- describe "api tokens" do
- feature "create, use using curl and delete API tokens", %{
- session: session,
- user: user,
- user_agent: user_agent
- } do
- session =
- session
- |> visit(~p"/settings/account")
- |> assert_el(Query.text("API Tokens"))
- |> assert_el(Query.text("No API tokens."))
- |> click(Query.css("[href=\"/settings/account/api_token\"]"))
- |> assert_el(Query.text("Add API Token", minimum: 1))
- |> fill_form(%{
- "api_token[expires_in]" => 1
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("API token secret:"))
-
- api_token_secret = text(session, Query.css("#api-token-secret"))
- curl_example = text(session, Query.css("#api-usage-example"))
- curl_example = String.replace(curl_example, ~r/^.*curl/is, "curl")
-
- assert String.contains?(curl_example, api_token_secret)
- assert api_token = Repo.one(Domain.ApiTokens.ApiToken)
- assert api_token.user_id == user.id
-
- args =
- curl_example
- |> String.trim_leading("curl ")
- |> String.replace("\\\n", "")
- |> String.replace(~r/[ ]+/, " ")
- |> String.replace("'", "")
- |> String.split(" ")
- |> curl_args([])
-
- args = ["-s", "-H", "User-Agent:#{user_agent}"] ++ args
- {resp, _} = System.cmd("curl", args, stderr_to_stdout: true)
-
- assert %{"data" => [%{"id" => user_id}]} = Jason.decode!(resp)
- assert user_id == user.id
-
- session =
- session
- |> click(Query.css("button[aria-label=\"close\"]"))
- |> assert_el(Query.text("API Tokens"))
- |> assert_el(Query.link("Delete"))
- |> click(Query.link(api_token.id))
- |> assert_el(Query.text("API token secret:"))
- |> click(Query.css("button[aria-label=\"close\"]"))
- |> assert_el(Query.link("Delete"))
- |> assert_path(~p"/settings/account")
-
- accept_confirm(session, fn session ->
- click(session, Query.link("Delete"))
- end)
-
- assert_el(session, Query.text("No API tokens."))
-
- assert is_nil(Repo.one(Domain.ApiTokens.ApiToken))
- end
- end
-
- defp curl_args([], acc) do
- acc
- end
-
- defp curl_args(["-H", header, "Bearer", token | rest], acc) do
- acc = acc ++ ["-H", "#{header}Bearer #{token}"]
- curl_args(rest, acc)
- end
-
- defp curl_args(["-H", header, value | rest], acc) do
- acc = acc ++ ["-H", "#{header}#{value}"]
- curl_args(rest, acc)
- end
-
- defp curl_args(["http" <> _ = url | rest], acc) do
- acc = acc ++ [url]
- curl_args(rest, acc)
- end
-
- defp curl_args([other | rest], acc) do
- curl_args(rest, acc ++ [other])
- end
-end
diff --git a/apps/web/test/web/acceptance/authentication_test.exs b/apps/web/test/web/acceptance/authentication_test.exs
deleted file mode 100644
index 5178e00f3..000000000
--- a/apps/web/test/web/acceptance/authentication_test.exs
+++ /dev/null
@@ -1,499 +0,0 @@
-defmodule Web.Acceptance.AuthenticationTest do
- use Web.AcceptanceCase, async: true
- alias Domain.UsersFixtures
- alias Domain.MFAFixtures
-
- describe "using login and password" do
- feature "renders error on invalid login or password", %{session: session} do
- session
- |> password_login_flow("foo@bar.com", "firezone1234")
- |> assert_error_flash(
- "Error signing in: user credentials are invalid or user does not exist"
- )
- end
-
- feature "renders error on invalid password", %{session: session} do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session
- |> password_login_flow(user.email, "firezone1234")
- |> assert_error_flash(
- "Error signing in: user credentials are invalid or user does not exist"
- )
- |> Auth.assert_unauthenticated()
- end
-
- feature "redirects to /users after successful log in as admin", %{session: session} do
- password = "firezone1234"
-
- user =
- UsersFixtures.create_user_with_role(:admin,
- password: password,
- password_confirmation: password
- )
-
- session
- |> password_login_flow(user.email, password)
- |> assert_el(Query.css(".is-user-name span"))
- |> assert_path("/users")
- |> Auth.assert_authenticated(user)
- end
-
- feature "redirects to /user_devices after successful log in as unprivileged user", %{
- session: session
- } do
- password = "firezone1234"
-
- user =
- UsersFixtures.create_user_with_role(
- :unprivileged,
- password: password,
- password_confirmation: password
- )
-
- session
- |> password_login_flow(user.email, password)
- |> assert_el(Query.text("Your Devices"))
- |> assert_path("/user_devices")
- |> Auth.assert_authenticated(user)
- end
-
- feature "can't reset password using invalid email", %{session: session} do
- UsersFixtures.create_user_with_role(:unprivileged)
-
- session
- |> visit(~p"/")
- |> assert_el(Query.link("Sign in with email"))
- |> click(Query.link("Sign in with email"))
- |> assert_el(Query.link("Forgot password"))
- |> click(Query.link("Forgot password"))
- |> assert_el(Query.text("Reset Password"))
- |> fill_form(%{"email" => "foo@bar.com"})
- |> click(Query.button("Send"))
- |> assert_el(Query.text("Reset Password"))
-
- emails = Swoosh.Adapters.Local.Storage.Memory.all()
- refute Enum.find(emails, &(&1.to == "foo@bar.com"))
- end
-
- feature "can reset password using email link", %{session: session} do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session =
- session
- |> visit(~p"/")
- |> assert_el(Query.link("Sign in with email"))
- |> click(Query.link("Sign in with email"))
- |> assert_el(Query.link("Forgot password"))
- |> click(Query.link("Forgot password"))
- |> assert_el(Query.text("Reset Password"))
- |> fill_form(%{
- "email" => user.email
- })
- |> click(Query.button("Send"))
- |> assert_el(Query.text("Please check your inbox for the magic link."))
- |> visit(~p"/dev/mailbox")
- |> click(Query.link("Firezone Magic Link"))
- |> assert_el(Query.text("HTML body preview:"))
-
- email_text = text(session, Query.css(".body-text"))
- [link] = Regex.run(~r|http://localhost[^ ]*|, email_text)
-
- session
- |> visit(link)
- |> assert_el(Query.text("Your Devices"))
- |> assert_el(Query.text("Signed in as #{user.email}."))
- end
- end
-
- describe "using SAML provider" do
- feature "creates a user when auto_create_users is true", %{session: session} do
- :ok = SimpleSAML.setup_saml_provider()
-
- session
- |> visit(~p"/")
- |> assert_el(Query.text("Sign In", minimum: 1))
- |> click(Query.link("Sign in with test-saml-idp"))
- |> assert_el(Query.link("Enter your username and password"))
- |> fill_in(Query.fillable_field("username"), with: "user1")
- |> fill_in(Query.fillable_field("password"), with: "user1pass")
- |> click(Query.button("Login"))
- |> assert_el(Query.text("Your Devices"))
- end
-
- feature "does not create new users when auto_create_users is false", %{session: session} do
- Domain.Config.put_config!(:local_auth_enabled, false)
- :ok = SimpleSAML.setup_saml_provider(%{"auto_create_users" => false})
-
- session
- |> visit(~p"/")
- |> assert_el(Query.text("Sign In", minimum: 1))
- |> click(Query.link("Sign in with test-saml-idp"))
- |> assert_el(Query.link("Enter your username and password"))
- |> fill_in(Query.fillable_field("username"), with: "user1")
- |> fill_in(Query.fillable_field("password"), with: "user1pass")
- |> click(Query.button("Login"))
- |> assert_el(Query.text("user not found and auto_create_users disabled"))
- end
- end
-
- describe "using OpenID Connect provider" do
- feature "creates a user when auto_create_users is true", %{session: session} do
- oidc_login = "firezone-1"
- oidc_password = "firezone1234_oidc"
- attrs = UsersFixtures.user_attrs()
-
- :ok = Vault.setup_oidc_provider(@endpoint.url, %{"auto_create_users" => true})
- :ok = Vault.upsert_user(oidc_login, attrs.email, oidc_password)
-
- session
- |> visit(~p"/")
- |> assert_el(Query.text("Sign In", minimum: 1))
- |> click(Query.link("OIDC Vault"))
- |> Vault.userpass_flow(oidc_login, oidc_password)
- |> assert_el(Query.text("Your Devices"))
- |> assert_path("/user_devices")
-
- assert user = Domain.Repo.one(Domain.Users.User)
- assert user.email == attrs.email
- assert user.role == :unprivileged
- assert user.last_signed_in_method == "vault"
- end
-
- feature "authenticates existing user", %{session: session} do
- user = UsersFixtures.create_user_with_role(:admin)
-
- oidc_login = "firezone-2"
- oidc_password = "firezone1234_oidc"
-
- :ok = Vault.setup_oidc_provider(@endpoint.url, %{"auto_create_users" => false})
- :ok = Vault.upsert_user(oidc_login, user.email, oidc_password)
-
- session
- |> visit(~p"/")
- |> assert_el(Query.text("Sign In", minimum: 1))
- |> click(Query.link("OIDC Vault"))
- |> Vault.userpass_flow(oidc_login, oidc_password)
- |> find(Query.text("Users", count: 2), fn _ -> :ok end)
- |> assert_path("/users")
-
- assert user = Domain.Repo.one(Domain.Users.User)
- assert user.email == user.email
- assert user.role == :admin
- assert user.last_signed_in_method == "vault"
- end
-
- feature "does not create new users when auto_create_users is false", %{session: session} do
- user_attrs = UsersFixtures.user_attrs()
-
- oidc_login = "firezone-2"
- oidc_password = "firezone1234_oidc"
-
- :ok = Vault.setup_oidc_provider(@endpoint.url, %{"auto_create_users" => false})
- :ok = Vault.upsert_user(oidc_login, user_attrs.email, oidc_password)
-
- session
- |> visit(~p"/")
- |> assert_el(Query.text("Sign In", minimum: 1))
- |> click(Query.link("OIDC Vault"))
- |> Vault.userpass_flow(oidc_login, oidc_password)
- |> assert_error_flash("Error signing in: user not found and auto_create_users disabled")
- |> Auth.assert_unauthenticated()
- end
-
- feature "allows to use OIDC when password auth is disabled", %{session: session} do
- user_attrs = UsersFixtures.user_attrs()
-
- oidc_login = "firezone-2"
- oidc_password = "firezone1234_oidc"
-
- :ok = Vault.setup_oidc_provider(@endpoint.url, %{"auto_create_users" => false})
- :ok = Vault.upsert_user(oidc_login, user_attrs.email, oidc_password)
-
- Domain.Config.put_config!(:local_auth_enabled, false)
-
- session = visit(session, ~p"/")
- assert find(session, Query.css(".input", count: 0))
- assert_el(session, Query.text("Please sign in via one of the methods below."))
- end
- end
-
- describe "MFA" do
- feature "allows unprivileged user to add and remove MFA method", %{
- session: session
- } do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session
- |> visit(~p"/")
- |> Auth.authenticate(user)
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> click(Query.link("My Account"))
- |> assert_el(Query.text("Account Settings"))
- |> click(Query.link("Add MFA Method"))
- |> mfa_create_flow()
- |> remove_mfa_flow()
- end
-
- feature "returns error when MFA method name is already taken", %{
- session: session
- } do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session =
- session
- |> visit(~p"/")
- |> Auth.authenticate(user)
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> click(Query.link("My Account"))
- |> assert_el(Query.text("Account Settings"))
- |> click(Query.link("Add MFA Method"))
- |> click(Query.button("Next"))
- |> assert_el(Query.text("Register Authenticator"))
-
- MFAFixtures.create_totp_method(name: "My MFA Name", user: user)
-
- session
- |> fill_in(Query.fillable_field("name"), with: "My MFA Name")
- |> click(Query.button("Next"))
- |> assert_el(Query.text("has already been taken"))
- end
-
- feature "allows admin user to add and remove MFA method", %{
- session: session
- } do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session
- |> visit(~p"/")
- |> Auth.authenticate(user)
- |> visit(~p"/users")
- |> hover(Query.css(".is-user-name span"))
- |> click(Query.link("Account Settings"))
- |> assert_el(Query.text("Multi Factor Authentication"))
- |> click(Query.link("Add MFA Method"))
- |> mfa_create_flow()
- |> remove_mfa_flow()
- end
-
- feature "MFA code is requested on unprivileged user login", %{session: session} do
- password = "firezone1234"
-
- user =
- UsersFixtures.create_user_with_role(
- :unprivileged,
- password: password,
- password_confirmation: password
- )
-
- secret = NimbleTOTP.secret()
- verification_code = NimbleTOTP.verification_code(secret)
-
- MFAFixtures.create_totp_method(%{
- payload: %{"secret" => Base.encode64(secret)},
- code: verification_code,
- user: user
- })
- |> MFAFixtures.rotate_totp_method_key()
-
- session
- |> password_login_flow(user.email, password)
- |> mfa_login_flow(verification_code)
- |> assert_el(Query.text("Your Devices"))
- |> assert_path("/user_devices")
- |> Auth.assert_authenticated(user)
- end
-
- feature "MFA code is requested on admin user login", %{session: session} do
- password = "firezone1234"
-
- user =
- UsersFixtures.create_user_with_role(
- :admin,
- password: password,
- password_confirmation: password
- )
-
- secret = NimbleTOTP.secret()
- verification_code = NimbleTOTP.verification_code(secret)
-
- MFAFixtures.create_totp_method(%{
- payload: %{"secret" => Base.encode64(secret)},
- code: verification_code,
- user: user
- })
- |> MFAFixtures.rotate_totp_method_key()
-
- session
- |> password_login_flow(user.email, password)
- |> mfa_login_flow(verification_code)
- |> assert_el(Query.css(".is-user-name"))
- |> assert_path("/users")
- |> Auth.assert_authenticated(user)
- end
-
- feature "user can sign out during MFA flow", %{session: session} do
- password = "firezone1234"
-
- user =
- UsersFixtures.create_user_with_role(
- :admin,
- password: password,
- password_confirmation: password
- )
-
- secret = NimbleTOTP.secret()
- verification_code = NimbleTOTP.verification_code(secret)
-
- MFAFixtures.create_totp_method(%{
- payload: %{"secret" => Base.encode64(secret)},
- code: verification_code,
- user: user
- })
- |> MFAFixtures.rotate_totp_method_key()
-
- session
- |> password_login_flow(user.email, password)
- |> assert_el(Query.text("Multi-factor Authentication"))
- |> click(Query.css("[data-to=\"/sign_out\"]"))
- |> assert_el(Query.text("Sign In"))
- |> Auth.assert_unauthenticated()
- |> assert_path("/")
- end
-
- feature "user can see other methods during MFA flow", %{session: session} do
- password = "firezone1234"
-
- user =
- UsersFixtures.create_user_with_role(
- :admin,
- password: password,
- password_confirmation: password
- )
-
- secret = NimbleTOTP.secret()
- verification_code = NimbleTOTP.verification_code(secret)
-
- method =
- MFAFixtures.create_totp_method(%{
- payload: %{"secret" => Base.encode64(secret)},
- code: verification_code,
- user: user
- })
- |> MFAFixtures.rotate_totp_method_key()
-
- session
- |> password_login_flow(user.email, password)
- |> assert_el(Query.text("Multi-factor Authentication"))
- |> click(Query.css("[href=\"/mfa/types\"]"))
- |> assert_el(Query.css("[href=\"/mfa/auth/#{method.id}\"]"))
- end
- end
-
- describe "sign out" do
- feature "signs out unprivileged user", %{session: session} do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session
- |> visit(~p"/")
- |> Auth.authenticate(user)
- |> visit(~p"/user_devices")
- |> click(Query.link("Sign out"))
- |> assert_el(Query.text("Sign In"))
- |> Auth.assert_unauthenticated()
- |> assert_path("/")
- end
-
- feature "signs out admin user", %{session: session} do
- user = UsersFixtures.create_user_with_role(:admin)
-
- session
- |> visit(~p"/")
- |> Auth.authenticate(user)
- |> visit(~p"/users")
- |> hover(Query.css(".is-user-name span"))
- |> click(Query.link("Log Out"))
- |> assert_el(Query.text("Sign In"))
- |> Auth.assert_unauthenticated()
- |> assert_path("/")
- end
- end
-
- defp assert_error_flash(session, text) do
- assert_text(session, Query.css(".flash-error"), text)
- session
- end
-
- defp password_login_flow(session, email, password) do
- session
- |> visit(~p"/")
- |> assert_el(Query.link("Sign in with email"))
- |> click(Query.link("Sign in with email"))
- |> assert_el(Query.text("Sign In", minimum: 1))
- |> fill_form(%{
- "Email" => email,
- "Password" => password
- })
- |> click(Query.button("Sign In"))
- end
-
- defp mfa_login_flow(session, verification_code) do
- session
- |> assert_el(Query.text("Multi-factor Authentication"))
- |> fill_form(%{"code" => "111111"})
- |> click(Query.button("Verify"))
- |> assert_el(Query.text("is invalid"))
- |> fill_form(%{"code" => verification_code})
- |> click(Query.button("Verify"))
- end
-
- defp mfa_create_flow(session) do
- assert selected?(session, Query.radio_button("mfa-method-totp"))
-
- session =
- session
- |> click(Query.button("Next"))
- |> assert_el(Query.text("Register Authenticator"))
- |> fill_in(Query.fillable_field("name"), with: "My MFA Name")
-
- secret =
- Browser.text(session, Query.css("#copy-totp-key"))
- |> String.replace(" ", "")
- |> Base.decode32!()
-
- session =
- session
- |> click(Query.button("Next"))
- |> assert_el(Query.text("Verify Code"))
- |> fill_in(Query.fillable_field("code"), with: "123456")
- |> click(Query.button("Next"))
- |> assert_el(Query.css("input.is-danger"))
- |> assert_el(Query.text("is invalid"))
- |> fill_in(Query.fillable_field("code"), with: NimbleTOTP.verification_code(secret))
- |> click(Query.button("Next"))
- |> assert_el(Query.text("Confirm to save this Authentication method."))
- |> click(Query.button("Save"))
- |> assert_el(Query.text("MFA method added!"))
-
- assert mfa_method = Repo.one(Domain.Auth.MFA.Method)
- assert mfa_method.name == "My MFA Name"
- assert mfa_method.payload["secret"] == Base.encode64(secret)
-
- session
- end
-
- defp remove_mfa_flow(session) do
- session =
- session
- |> assert_el(Query.text("Multi Factor Authentication"))
-
- accept_confirm(session, fn session ->
- click(session, Query.css("[phx-click=\"delete_authenticator\"]"))
- end)
-
- session
- |> assert_el(Query.text("No MFA methods added."))
- end
-end
diff --git a/apps/web/test/web/acceptance/unprivileged_user_test.exs b/apps/web/test/web/acceptance/unprivileged_user_test.exs
deleted file mode 100644
index 75e0c60e7..000000000
--- a/apps/web/test/web/acceptance/unprivileged_user_test.exs
+++ /dev/null
@@ -1,167 +0,0 @@
-defmodule Web.Acceptance.UnprivilegedUserTest do
- use Web.AcceptanceCase, async: true
- alias Domain.{UsersFixtures, DevicesFixtures}
-
- describe "device management" do
- setup tags do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session =
- tags.session
- |> visit(~p"/")
- |> Auth.authenticate(user)
-
- tags
- |> Map.put(:session, session)
- |> Map.put(:user, user)
- end
-
- feature "allows user to add and configure a device", %{
- session: session
- } do
- Domain.Config.put_config!(:allow_unprivileged_device_configuration, true)
-
- session
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> click(Query.link("Add Device"))
- |> assert_el(Query.button("Generate Configuration"))
- |> set_value(Query.css("#create-device_use_default_allowed_ips_false"), :selected)
- |> set_value(Query.css("#create-device_use_default_dns_false"), :selected)
- |> set_value(Query.css("#create-device_use_default_endpoint_false"), :selected)
- |> set_value(Query.css("#create-device_use_default_mtu_false"), :selected)
- |> set_value(
- Query.css("#create-device_use_default_persistent_keepalive_false"),
- :selected
- )
- |> fill_form(%{
- "device[allowed_ips]" => "127.0.0.1",
- "device[name]" => "big-head-007",
- "device[description]" => "Dummy description",
- "device[dns]" => "1.1.1.1,2.2.2.2",
- "device[endpoint]" => "example.com:51820",
- "device[mtu]" => "1400",
- "device[persistent_keepalive]" => "10",
- "device[ipv4]" => "100.64.255.100",
- "device[ipv6]" => "fd00::1e:3f96"
- })
- |> click(Query.button("Generate Configuration"))
- |> assert_el(Query.text("Device added!"))
- |> click(Query.css("button[phx-click=\"close\"]"))
- |> assert_el(Query.text("big-head-007"))
- |> assert_path(~p"/user_devices")
-
- assert device = Repo.one(Domain.Devices.Device)
- assert device.name == "big-head-007"
- assert device.description == "Dummy description"
- assert device.allowed_ips == [%Postgrex.INET{address: {127, 0, 0, 1}, netmask: nil}]
- assert device.dns == ["1.1.1.1", "2.2.2.2"]
- assert device.endpoint == "example.com:51820"
- assert device.mtu == 1400
- assert device.persistent_keepalive == 10
- assert device.ipv4 == %Postgrex.INET{address: {100, 64, 255, 100}}
- assert device.ipv6 == %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 0, 30, 16_278}}
- end
-
- feature "allows user to add a device, download config and close the modal", %{
- session: session
- } do
- Domain.Config.put_config!(:allow_unprivileged_device_configuration, false)
-
- session
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> click(Query.link("Add Device"))
- |> assert_el(Query.button("Generate Configuration"))
- |> fill_form(%{
- "device[name]" => "big-hand-007",
- "device[description]" => "Dummy description"
- })
- |> click(Query.button("Generate Configuration"))
- |> assert_el(Query.text("Device added!"))
- |> click(Query.css("#download-config"))
- |> click(Query.css("button[phx-click=\"close\"]"))
- |> assert_el(Query.text("big-hand-007"))
- |> assert_path(~p"/user_devices")
-
- assert device = Repo.one(Domain.Devices.Device)
- assert device.name == "big-hand-007"
- assert device.description == "Dummy description"
- end
-
- feature "does not allow adding devices", %{session: session} do
- Domain.Config.put_config!(:allow_unprivileged_device_management, false)
-
- session
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> refute_has(Query.link("Add Device"))
- end
-
- feature "allows user to delete a device", %{
- session: session,
- user: user
- } do
- device = DevicesFixtures.create_device(user: user)
-
- session =
- session
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> assert_el(Query.text(device.public_key))
- |> click(Query.link(device.name))
- |> assert_el(Query.text(device.description))
-
- accept_confirm(session, fn session ->
- click(session, Query.button("Delete Device #{device.name}"))
- end)
-
- assert_el(session, Query.text("No devices to show."))
-
- assert Repo.one(Domain.Devices.Device) == nil
- end
- end
-
- describe "profile" do
- setup tags do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- session =
- tags.session
- |> visit(~p"/")
- |> Auth.authenticate(user)
-
- tags
- |> Map.put(:session, session)
- |> Map.put(:user, user)
- end
-
- feature "allows to change password", %{
- session: session,
- user: user
- } do
- session
- |> visit(~p"/user_devices")
- |> assert_el(Query.text("Your Devices"))
- |> click(Query.link("My Account"))
- |> assert_el(Query.text("Account Settings"))
- |> click(Query.link("Change Password"))
- |> assert_el(Query.text("Enter new password below."))
- |> fill_form(%{
- "user[password]" => "foo",
- "user[password_confirmation]" => ""
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("should be at least 12 character(s)"))
- |> assert_el(Query.text("does not match confirmation"))
- |> fill_form(%{
- "user[password]" => "new_password",
- "user[password_confirmation]" => "new_password"
- })
- |> click(Query.button("Save"))
- |> assert_el(Query.text("Password updated successfully"))
-
- assert Repo.one(Domain.Users.User).password_hash != user.password_hash
- end
- end
-end
diff --git a/apps/web/test/web/auth/json/authentication_test.exs b/apps/web/test/web/auth/json/authentication_test.exs
deleted file mode 100644
index df3c71b4d..000000000
--- a/apps/web/test/web/auth/json/authentication_test.exs
+++ /dev/null
@@ -1,34 +0,0 @@
-defmodule Web.Auth.JSON.AuthenticationTest do
- use Web.ApiCase, async: true
- alias Domain.UsersFixtures
- import Web.ApiCase
-
- test "renders error when api token is invalid" do
- conn =
- api_conn()
- |> Plug.Conn.put_req_header("authorization", "bearer invalid")
- |> Web.Auth.JSON.Pipeline.call([])
-
- assert json_response(conn, 401) == %{"errors" => %{"auth" => "invalid_token"}}
- end
-
- test "renders error when api token resource is invalid" do
- user = UsersFixtures.create_user_with_role(:admin)
- subject = Domain.Auth.fetch_subject!(user, "127.0.0.1", "AuthTest")
-
- claims = %{
- "api" => Ecto.UUID.generate(),
- "exp" => DateTime.to_unix(DateTime.utc_now() |> DateTime.add(1, :hour))
- }
-
- {:ok, token, _claims} =
- Guardian.encode_and_sign(Web.Auth.JSON.Authentication, subject, claims)
-
- conn =
- api_conn()
- |> Plug.Conn.put_req_header("authorization", "bearer #{token}")
- |> Web.Auth.JSON.Pipeline.call([])
-
- assert json_response(conn, 401) == %{"errors" => %{"auth" => "no_resource_found"}}
- end
-end
diff --git a/apps/web/test/web/channels/notification_channel_test.exs b/apps/web/test/web/channels/notification_channel_test.exs
deleted file mode 100644
index 9bad19ed1..000000000
--- a/apps/web/test/web/channels/notification_channel_test.exs
+++ /dev/null
@@ -1,30 +0,0 @@
-defmodule Web.NotificationChannelTest do
- use Web.ChannelCase, async: true
-
- alias Domain.UsersFixtures
- alias Web.NotificationChannel
-
- describe "channel join" do
- setup _tags do
- user = UsersFixtures.create_user_with_role(:admin)
-
- socket =
- Web.UserSocket
- |> socket(user.id, %{remote_ip: "127.0.0.1", user_agent: "test", current_user_id: user.id})
-
- %{
- user: user,
- socket: socket,
- token: Phoenix.Token.sign(socket, "channel auth", user.id)
- }
- end
-
- test "joins channel ", %{socket: socket, user: user} do
- {:ok, _, test_socket} =
- socket
- |> subscribe_and_join(NotificationChannel, "notification:session", %{})
-
- assert test_socket.assigns.current_user.id == user.id
- end
- end
-end
diff --git a/apps/web/test/web/controllers/auth_controller_test.exs b/apps/web/test/web/controllers/auth_controller_test.exs
deleted file mode 100644
index b5376df7b..000000000
--- a/apps/web/test/web/controllers/auth_controller_test.exs
+++ /dev/null
@@ -1,345 +0,0 @@
-defmodule Web.AuthControllerTest do
- use Web.ConnCase, async: true
- alias Domain.ConfigFixtures
- alias Domain.Repo
-
- setup do
- {bypass, _openid_connect_providers_attrs} =
- ConfigFixtures.start_openid_providers([
- "google",
- "okta",
- "auth0",
- "azure",
- "onelogin",
- "keycloak",
- "vault"
- ])
-
- Domain.Config.put_config!(
- :saml_identity_providers,
- [Domain.ConfigFixtures.saml_identity_providers_attrs(%{"label" => "SAML"})]
- )
-
- %{bypass: bypass}
- end
-
- describe "new" do
- setup [:create_user]
-
- test "unauthed: loads the sign in form", %{unauthed_conn: conn} do
- test_conn = get(conn, ~p"/")
-
- # Assert that we have email, OIDC and Oauth2 buttons provided
- for expected <- [
- "Sign in with email",
- "Sign in with OIDC Google",
- "Sign in with OIDC Okta",
- "Sign in with OIDC Auth0",
- "Sign in with OIDC Azure",
- "Sign in with OIDC Onelogin",
- "Sign in with OIDC Keycloak",
- "Sign in with OIDC Vault",
- "Sign in with SAML"
- ] do
- assert html_response(test_conn, 200) =~ expected
- end
- end
-
- test "authed as admin: redirects to users page", %{admin_conn: conn} do
- test_conn = get(conn, ~p"/")
-
- assert redirected_to(test_conn) == ~p"/users"
- end
-
- test "authed as unprivileged: redirects to user_devices page", %{unprivileged_conn: conn} do
- test_conn = get(conn, ~p"/")
-
- assert redirected_to(test_conn) == ~p"/user_devices"
- end
- end
-
- describe "create session" do
- setup [:create_user]
-
- test "GET /auth/identity/callback redirects to /", %{unauthed_conn: conn} do
- assert redirected_to(get(conn, ~p"/auth/identity/callback")) == ~p"/"
- end
-
- test "GET /auth/identity omits forgot password link when local_auth disabled", %{
- unauthed_conn: conn
- } do
- Domain.Config.put_config!(:local_auth_enabled, false)
- test_conn = get(conn, ~p"/auth/identity")
-
- assert text_response(test_conn, 404) == "Local auth disabled"
- end
-
- test "when local_auth is disabled responds with 404", %{unauthed_conn: conn} do
- Domain.Config.put_config!(:local_auth_enabled, false)
- test_conn = post(conn, ~p"/auth/identity/callback", %{})
-
- assert text_response(test_conn, 404) == "Local auth disabled"
- end
-
- test "invalid email", %{unauthed_conn: conn} do
- params = %{
- "email" => "invalid@test",
- "password" => "test"
- }
-
- test_conn = post(conn, ~p"/auth/identity/callback", params)
-
- assert test_conn.request_path == ~p"/auth/identity/callback"
-
- assert Phoenix.Flash.get(test_conn.assigns.flash, :error) ==
- "Error signing in: user credentials are invalid or user does not exist"
- end
-
- test "invalid password", %{unauthed_conn: conn, user: user} do
- params = %{
- "email" => user.email,
- "password" => "invalid"
- }
-
- test_conn = post(conn, ~p"/auth/identity/callback", params)
-
- assert test_conn.request_path == ~p"/auth/identity/callback"
-
- assert Phoenix.Flash.get(test_conn.assigns.flash, :error) ==
- "Error signing in: user credentials are invalid or user does not exist"
- end
-
- test "valid params", %{unauthed_conn: conn, user: user} do
- params = %{
- "email" => user.email,
- "password" => "password1234"
- }
-
- test_conn = post(conn, ~p"/auth/identity/callback", params)
-
- assert redirected_to(test_conn) == ~p"/users"
- assert current_user(test_conn).id == user.id
- end
-
- test "prevents signing in when local_auth_disabled", %{unauthed_conn: conn, user: user} do
- params = %{
- "email" => user.email,
- "password" => "password1234"
- }
-
- Domain.Config.put_config!(:local_auth_enabled, false)
-
- test_conn = post(conn, ~p"/auth/identity/callback", params)
- assert text_response(test_conn, 404) == "Local auth disabled"
- end
- end
-
- describe "GET /auth/reset_password" do
- test "protects route when local_auth is disabled", %{unauthed_conn: conn} do
- Domain.Config.put_config!(:local_auth_enabled, false)
- test_conn = get(conn, ~p"/auth/reset_password")
-
- assert text_response(test_conn, 404) == "Local auth disabled"
- end
- end
-
- describe "creating session from OpenID Connect" do
- setup :create_user
-
- @key "fz_oidc_state"
- @state "test"
-
- @params %{
- "code" => "MyFaketoken",
- "provider" => "google",
- "state" => @state
- }
-
- setup %{unauthed_conn: conn} = context do
- signed_state =
- Plug.Crypto.sign(
- Domain.Config.fetch_env!(:web, Web.Endpoint)[:secret_key_base],
- @key <> "_cookie",
- @state,
- key: Plug.Keys,
- max_age: context[:max_age] || 300
- )
-
- {:ok, unauthed_conn: put_req_cookie(conn, "fz_oidc_state", signed_state)}
- end
-
- test "when a user returns with a valid claim", %{
- unauthed_conn: conn,
- user: user,
- bypass: bypass
- } do
- jwk = ConfigFixtures.jwks_attrs()
-
- claims = %{"email" => user.email, "sub" => user.id}
-
- {_alg, token} =
- jwk
- |> JOSE.JWK.from()
- |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"})
- |> JOSE.JWS.compact()
-
- ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token})
-
- test_conn = get(conn, ~p"/auth/oidc/google/callback", @params)
- assert redirected_to(test_conn) == ~p"/users"
-
- assert get_session(test_conn, "id_token")
- end
-
- test "when a user returns with an invalid claim", %{unauthed_conn: conn, bypass: bypass} do
- jwk = ConfigFixtures.jwks_attrs()
-
- claims = %{"email" => "foo@example.com", "sub" => Ecto.UUID.generate()}
-
- {_alg, token} =
- jwk
- |> JOSE.JWK.from()
- |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"})
- |> JOSE.JWS.compact()
-
- ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token})
-
- test_conn = get(conn, ~p"/auth/oidc/google/callback", @params)
-
- assert Phoenix.Flash.get(test_conn.assigns.flash, :error) ==
- "Error signing in: user not found and auto_create_users disabled"
- end
-
- test "when a user returns with an invalid state", %{unauthed_conn: conn} do
- test_conn =
- get(conn, ~p"/auth/oidc/google/callback", %{
- @params
- | "state" => "not_valid"
- })
-
- assert Phoenix.Flash.get(test_conn.assigns.flash, :error) ==
- "An OpenIDConnect error occurred. Details: \"Cannot verify state\""
- end
-
- @tag max_age: 0
- test "when a user returns with an expired state", %{unauthed_conn: conn} do
- test_conn = get(conn, ~p"/auth/oidc/google/callback", @params)
-
- assert Phoenix.Flash.get(test_conn.assigns.flash, :error) ==
- "An OpenIDConnect error occurred. Details: \"Cannot verify state\""
- end
- end
-
- describe "when deleting a session" do
- setup :create_user
-
- test "user signed in", %{admin_conn: conn} do
- test_conn = delete(conn, ~p"/sign_out")
- assert redirected_to(test_conn) == ~p"/"
- end
-
- test "user not signed in", %{unauthed_conn: conn} do
- test_conn = delete(conn, ~p"/sign_out")
- assert redirected_to(test_conn) == ~p"/"
- end
- end
-
- describe "getting magic link" do
- setup :create_user
-
- test "redirects to root path", %{unauthed_conn: conn, user: user} do
- refute user.sign_in_token
-
- test_conn = post(conn, ~p"/auth/magic_link", %{"email" => user.email})
-
- assert redirected_to(test_conn) == ~p"/"
-
- assert Phoenix.Flash.get(test_conn.assigns.flash, :info) ==
- "Please check your inbox for the magic link."
-
- user = Repo.get(Domain.Users.User, user.id)
- assert user.sign_in_token_hash
-
- assert_receive {:email, email}
-
- assert email.subject == "Firezone Magic Link"
- assert email.to == [{"", user.email}]
- assert email.text_body =~ "/auth/magic/#{user.id}/"
-
- token = String.split(email.assigns.link, "/") |> List.last()
-
- assert {:ok, _user} = Domain.Users.consume_sign_in_token(user, token)
- end
- end
-
- describe "when using magic link" do
- setup :create_user
-
- setup context do
- {:ok, user} = Domain.Users.request_sign_in_token(context.user)
- Map.put(context, :user, user)
- end
-
- test "user sign_in_token is cleared", %{unauthed_conn: conn, user: user} do
- assert not is_nil(user.sign_in_token)
- assert not is_nil(user.sign_in_token_created_at)
-
- get(conn, ~p"/auth/magic/#{user.id}/#{user.sign_in_token}")
-
- user = Repo.reload!(user)
-
- assert is_nil(user.sign_in_token)
- assert is_nil(user.sign_in_token_created_at)
- end
-
- test "user last signed in with magic_link provider", %{unauthed_conn: conn, user: user} do
- get(conn, ~p"/auth/magic/#{user.id}/#{user.sign_in_token}")
-
- user = Repo.reload!(user)
-
- assert user.last_signed_in_method == "magic_link"
- end
-
- test "user is signed in", %{unauthed_conn: conn, user: user} do
- test_conn = get(conn, ~p"/auth/magic/#{user.id}/#{user.sign_in_token}")
-
- assert current_user(test_conn).id == user.id
- end
-
- test "prevents signing in when local_auth_disabled", %{unauthed_conn: conn, user: user} do
- Domain.Config.put_config!(:local_auth_enabled, false)
-
- test_conn = get(conn, ~p"/auth/magic/#{user.id}/#{user.sign_in_token}")
- assert text_response(test_conn, 404) == "Local auth disabled"
- end
- end
-
- describe "oidc signout url" do
- @tag session: [login_method: "okta", id_token: "abc"]
- test "redirects to oidc end_session_uri", %{admin_conn: conn} do
- query =
- URI.encode_query(%{
- "id_token_hint" => "abc",
- "post_logout_redirect_uri" => Domain.Config.fetch_env!(:web, :external_url),
- "client_id" => "okta-client-id"
- })
-
- complete_uri =
- "https://example.com"
- |> URI.merge("?#{query}")
- |> URI.to_string()
-
- test_conn = delete(conn, ~p"/sign_out")
- assert redirected_to(test_conn) == complete_uri
- end
- end
-
- describe "oidc signin url" do
- test "redirects to oidc auth uri", %{unauthed_conn: conn, bypass: bypass} do
- test_conn = get(conn, ~p"/auth/oidc/google")
-
- bypass_url = "http://localhost:#{bypass.port}/authorize"
- assert String.starts_with?(redirected_to(test_conn), bypass_url)
- end
- end
-end
diff --git a/apps/web/test/web/controllers/browser_controller_test.exs b/apps/web/test/web/controllers/browser_controller_test.exs
deleted file mode 100644
index d9d7ea1e5..000000000
--- a/apps/web/test/web/controllers/browser_controller_test.exs
+++ /dev/null
@@ -1,12 +0,0 @@
-defmodule Web.BrowserControllerTest do
- use Web.ConnCase, async: true
-
- describe "config/2" do
- test "returns valid XML browse config", %{unauthed_conn: conn} do
- test_conn = get(conn, ~p"/browser/config.xml")
-
- assert response(test_conn, 200) =~ " doc()
-
- assert json_response(conn, 200)["data"]
- end
-
- test "renders logotype" do
- Domain.Config.put_config!(:logo, %{"url" => "https://example.com/logo.png"})
-
- conn = get(authed_conn(), ~p"/v0/configuration")
-
- assert %{
- "logo" => %{
- "data" => nil,
- "type" => nil,
- "url" => "https://example.com/logo.png"
- }
- } = json_response(conn, 200)["data"]
- end
-
- test "renders 401 for missing authorization header" do
- conn = get(unauthed_conn(), ~p"/v0/configuration")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "PUT /v0/configuration" do
- test "updates fields when data is valid" do
- attrs = %{
- "local_auth_enabled" => false,
- "allow_unprivileged_device_management" => false,
- "allow_unprivileged_device_configuration" => false,
- "openid_connect_providers" => [
- %{
- "id" => "google",
- "label" => "google",
- "scope" => "email openid",
- "response_type" => "code",
- "client_id" => "test-id",
- "client_secret" => "test-secret",
- "discovery_document_uri" =>
- "https://accounts.google.com/.well-known/openid-configuration",
- "redirect_uri" => "https://invalid",
- "auto_create_users" => false
- }
- ],
- "saml_identity_providers" => [
- %{
- "id" => "okta",
- "label" => "okta",
- "base_url" => "https://saml",
- "metadata" => ConfigFixtures.saml_metadata(),
- "sign_requests" => false,
- "sign_metadata" => false,
- "signed_assertion_in_resp" => false,
- "signed_envelopes_in_resp" => false,
- "auto_create_users" => false
- }
- ],
- "disable_vpn_on_oidc_error" => true,
- "vpn_session_duration" => 100,
- "default_client_persistent_keepalive" => 1,
- "default_client_mtu" => 1100,
- "default_client_endpoint" => "new-endpoint",
- "default_client_dns" => ["1.1.1.1"],
- "default_client_allowed_ips" => ["1.1.1.1", "2.2.2.2"]
- }
-
- conn =
- put(authed_conn(), ~p"/v0/configuration", configuration: attrs)
- |> doc()
-
- {generated_attrs, update_attrs} =
- Map.split(json_response(conn, 200)["data"], ~w[id inserted_at logo updated_at])
-
- assert update_attrs == attrs
- assert %{"id" => _, "inserted_at" => _, "logo" => _, "updated_at" => _} = generated_attrs
-
- attrs = %{
- "local_auth_enabled" => true,
- "allow_unprivileged_device_management" => true,
- "allow_unprivileged_device_configuration" => true,
- "openid_connect_providers" => [
- %{
- "id" => "google",
- "label" => "google-label",
- "scope" => "email openid",
- "response_type" => "code",
- "client_id" => "test-id-2",
- "client_secret" => "test-secret-2",
- "discovery_document_uri" =>
- "https://accounts.google.com/.well-known/openid-configuration",
- "redirect_uri" => "https://invalid-2",
- "auto_create_users" => true
- }
- ],
- "saml_identity_providers" => [
- %{
- "id" => "okta",
- "label" => "okta-label",
- "base_url" => "https://saml-old",
- "metadata" => ConfigFixtures.saml_metadata(),
- "sign_requests" => true,
- "sign_metadata" => true,
- "signed_assertion_in_resp" => true,
- "signed_envelopes_in_resp" => true,
- "auto_create_users" => true
- }
- ],
- "disable_vpn_on_oidc_error" => false,
- "vpn_session_duration" => 1,
- "default_client_persistent_keepalive" => 25,
- "default_client_mtu" => 1200,
- "default_client_endpoint" => "old-endpoint",
- "default_client_dns" => ["4.4.4.4"],
- "default_client_allowed_ips" => ["8.8.8.8"]
- }
-
- conn = put(authed_conn(), ~p"/v0/configuration", configuration: attrs)
-
- {generated_attrs, update_attrs} =
- Map.split(json_response(conn, 200)["data"], ~w[id inserted_at logo updated_at])
-
- assert update_attrs == attrs
- assert %{"id" => _, "inserted_at" => _, "logo" => _, "updated_at" => _} = generated_attrs
- end
-
- test "renders errors when data is invalid" do
- conn =
- put(authed_conn(), ~p"/v0/configuration", configuration: %{"local_auth_enabled" => 123})
-
- assert json_response(conn, 422)["errors"] == %{"local_auth_enabled" => ["is invalid"]}
- end
-
- test "renders error when trying to override a value with environment override" do
- Domain.Config.put_system_env_override(:local_auth_enabled, true)
-
- attrs = %{
- "local_auth_enabled" => false
- }
-
- conn = put(authed_conn(), ~p"/v0/configuration", configuration: attrs)
-
- assert json_response(conn, 422) == %{
- "errors" => %{
- "local_auth_enabled" => [
- "cannot be changed; it is overridden by LOCAL_AUTH_ENABLED environment variable"
- ]
- }
- }
- end
-
- test "renders 401 for missing authorization header" do
- conn = put(unauthed_conn(), ~p"/v0/configuration")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-end
diff --git a/apps/web/test/web/controllers/json/device_controller_test.exs b/apps/web/test/web/controllers/json/device_controller_test.exs
deleted file mode 100644
index fc81cb4f4..000000000
--- a/apps/web/test/web/controllers/json/device_controller_test.exs
+++ /dev/null
@@ -1,163 +0,0 @@
-defmodule Web.JSON.DeviceControllerTest do
- use Web.ApiCase, async: true
- alias Domain.{DevicesFixtures, UsersFixtures}
-
- @params %{
- "name" => "create-name",
- "description" => "create-description",
- "public_key" => "CHqFuS+iL3FTog5F4Ceumqlk0CU4Cl/dyUP/9F9NDnI=",
- "preshared_key" => "CHqFuS+iL3FTog5F4Ceumqlk0CU4Cl/dyUP/9F9NDnI=",
- "use_default_allowed_ips" => false,
- "use_default_dns" => false,
- "use_default_endpoint" => false,
- "use_default_mtu" => false,
- "use_default_persistent_keepalive" => false,
- "endpoint" => "9.9.9.9",
- "mtu" => 999,
- "persistent_keepalive" => 9,
- "allowed_ips" => ["0.0.0.0/0", "::/0", "1.1.1.1"],
- "dns" => ["9.9.9.8"],
- "ipv4" => "100.64.0.2",
- "ipv6" => "fd00::2"
- }
-
- describe "GET /v0/devices/:id" do
- test "shows device" do
- id = DevicesFixtures.create_device().id
-
- conn =
- get(authed_conn(), ~p"/v0/devices/#{id}")
- |> doc()
-
- assert %{"id" => ^id} = json_response(conn, 200)["data"]
- end
-
- test "renders 404 for device not found" do
- conn = get(authed_conn(), ~p"/v0/devices/003da73d-2dd9-4492-8136-3282843545e8")
-
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- device = DevicesFixtures.create_device()
- conn = get(unauthed_conn(), ~p"/v0/devices/#{device}")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "POST /v0/devices" do
- test "creates device for unprivileged user" do
- unprivileged_user = UsersFixtures.create_user_with_role(:unprivileged)
-
- conn =
- post(authed_conn(), ~p"/v0/devices",
- device: Map.merge(@params, %{"user_id" => unprivileged_user.id})
- )
- |> doc()
-
- assert @params = json_response(conn, 201)["data"]
- end
-
- test "creates device for self" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
- conn = post(conn, ~p"/v0/devices", device: Map.merge(@params, %{"user_id" => user.id}))
- assert @params = json_response(conn, 201)["data"]
- end
-
- test "creates device for other admin" do
- admin_user = UsersFixtures.create_user_with_role(:admin)
-
- conn =
- post(authed_conn(), ~p"/v0/devices",
- device: Map.merge(@params, %{"user_id" => admin_user.id})
- )
-
- assert @params = json_response(conn, 201)["data"]
- end
-
- test "renders 401 for missing authorization header" do
- conn = post(unauthed_conn(), ~p"/v0/devices", device: %{})
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "PUT /v0/devices/:id" do
- test "updates device" do
- device = DevicesFixtures.create_device()
-
- update_attrs = %{
- "name" => "update-name",
- "description" => "update-description"
- }
-
- conn =
- put(authed_conn(), ~p"/v0/devices/#{device}", device: update_attrs)
- |> doc()
-
- assert device = json_response(conn, 200)["data"]
- assert device["name"] == update_attrs["name"]
- assert device["description"] == update_attrs["description"]
- end
-
- test "renders 404 for device not found" do
- conn = put(authed_conn(), ~p"/v0/devices/003da73d-2dd9-4492-8136-3282843545e8", device: %{})
-
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = put(unauthed_conn(), ~p"/v0/devices/#{DevicesFixtures.create_device()}", device: %{})
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "GET /v0/devices" do
- test "lists all devices" do
- devices = for _i <- 1..5, do: DevicesFixtures.create_device()
-
- conn =
- get(authed_conn(), ~p"/v0/devices")
- |> doc()
-
- assert json_response(conn, 200)["data"]
- |> Enum.map(& &1["id"])
- |> MapSet.new() ==
- devices
- |> Enum.map(& &1.id)
- |> MapSet.new()
- end
-
- test "renders 401 for missing authorization header" do
- conn = get(unauthed_conn(), ~p"/v0/devices")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "DELETE /v0/devices/:id" do
- test "deletes device" do
- device = DevicesFixtures.create_device()
-
- conn =
- delete(authed_conn(), ~p"/v0/devices/#{device}")
- |> doc()
-
- assert response(conn, 204)
-
- conn = get(conn, ~p"/v0/devices/#{device}")
-
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 404 for device not found" do
- conn = delete(authed_conn(), ~p"/v0/devices/003da73d-2dd9-4492-8136-3282843545e8")
-
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = delete(unauthed_conn(), ~p"/v0/devices/#{DevicesFixtures.create_device()}")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-end
diff --git a/apps/web/test/web/controllers/json/rule_controller_test.exs b/apps/web/test/web/controllers/json/rule_controller_test.exs
deleted file mode 100644
index 70f8bf0dd..000000000
--- a/apps/web/test/web/controllers/json/rule_controller_test.exs
+++ /dev/null
@@ -1,176 +0,0 @@
-defmodule Web.JSON.RuleControllerTest do
- use Web.ApiCase, async: true
- alias Domain.RulesFixtures
- import Web.ApiCase
-
- @accept_rule_params %{
- "destination" => "1.1.1.1/24",
- "action" => "accept",
- "port_type" => "udp",
- "port_range" => "1 - 2"
- }
-
- @drop_rule_params %{
- "destination" => "5.5.5.5/24",
- "action" => "drop",
- "port_type" => "tcp",
- "port_range" => "1 - 65000"
- }
-
- describe "GET /v0/rules/:id" do
- test "shows rule" do
- id = RulesFixtures.create_rule().id
-
- conn =
- get(authed_conn(), ~p"/v0/rules/#{id}")
- |> doc()
-
- assert %{"id" => ^id} = json_response(conn, 200)["data"]
- end
-
- test "renders 404 for rule not found" do
- conn = get(authed_conn(), ~p"/v0/rules/003da73d-2dd9-4492-8136-3282843545e8")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- rule = RulesFixtures.create_rule()
- conn = get(unauthed_conn(), ~p"/v0/rules/#{rule}")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "POST /v0/rules" do
- test "creates accept rule when valid" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
-
- conn =
- post(conn, ~p"/v0/rules", rule: Map.merge(@accept_rule_params, %{"user_id" => user.id}))
- |> doc()
-
- assert @accept_rule_params = json_response(conn, 201)["data"]
- end
-
- test "creates drop rule when valid" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
-
- conn =
- post(conn, ~p"/v0/rules", rule: Map.merge(@drop_rule_params, %{"user_id" => user.id}))
-
- assert @drop_rule_params = json_response(conn, 201)["data"]
- end
-
- test "returns errors when invalid" do
- params = %{"action" => "invalid"}
- conn = post(authed_conn(), ~p"/v0/rules", rule: params)
-
- assert json_response(conn, 422)["errors"] == %{
- "action" => ["is invalid"],
- "destination" => ["can't be blank"]
- }
- end
-
- test "renders 401 for missing authorization header" do
- conn = post(unauthed_conn(), ~p"/v0/rules", rule: %{})
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "PUT /v0/rules/:id" do
- test "updates accept rule when valid" do
- rule = RulesFixtures.create_rule()
-
- conn =
- put(authed_conn(), ~p"/v0/rules/#{rule}", rule: @accept_rule_params)
- |> doc()
-
- assert @accept_rule_params = json_response(conn, 200)["data"]
-
- conn = get(conn, ~p"/v0/rules/#{rule}")
- assert @accept_rule_params = json_response(conn, 200)["data"]
- end
-
- test "updates drop rule when valid" do
- rule = RulesFixtures.create_rule()
- conn = put(authed_conn(), ~p"/v0/rules/#{rule}", rule: @drop_rule_params)
- assert @drop_rule_params = json_response(conn, 200)["data"]
-
- conn = get(authed_conn(), ~p"/v0/rules/#{rule}")
- assert @drop_rule_params = json_response(conn, 200)["data"]
- end
-
- test "returns errors when invalid" do
- rule = RulesFixtures.create_rule()
- params = %{"action" => "invalid"}
- conn = put(authed_conn(), ~p"/v0/rules/#{rule}", rule: params)
- assert json_response(conn, 422)["errors"] == %{"action" => ["is invalid"]}
- end
-
- test "renders 404 for rule not found" do
- conn = put(authed_conn(), ~p"/v0/rules/003da73d-2dd9-4492-8136-3282843545e8", rule: %{})
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = put(unauthed_conn(), ~p"/v0/rules/#{RulesFixtures.create_rule()}", rule: %{})
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "GET /v0/rules" do
- test "lists rules" do
- rules =
- for i <- 1..5 do
- RulesFixtures.create_rule(%{destination: "10.3.2.#{i}"})
- end
-
- conn =
- get(authed_conn(), ~p"/v0/rules")
- |> doc()
-
- actual =
- rules
- |> Enum.map(& &1.id)
- |> Enum.sort()
-
- expected =
- json_response(conn, 200)["data"]
- |> Enum.map(& &1["id"])
- |> Enum.sort()
-
- assert actual == expected
- end
-
- test "renders 401 for missing authorization header" do
- conn = get(unauthed_conn(), ~p"/v0/rules")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "DELETE /v0/rules/:id" do
- test "deletes rule" do
- rule = RulesFixtures.create_rule()
-
- conn =
- delete(authed_conn(), ~p"/v0/rules/#{rule}")
- |> doc()
-
- assert response(conn, 204)
-
- conn = get(authed_conn(), ~p"/v0/rules/#{rule}")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 404 for rule not found" do
- conn = delete(authed_conn(), ~p"/v0/rules/#{Ecto.UUID.generate()}")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = delete(unauthed_conn(), ~p"/v0/rules/#{RulesFixtures.create_rule()}")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-end
diff --git a/apps/web/test/web/controllers/json/user_controller_test.exs b/apps/web/test/web/controllers/json/user_controller_test.exs
deleted file mode 100644
index d8c6a6664..000000000
--- a/apps/web/test/web/controllers/json/user_controller_test.exs
+++ /dev/null
@@ -1,293 +0,0 @@
-defmodule Web.JSON.UserControllerTest do
- use Web.ApiCase, async: true
- import Web.ApiCase
- alias Domain.UsersFixtures
- alias Domain.Users
-
- @create_attrs %{
- "email" => "test@test.com",
- "password" => "test1234test",
- "password_confirmation" => "test1234test"
- }
- @update_attrs %{
- "email" => "test2@test.com"
- }
- @invalid_attrs %{
- "email" => "test@test.com",
- "password" => "test1234"
- }
-
- describe "GET /v0/users" do
- test "lists all users" do
- for _i <- 1..3, do: UsersFixtures.create_user_with_role(:admin)
-
- conn =
- get(authed_conn(), ~p"/v0/users")
- |> doc()
-
- actual =
- Repo.all(Users.User)
- |> Enum.map(fn u -> u.id end)
- |> Enum.sort()
-
- expected =
- json_response(conn, 200)["data"]
- |> Enum.map(fn m -> m["id"] end)
- |> Enum.sort()
-
- assert actual == expected
- end
-
- test "renders 401 for missing authorization header" do
- conn = get(unauthed_conn(), ~p"/v0/users")
-
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "POST /v0/users" do
- test "can create unprivileged user with password" do
- params = %{
- "email" => "new-user@test",
- "role" => "unprivileged",
- "password" => "test1234test",
- "password_confirmation" => "test1234test"
- }
-
- conn =
- post(authed_conn(), ~p"/v0/users", user: params)
- |> doc()
-
- assert json_response(conn, 201)["data"]["role"] == "unprivileged"
- end
-
- test "can create unprivileged user" do
- params = %{"email" => "new-user@test", "role" => "unprivileged"}
-
- conn =
- post(authed_conn(), ~p"/v0/users", user: params)
- |> doc(example_description: "Provision an unprivileged OpenID User")
-
- assert json_response(conn, 201)["data"]["role"] == "unprivileged"
- end
-
- test "can create admin user" do
- params = %{"email" => "new-user@test", "role" => "admin"}
-
- conn =
- post(authed_conn(), ~p"/v0/users", user: params)
- |> doc(example_description: "Provision an admin OpenID User")
-
- assert json_response(conn, 201)["data"]["role"] == "admin"
- end
-
- test "renders user when data is valid" do
- conn = post(authed_conn(), ~p"/v0/users", user: @create_attrs)
- assert %{"id" => id} = json_response(conn, 201)["data"]
-
- conn = get(conn, ~p"/v0/users/#{id}")
-
- assert %{
- "id" => ^id
- } = json_response(conn, 200)["data"]
- end
-
- test "renders errors when data is invalid" do
- conn =
- post(authed_conn(), ~p"/v0/users", user: @invalid_attrs)
- |> doc(example_description: "Error due to invalid parameters")
-
- assert json_response(conn, 422)["errors"] == %{
- "password" => ["should be at least 12 character(s)"],
- "password_confirmation" => ["can't be blank"]
- }
- end
-
- test "renders 401 for missing authorization header" do
- conn = post(unauthed_conn(), ~p"/v0/users", user: %{})
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "PUT /v0/users/:id_or_email" do
- test "returns user that was updated via email" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- conn =
- put(authed_conn(), ~p"/v0/users/#{user.email}", user: %{})
- |> doc(example_description: "Update by email")
-
- assert json_response(conn, 200)["data"]["id"] == user.id
- end
-
- test "returns user that was updated via id" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- conn =
- put(authed_conn(), ~p"/v0/users/#{user}", user: %{})
- |> doc(example_description: "Update by ID")
-
- assert json_response(conn, 200)["data"]["id"] == user.id
- end
-
- test "can update other unprivileged user's password" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
- old_hash = user.password_hash
- params = %{"password" => "update-password", "password_confirmation" => "update-password"}
- conn = put(authed_conn(), ~p"/v0/users/#{user}", user: params)
-
- assert Users.fetch_user_by_id!(json_response(conn, 200)["data"]["id"]).password_hash !=
- old_hash
- end
-
- test "can update other unprivileged user's role" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
- params = %{role: :admin}
- conn = put(authed_conn(), ~p"/v0/users/#{user}", user: params)
- assert json_response(conn, 200)["data"]["role"] == "admin"
- end
-
- test "can update other unprivileged user's email" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
- params = %{email: "new-email@test"}
- conn = put(authed_conn(), ~p"/v0/users/#{user}", user: params)
- assert json_response(conn, 200)["data"]["email"] == "new-email@test"
- end
-
- test "can update other admin user's password" do
- user = UsersFixtures.create_user_with_role(:admin)
- old_hash = user.password_hash
- params = %{"password" => "update-password", "password_confirmation" => "update-password"}
- conn = put(authed_conn(), ~p"/v0/users/#{user}", user: params)
-
- assert Users.fetch_user_by_id!(json_response(conn, 200)["data"]["id"]).password_hash !=
- old_hash
- end
-
- test "can update other admin user's role" do
- user = UsersFixtures.create_user_with_role(:admin)
- params = %{role: :unprivileged}
- conn = put(authed_conn(), ~p"/v0/users/#{user}", user: params)
- assert json_response(conn, 200)["data"]["role"] == "unprivileged"
- end
-
- test "can update other admin user's email" do
- user = UsersFixtures.create_user_with_role(:admin)
- params = %{email: "new-email@test"}
- conn = put(authed_conn(), ~p"/v0/users/#{user}", user: params)
- assert json_response(conn, 200)["data"]["email"] == "new-email@test"
- end
-
- test "can not update own role" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
-
- conn = put(conn, ~p"/v0/users/#{user}", user: %{role: :unprivileged})
-
- assert json_response(conn, 422)["errors"] == %{
- "role" => ["You cannot change your own role"]
- }
- end
-
- test "renders user when data is valid" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
- conn = put(conn, ~p"/v0/users/#{user}", user: @update_attrs)
- assert @update_attrs = json_response(conn, 200)["data"]
-
- conn = get(conn, ~p"/v0/users/#{user}")
- assert @update_attrs = json_response(conn, 200)["data"]
- end
-
- test "renders errors when data is invalid" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
- conn = put(conn, ~p"/v0/users/#{user}", user: @invalid_attrs)
-
- assert json_response(conn, 422)["errors"] == %{
- "password" => ["should be at least 12 character(s)"],
- "password_confirmation" => ["can't be blank"]
- }
- end
-
- test "renders 404 for user not found" do
- conn = put(authed_conn(), ~p"/v0/users/#{Ecto.UUID.generate()}", user: %{})
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = put(unauthed_conn(), ~p"/v0/users/invalid", user: %{})
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "GET /v0/users/:id" do
- test "gets user by id" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
-
- conn = get(conn, ~p"/v0/users/#{user}")
-
- assert json_response(conn, 200)["data"]["id"] == user.id
- end
-
- test "gets user by email" do
- conn = authed_conn()
- {:user, user} = conn.private.guardian_default_resource.actor
-
- conn =
- get(conn, ~p"/v0/users/#{user.email}")
- |> doc(example_description: "An email can be used instead of ID.")
-
- assert json_response(conn, 200)["data"]["id"] == user.id
- end
-
- test "renders 404 for user not found" do
- conn = get(authed_conn(), ~p"/v0/users/003da73d-2dd9-4492-8136-3282843545e8")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = get(unauthed_conn(), ~p"/v0/users/invalid")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-
- describe "DELETE /v0/users/:id" do
- test "deletes user by id" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- conn =
- delete(authed_conn(), ~p"/v0/users/#{user}")
- |> doc()
-
- assert response(conn, 204)
-
- conn = get(conn, ~p"/v0/users/#{user}")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "deletes user by email" do
- user = UsersFixtures.create_user_with_role(:unprivileged)
-
- conn =
- delete(authed_conn(), ~p"/v0/users/#{user.email}")
- |> doc(example_description: "An email can be used instead of ID.")
-
- assert response(conn, 204)
-
- conn = get(conn, ~p"/v0/users/#{user}")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 404 for user not found" do
- conn = delete(authed_conn(), ~p"/v0/users/003da73d-2dd9-4492-8136-3282843545e8")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "renders 401 for missing authorization header" do
- conn = delete(unauthed_conn(), ~p"/v0/users/invalid")
- assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"}
- end
- end
-end
diff --git a/apps/web/test/web/controllers/page_controller_test.exs b/apps/web/test/web/controllers/page_controller_test.exs
new file mode 100644
index 000000000..aed8bec9b
--- /dev/null
+++ b/apps/web/test/web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule Web.PageControllerTest do
+ use Web.ConnCase
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, ~p"/")
+ assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ end
+end
diff --git a/apps/web/test/web/controllers/user_controller_test.exs b/apps/web/test/web/controllers/user_controller_test.exs
deleted file mode 100644
index 762ad29a8..000000000
--- a/apps/web/test/web/controllers/user_controller_test.exs
+++ /dev/null
@@ -1,42 +0,0 @@
-defmodule Web.UserControllerTest do
- use Web.ConnCase, async: true
- alias Domain.UsersFixtures
-
- describe "delete/2" do
- test "deletes the admin user if there is at least one additional admin", %{
- admin_user: user,
- admin_conn: conn
- } do
- UsersFixtures.create_user_with_role(:admin)
-
- conn = delete(conn, ~p"/user")
- assert redirected_to(conn) == ~p"/"
-
- refute Repo.get(Domain.Users.User, user.id)
- end
-
- test "returns 404 when user is already deleted", %{admin_user: user, admin_conn: conn} do
- UsersFixtures.create_user_with_role(:admin)
-
- Repo.delete!(user)
-
- conn = delete(conn, ~p"/user")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
-
- test "returns error if the last admin is deleted", %{admin_conn: conn} do
- conn = delete(conn, ~p"/user")
- assert json_response(conn, 422) == %{"error" => "Can't delete the last admin user."}
- end
-
- test "returns error for unauthorized users", %{unauthed_conn: conn} do
- conn = delete(conn, ~p"/user")
- assert redirected_to(conn) == ~p"/"
- end
-
- test "returns error for unprivileged users", %{unprivileged_conn: conn} do
- conn = delete(conn, ~p"/user")
- assert json_response(conn, 404) == %{"error" => "not_found"}
- end
- end
-end
diff --git a/apps/web/test/web/header_helpers_test.exs b/apps/web/test/web/header_helpers_test.exs
deleted file mode 100644
index b2d394749..000000000
--- a/apps/web/test/web/header_helpers_test.exs
+++ /dev/null
@@ -1,42 +0,0 @@
-defmodule Web.HeaderHelpersTest do
- use ExUnit.Case, async: true
- import Web.HeaderHelpers
-
- describe "remote_ip_opts/0" do
- test "returns an empty proxies list for remote_ip/2" do
- Domain.Config.put_env_override(:web, :external_trusted_proxies, [])
-
- assert remote_ip_opts() == [
- headers: ["x-forwarded-for"],
- proxies: [],
- clients: ["172.28.0.0/16"]
- ]
- end
-
- test "returns a list of options for remote_ip/2 with ipv4 proxies" do
- Domain.Config.put_env_override(:web, :external_trusted_proxies, [
- %Postgrex.INET{address: {127, 0, 0, 1}, netmask: nil},
- %Postgrex.INET{address: {10, 10, 10, 0}, netmask: 16}
- ])
-
- assert remote_ip_opts() == [
- headers: ["x-forwarded-for"],
- proxies: ["127.0.0.1", "10.10.10.0/16"],
- clients: ["172.28.0.0/16"]
- ]
- end
-
- test "returns a list of options for remote_ip/2 with ipv6 proxies" do
- Domain.Config.put_env_override(:web, :external_trusted_proxies, [
- %Postgrex.INET{address: {1, 0, 0, 0, 0, 0, 0, 0}, netmask: 106},
- %Postgrex.INET{address: {1, 1, 1, 1, 1, 1, 1, 1}, netmask: nil}
- ])
-
- assert remote_ip_opts() == [
- headers: ["x-forwarded-for"],
- proxies: ["1::/106", "1:1:1:1:1:1:1:1"],
- clients: ["172.28.0.0/16"]
- ]
- end
- end
-end
diff --git a/apps/web/test/web/html_authentication_test.exs b/apps/web/test/web/html_authentication_test.exs
deleted file mode 100644
index 6654346ca..000000000
--- a/apps/web/test/web/html_authentication_test.exs
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule Web.HTMLAuthenticationTest do
- use Web.ConnCase, async: true
-
- alias Web.Auth.HTML.Authentication
-
- describe "authenticate/2" do
- setup :create_user
-
- @success {:ok, %{}}
- @error {:error, :invalid_credentials}
-
- test "authenticates user with valid credentials", %{user: user} do
- assert @success = Authentication.authenticate(user, "password1234")
- end
-
- test "returns error for missing user" do
- assert @error = Authentication.authenticate(nil, "password1234")
- end
-
- test "returns error for missing password", %{user: user} do
- assert @error = Authentication.authenticate(user, nil)
- end
-
- test "returns error for incorrect password", %{user: user} do
- assert @error = Authentication.authenticate(user, "incorrect password")
- end
- end
-end
diff --git a/apps/web/test/web/layout_view_test.exs b/apps/web/test/web/layout_view_test.exs
deleted file mode 100644
index 55775b07d..000000000
--- a/apps/web/test/web/layout_view_test.exs
+++ /dev/null
@@ -1,16 +0,0 @@
-defmodule Web.SidebarComponentTest do
- use Web.ConnCase, async: true
-
- alias Web.SidebarComponent
-
- describe "nav_class/2" do
- test "it computes nav class for account route" do
- assert SidebarComponent.nav_class("/account/something", "/account") ==
- "is-active has-icon"
- end
-
- test "it defaults to has-icon" do
- assert SidebarComponent.nav_class("/bar", "/foo") == "has-icon"
- end
- end
-end
diff --git a/apps/web/test/web/live/connectivity_check_live/index_test.exs b/apps/web/test/web/live/connectivity_check_live/index_test.exs
deleted file mode 100644
index 349b35fce..000000000
--- a/apps/web/test/web/live/connectivity_check_live/index_test.exs
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Web.ConnectivityCheckLive.IndexTest do
- use Web.ConnCase, async: true
-
- describe "authenticated/connectivity_checks list" do
- setup :create_connectivity_checks
-
- test "show connectivity checks", %{
- admin_conn: conn,
- connectivity_checks: connectivity_checks
- } do
- path = ~p"/diagnostics/connectivity_checks"
- {:ok, _view, html} = live(conn, path)
-
- for connectivity_check <- connectivity_checks do
- assert html =~ DateTime.to_iso8601(connectivity_check.inserted_at)
- end
- end
- end
-
- describe "unauthenticated/connectivity_checks list" do
- test "mount redirects to session path", %{unauthed_conn: conn} do
- path = ~p"/diagnostics/connectivity_checks"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-end
diff --git a/apps/web/test/web/live/device_live/admin/index_test.exs b/apps/web/test/web/live/device_live/admin/index_test.exs
deleted file mode 100644
index a39823411..000000000
--- a/apps/web/test/web/live/device_live/admin/index_test.exs
+++ /dev/null
@@ -1,49 +0,0 @@
-defmodule Web.DeviceLive.Admin.IndexTest do
- use Web.ConnCase, async: true
-
- describe "authenticated/device list" do
- setup :create_devices
-
- test "includes the device details in the list", %{admin_conn: conn, devices: devices} do
- path = ~p"/devices"
- {:ok, _view, html} = live(conn, path)
-
- assert html =~ "Latest Handshake"
-
- for device <- devices do
- assert html =~ device.name
- end
- end
-
- test "includes the user in the list", %{admin_conn: conn, devices: devices} do
- path = ~p"/devices"
- {:ok, _view, html} = live(conn, path)
-
- assert html =~ "User"
-
- devices = Domain.Repo.preload(devices, :user)
-
- for device <- devices do
- assert html =~ device.user.email
- assert html =~ ~s[href="/users/#{device.user.id}"]
- end
- end
- end
-
- describe "authenticated but user deleted" do
- test "redirects to not authorized", %{admin_conn: conn} do
- path = ~p"/devices"
- clear_users()
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-
- describe "unauthenticated" do
- test "mount redirects to session path", %{unauthed_conn: conn} do
- path = ~p"/devices"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-end
diff --git a/apps/web/test/web/live/device_live/admin/show_test.exs b/apps/web/test/web/live/device_live/admin/show_test.exs
deleted file mode 100644
index 34eefa14c..000000000
--- a/apps/web/test/web/live/device_live/admin/show_test.exs
+++ /dev/null
@@ -1,53 +0,0 @@
-defmodule Web.DeviceLive.Admin.ShowTest do
- use Web.ConnCase, async: true
-
- describe "unauthenticated" do
- setup :create_device
-
- @tag :unauthed
- test "mount redirects to session path", %{unauthed_conn: conn, device: device} do
- path = ~p"/devices/#{device}"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-
- describe "authenticated" do
- setup :create_device
-
- test "includes the device details", %{admin_conn: conn, device: device} do
- path = ~p"/devices/#{device}"
- {:ok, _view, html} = live(conn, path)
-
- assert html =~ device.name
- assert html =~ "Latest Handshake"
- end
-
- test "deletes the device", %{admin_conn: conn, device: device} do
- path = ~p"/devices/#{device}"
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("#delete-device-button")
- |> render_click()
-
- {new_path, _flash} = assert_redirect(view)
- assert new_path == ~p"/devices"
- end
- end
-
- # XXX: Revisit this when RBAC is more fleshed out. Admins can now view other admins' devices.
- # describe "authenticated as other user" do
- # setup [:create_device, :create_other_user_device]
- #
- # test "mount redirects to session path", %{
- # admin_conn: conn,
- # device: _device,
- # other_device: other_device
- # } do
- # path = Routes.device_admin_show_path(conn, :show, other_device)
- # expected_path = Routes.auth_path(conn, :request)
- # assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- # end
- # end
-end
diff --git a/apps/web/test/web/live/device_live/unprivileged/index_test.exs b/apps/web/test/web/live/device_live/unprivileged/index_test.exs
deleted file mode 100644
index cc91d03f8..000000000
--- a/apps/web/test/web/live/device_live/unprivileged/index_test.exs
+++ /dev/null
@@ -1,125 +0,0 @@
-defmodule Web.DeviceLive.Unprivileged.IndexTest do
- use Web.ConnCase, async: true
-
- describe "authenticated/device list" do
- test "includes the device name in the list", %{
- unprivileged_user: user,
- unprivileged_conn: conn
- } do
- {:ok, devices: devices} = create_devices(user_id: user.id)
-
- path = ~p"/user_devices"
- {:ok, _view, html} = live(conn, path)
-
- for device <- devices do
- assert html =~ device.name
- end
- end
- end
-
- describe "authenticated but user deleted" do
- test "redirects to not authorized", %{admin_conn: conn} do
- path = ~p"/devices"
- clear_users()
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-
- describe "authenticated device management disabled" do
- setup do
- Domain.Config.put_config!(:allow_unprivileged_device_management, false)
- :ok
- end
-
- test "prevents navigating to /user_devices/new", %{unprivileged_conn: conn} do
- path = ~p"/user_devices/new"
- expected_path = ~p"/"
-
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-
- describe "authenticated device configuration disabled" do
- setup do
- Domain.Config.put_config!(:allow_unprivileged_device_configuration, false)
- :ok
- end
-
- @tag fields: ~w(
- use_default_allowed_ips
- allowed_ips
- use_default_dns
- dns
- use_default_endpoint
- endpoint
- use_default_mtu
- mtu
- use_default_persistent_keepalive
- persistent_keepalive
- ipv4
- ipv6
- )
- test "hides the customization fields", %{fields: fields, unprivileged_conn: conn} do
- path = ~p"/user_devices/new"
- {:ok, _view, html} = live(conn, path)
-
- for field <- fields do
- refute html =~ "device[#{field}]"
- end
- end
-
- @tag fields: ~w(
- name
- description
- public_key
- preshared_key
- )
- test "renders the needed fields", %{fields: fields, unprivileged_conn: conn} do
- path = ~p"/user_devices/new"
- {:ok, _view, html} = live(conn, path)
-
- for field <- fields do
- assert html =~ "device[#{field}]"
- end
- end
- end
-
- describe "authenticated/creates device" do
- test "shows new form", %{unprivileged_conn: conn} do
- path = ~p"/user_devices"
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("a", "Add Device")
- |> render_click()
-
- assert_patch(view, ~p"/user_devices/new")
- end
-
- test "creates device", %{unprivileged_conn: conn} do
- path = ~p"/user_devices/new"
- {:ok, view, _html} = live(conn, path)
-
- new_view =
- view
- |> element("#create-device")
- |> render_submit(%{
- "device" => %{
- "public_key" => "8IkpsAXiqhqNdc9PJS76YeJjig4lyTBaf8Rm7gTApXk=",
- "name" => "test-tunnel"
- }
- })
-
- assert new_view =~ "Device added!"
- end
- end
-
- describe "unauthenticated" do
- test "mount redirects to session path", %{unauthed_conn: conn} do
- path = ~p"/user_devices"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-end
diff --git a/apps/web/test/web/live/device_live/unprivileged/show_test.exs b/apps/web/test/web/live/device_live/unprivileged/show_test.exs
deleted file mode 100644
index 6e8a12e6c..000000000
--- a/apps/web/test/web/live/device_live/unprivileged/show_test.exs
+++ /dev/null
@@ -1,54 +0,0 @@
-defmodule Web.DeviceLive.Unprivileged.ShowTest do
- use Web.ConnCase, async: true
-
- describe "unauthenticated" do
- setup :create_device
-
- @tag :unauthed
- test "mount redirects to session path", %{unauthed_conn: conn, device: device} do
- path = ~p"/devices/#{device}"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-
- describe "authenticated" do
- setup :create_unprivileged_device
-
- test "includes the device details", %{unprivileged_conn: conn, device: device} do
- path = ~p"/user_devices/#{device}"
- {:ok, _view, html} = live(conn, path)
-
- assert html =~ device.name
- assert html =~ "Latest Handshake"
- end
-
- test "deletes the device", %{unprivileged_conn: conn, device: device} do
- path = ~p"/user_devices/#{device}"
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("#delete-device-button")
- |> render_click()
-
- {new_path, _flash} = assert_redirect(view)
- assert new_path == ~p"/user_devices"
- end
- end
-
- describe "authenticated; device management disabled" do
- test "prevents deleting a device", %{
- unprivileged_user: user,
- unprivileged_conn: conn
- } do
- {:ok, device: device} = create_device(user: user)
-
- Domain.Config.put_config!(:allow_unprivileged_device_management, false)
-
- path = ~p"/user_devices/#{device}"
- expected_path = ~p"/"
-
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-end
diff --git a/apps/web/test/web/live/mfa_live/auth_test.exs b/apps/web/test/web/live/mfa_live/auth_test.exs
deleted file mode 100644
index 48785d3f3..000000000
--- a/apps/web/test/web/live/mfa_live/auth_test.exs
+++ /dev/null
@@ -1,85 +0,0 @@
-defmodule Web.MFALive.AuthTest do
- use Web.ConnCase, async: true
- alias Domain.MFAFixtures
-
- setup %{admin_user: admin} do
- method = MFAFixtures.create_totp_method(user: admin)
-
- %{method: method}
- end
-
- test "redirect request with mfa required", %{admin_conn: conn, method: method} do
- path = ~p"/rules"
-
- {:error, {:redirect, %{to: redirected_to}}} =
- live(Plug.Conn.put_session(conn, :logged_in_at, DateTime.utc_now()), path)
-
- assert redirected_to =~ "/mfa/auth/#{method.id}"
- end
-
- describe "auth" do
- test "fails with invalid code", %{admin_conn: conn, method: method} do
- path = ~p"/mfa/auth/#{method.id}"
-
- {:ok, view, _html} = live(conn, path)
-
- assert render_submit(view, :verify, %{code: "ABCXYZ"}) =~ "is-danger"
- end
-
- test "redirects with good code", %{admin_conn: conn, method: method} do
- method = MFAFixtures.rotate_totp_method_key(method)
-
- path = ~p"/mfa/auth/#{method.id}"
-
- {:ok, view, _html} = live(conn, path)
-
- code = method.payload["secret"] |> Base.decode64!() |> NimbleTOTP.verification_code()
- render_submit(view, :verify, %{code: code})
-
- assert_redirect(view)
- end
-
- test "navigates to other methods", %{admin_conn: conn, method: method} do
- path = ~p"/mfa/auth/#{method.id}"
-
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("a[href=\"/mfa/types\"]")
- |> render_click()
-
- assert_redirect(view, "/mfa/types")
- end
- end
-
- describe "types" do
- setup %{admin_user: admin} do
- MFAFixtures.create_totp_method(user: admin, name: "Test 1")
- method = MFAFixtures.create_totp_method(user: admin, name: "Test 2")
-
- %{another_method: method}
- end
-
- test "displays all methods", %{admin_conn: conn} do
- path = ~p"/mfa/types"
-
- {:ok, _view, html} = live(conn, path)
-
- assert html =~ "Test Default"
- assert html =~ "Test 1"
- assert html =~ "Test 2"
- end
-
- test "navigates to selected method", %{admin_conn: conn, another_method: method} do
- path = ~p"/mfa/types"
-
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("a", "Test 2")
- |> render_click()
-
- assert_redirect(view, "/mfa/auth/#{method.id}")
- end
- end
-end
diff --git a/apps/web/test/web/live/notifications_live/badge_test.exs b/apps/web/test/web/live/notifications_live/badge_test.exs
deleted file mode 100644
index cdc8758a6..000000000
--- a/apps/web/test/web/live/notifications_live/badge_test.exs
+++ /dev/null
@@ -1,72 +0,0 @@
-defmodule Web.NotificationsLive.BadgeTest do
- @moduledoc """
- Test notifications badge.
- """
- # async: true causes intermittent failures...
- use Web.ConnCase, async: false
- alias Domain.Notifications
-
- setup tags do
- # Pass the pid to the Notifications views
- pid = start_supervised!(Notifications)
- conn = put_session(tags[:admin_conn], :notifications_pid, pid)
- {:ok, test_pid: pid, admin_conn: conn}
- end
-
- setup [:create_notifications]
-
- test "badge has no notifications", %{admin_conn: conn} do
- {:ok, _view, html} = live_isolated(conn, Web.NotificationsLive.Badge)
-
- assert html =~
- " "
- end
-
- test "badge has 5 notifications after adding 5", %{
- admin_conn: conn,
- test_pid: pid,
- notifications: notifications
- } do
- for notification <- notifications do
- Notifications.add(pid, notification)
- end
-
- {:ok, _view, html} = live_isolated(conn, Web.NotificationsLive.Badge)
-
- assert html =~ " 5 "
- end
-
- test "badge has 3 notifications after adding 5 and clearing 2", %{
- admin_conn: conn,
- test_pid: pid,
- notifications: notifications
- } do
- for notification <- notifications do
- Notifications.add(pid, notification)
- end
-
- Notifications.clear_at(pid, 0)
- Notifications.clear_at(pid, 1)
-
- {:ok, _view, html} = live_isolated(conn, Web.NotificationsLive.Badge)
-
- assert html =~ " 3 "
- end
-
- test "badge has 0 notifications after clearing all", %{
- admin_conn: conn,
- test_pid: pid,
- notifications: notifications
- } do
- for notification <- notifications do
- Notifications.add(pid, notification)
- end
-
- Notifications.clear_all(pid)
-
- {:ok, _view, html} = live_isolated(conn, Web.NotificationsLive.Badge)
-
- assert html =~
- " "
- end
-end
diff --git a/apps/web/test/web/live/notifications_live/index_test.exs b/apps/web/test/web/live/notifications_live/index_test.exs
deleted file mode 100644
index 8b94e05de..000000000
--- a/apps/web/test/web/live/notifications_live/index_test.exs
+++ /dev/null
@@ -1,75 +0,0 @@
-defmodule Web.NotificationsLive.IndexTest do
- @moduledoc """
- Test adding and removing notifications from the notifications table.
- """
- use Web.ConnCase, async: false
- alias Domain.Notifications
-
- setup tags do
- # Pass the pid to the Notifications views
- pid = start_supervised!(Notifications)
- conn = put_session(tags[:admin_conn], :notifications_pid, pid)
- {:ok, test_pid: pid, admin_conn: conn}
- end
-
- setup [:create_notification, :create_notifications]
-
- test "add notification to the table", %{
- test_pid: pid,
- admin_conn: conn,
- notification: notification
- } do
- path = ~p"/notifications"
- Notifications.add(pid, notification)
-
- {:ok, _view, html} = live(conn, path)
-
- assert html =~ notification.user
- end
-
- test "clear notification from the table", %{
- test_pid: pid,
- admin_conn: conn,
- notification: notification
- } do
- path = ~p"/notifications"
- Notifications.add(pid, notification)
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element(".delete")
- |> render_click()
-
- html =
- view
- |> render()
-
- refute html =~ notification.user
- end
-
- test "clear notification from the table at index", %{
- admin_conn: conn,
- test_pid: pid,
- notifications: notifications
- } do
- for notification <- notifications do
- Notifications.add(pid, notification)
- end
-
- path = ~p"/notifications"
- {:ok, view, _html} = live(conn, path)
-
- index = 2
- {notification, _} = List.pop_at(notifications, index)
-
- view
- |> element(".delete[phx-value-index=#{index}")
- |> render_click()
-
- html =
- view
- |> render()
-
- refute html =~ notification.user
- end
-end
diff --git a/apps/web/test/web/live/rule_live/index_test.exs b/apps/web/test/web/live/rule_live/index_test.exs
deleted file mode 100644
index 509fece63..000000000
--- a/apps/web/test/web/live/rule_live/index_test.exs
+++ /dev/null
@@ -1,203 +0,0 @@
-defmodule Web.RuleLive.IndexTest do
- use Web.ConnCase, async: true
-
- describe "allowlist" do
- setup :create_accept_rule
-
- @destination "1.2.3.4"
- @allow_params %{"rule" => %{"action" => "accept", "destination" => @destination}}
-
- test "adds to allowlist", %{admin_conn: conn} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> form("#accept-form")
- |> render_submit(@allow_params)
-
- assert test_view =~ @destination
- end
-
- test "validation fails", %{admin_conn: conn, rule: _rule} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> form("#accept-form")
- |> render_submit(%{
- "rule" => %{
- "destination" => "not a valid destination",
- "action" => "accept"
- }
- })
-
- assert test_view =~ "is invalid"
-
- valid_view =
- view
- |> form("#accept-form")
- |> render_submit(%{
- "rule" => %{
- "destination" => "::1",
- "action" => "accept"
- }
- })
-
- refute valid_view =~ "is invalid"
- end
-
- test "removes from allowlist", %{admin_conn: conn, rule: rule} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> element("a[phx-value-rule_id=#{rule.id}]")
- |> render_click()
-
- refute test_view =~ "#{rule.destination}"
- end
- end
-
- describe "denylist" do
- setup :create_drop_rule
-
- @destination "1.2.3.4"
- @deny_params %{"rule" => %{"action" => "drop", "destination" => @destination}}
-
- test "adds to denylist", %{admin_conn: conn, rule: _rule} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> form("#drop-form")
- |> render_submit(@deny_params)
-
- assert test_view =~ @destination
- end
-
- test "validation fails", %{admin_conn: conn, rule: _rule} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> form("#drop-form")
- |> render_submit(%{
- "rule" => %{
- "destination" => "not a valid destination",
- "action" => "drop"
- }
- })
-
- assert test_view =~ "is invalid"
- end
-
- test "removes from denylist", %{admin_conn: conn, rule: rule} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> element("a[phx-value-rule_id=#{rule.id}]")
- |> render_click()
-
- refute test_view =~ "#{rule.destination}"
- end
- end
-
- describe "adding scoped rules" do
- setup :create_user
-
- @destination "1.2.3.4"
-
- test "adds allow", %{admin_conn: conn, user: user} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- params = %{
- "rule" => %{
- "action" => "accept",
- "destination" => @destination,
- "user_id" => user.id
- }
- }
-
- view |> form("#accept-form") |> render_submit(params)
-
- accept_table =
- view
- |> element("#accept-rules")
- |> render()
-
- assert accept_table =~ @destination
- assert accept_table =~ user.email
- end
-
- test "adds deny", %{admin_conn: conn, user: user} do
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- params = %{
- "rule" => %{
- "action" => "drop",
- "destination" => @destination,
- "user_id" => user.id
- }
- }
-
- view |> form("#drop-form") |> render_submit(params)
-
- drop_table =
- view
- |> element("#drop-rules")
- |> render()
-
- assert drop_table =~ @destination
- assert drop_table =~ user.email
- end
- end
-
- describe "removing scoped rules" do
- @destination "1.2.3.4"
-
- test "removes allow", %{admin_conn: conn} do
- {:ok, rule: rule, user: user} =
- create_rule_with_user(%{action: "accept", destination: @destination})
-
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- view |> element("a[phx-value-rule_id=#{rule.id}]") |> render_click()
-
- accept_table =
- view
- |> element("#accept-rules")
- |> render()
-
- refute accept_table =~ @destination
- refute accept_table =~ user.email
- end
-
- test "removes deny", %{admin_conn: conn} do
- {:ok, rule: rule, user: user} =
- create_rule_with_user(%{action: "drop", destination: @destination})
-
- path = ~p"/rules"
- {:ok, view, _html} = live(conn, path)
-
- view |> element("a[phx-value-rule_id=#{rule.id}]") |> render_click()
-
- drop_table =
- view
- |> element("#drop-rules")
- |> render()
-
- refute drop_table =~ @destination
- refute drop_table =~ user.email
- end
- end
-end
diff --git a/apps/web/test/web/live/setting_live/account_test.exs b/apps/web/test/web/live/setting_live/account_test.exs
deleted file mode 100644
index dd5243fcc..000000000
--- a/apps/web/test/web/live/setting_live/account_test.exs
+++ /dev/null
@@ -1,90 +0,0 @@
-defmodule Web.SettingLive.AccountTest do
- use Web.ConnCase, async: true
-
- alias Domain.{Users, Users.User}
- alias Web.SettingLive.AccountFormComponent
-
- describe "when unauthenticated" do
- test "mount redirects to session path", %{unauthed_conn: conn} do
- path = ~p"/settings/account"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-
- describe "when live_action is show" do
- test "shows account details", %{admin_user: user, admin_conn: conn} do
- path = ~p"/settings/account"
- {:ok, _view, html} = live(conn, path)
-
- user = Users.fetch_user_by_id!(user.id)
-
- assert html =~ "Delete Your Account"
- assert html =~ user.email
- end
- end
-
- describe "when live_action is edit" do
- @valid_params %{"user" => %{"email" => "foobar@test"}}
- @invalid_params %{"user" => %{"email" => "foobar"}}
-
- test "loads the form" do
- assert render_component(AccountFormComponent, id: :test, user: %User{}) =~
- "Change email or enter new password below"
- end
-
- test "saves email when submitted", %{admin_conn: conn} do
- path = ~p"/settings/account/edit"
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("#account-edit")
- |> render_submit(@valid_params)
-
- flash = assert_redirect(view, ~p"/settings/account")
- assert flash["info"] == "Account updated successfully."
- end
-
- test "doesn't allow empty email", %{admin_conn: conn} do
- path = ~p"/settings/account/edit"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> element("#account-edit")
- |> render_submit(%{
- "user" => %{
- "email" => "",
- "password" => "",
- "password_confirmation" => ""
- }
- })
-
- refute_redirected(view, ~p"/settings/account")
- assert test_view =~ "can't be blank"
- end
-
- test "renders validation errors", %{admin_conn: conn} do
- path = ~p"/settings/account/edit"
- {:ok, view, _html} = live(conn, path)
-
- test_view =
- view
- |> element("#account-edit")
- |> render_submit(@invalid_params)
-
- assert test_view =~ "is invalid email address"
- end
-
- test "closes modal", %{admin_conn: conn} do
- path = ~p"/settings/account/edit"
- {:ok, view, _html} = live(conn, path)
-
- view
- |> element("button[aria-label=close]")
- |> render_click()
-
- assert_patch(view, ~p"/settings/account")
- end
- end
-end
diff --git a/apps/web/test/web/live/setting_live/client_defaults_test.exs b/apps/web/test/web/live/setting_live/client_defaults_test.exs
deleted file mode 100644
index f4a032f1d..000000000
--- a/apps/web/test/web/live/setting_live/client_defaults_test.exs
+++ /dev/null
@@ -1,206 +0,0 @@
-defmodule Web.SettingLive.ClientDefaultsTest do
- use Web.ConnCase, async: true
-
- alias Domain.Config
-
- describe "authenticated/client_defaults" do
- @valid_allowed_ips %{
- "configuration" => %{"default_client_allowed_ips" => ["1.1.1.1"]}
- }
- @valid_dns %{
- "configuration" => %{"default_client_dns" => ["1.1.1.1"]}
- }
- @valid_endpoint %{
- "configuration" => %{"default_client_endpoint" => "1.1.1.1"}
- }
- @valid_persistent_keepalive %{
- "configuration" => %{"default_client_persistent_keepalive" => "1"}
- }
- @valid_mtu %{
- "configuration" => %{"default_client_mtu" => "1000"}
- }
-
- @invalid_persistent_keepalive %{
- "configuration" => %{"default_client_persistent_keepalive" => "-1"}
- }
- @invalid_mtu %{
- "configuration" => %{"default_client_mtu" => "0"}
- }
-
- setup %{admin_conn: conn} do
- path = ~p"/settings/client_defaults"
- {:ok, view, html} = live(conn, path)
-
- %{conn: conn, html: html, view: view}
- end
-
- test "renders current configuration", %{html: html} do
- for allowed_ips <- Config.fetch_config!(:default_client_allowed_ips) do
- assert html =~ to_string(allowed_ips)
- end
-
- for dns <- Config.fetch_config!(:default_client_dns) do
- assert html =~ to_string(dns)
- end
-
- assert html =~ """
- id="client_defaults_form_component_default_client_endpoint"\
- """
-
- assert html =~ """
- id="client_defaults_form_component_default_client_persistent_keepalive"\
- """
- end
-
- test "updates default client allowed_ips", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@valid_allowed_ips)
-
- refute test_view =~ "is invalid"
-
- assert test_view =~ """
- \
- """
- end
-
- test "updates default client dns", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@valid_dns)
-
- refute test_view =~ "is invalid"
-
- assert test_view =~ """
- \
- """
- end
-
- test "updates default client endpoint", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@valid_endpoint)
-
- refute test_view =~ "is invalid"
-
- assert test_view =~ """
- \
- """
- end
-
- test "blocks overridden default client endpoint" do
- Domain.Config.put_system_env_override(:default_client_endpoint, "1.2.3.4:1234")
-
- {_admin_user, conn} = admin_conn(%{})
- {:ok, view, _html} = live(conn, ~p"/settings/client_defaults")
-
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render()
-
- assert Floki.find(test_view, "#client_defaults_form_component_default_client_endpoint") ==
- [
- {"input",
- [
- {"class", "input "},
- {"disabled", "disabled"},
- {"id", "client_defaults_form_component_default_client_endpoint"},
- {"name", "configuration[default_client_endpoint]"},
- {"placeholder", "firezone.example.com"},
- {"type", "text"},
- {"value", "Set in environment variable DEFAULT_CLIENT_ENDPOINT: 1.2.3.4:1234"}
- ], []}
- ]
- end
-
- test "updates default client persistent_keepalive", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@valid_persistent_keepalive)
-
- refute test_view =~ "is invalid"
-
- assert test_view =~ """
- \
- """
- end
-
- test "updates default client mtu", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@valid_mtu)
-
- refute test_view =~ "is invalid"
-
- assert test_view =~ """
- \
- """
- end
-
- test "prevents invalid allowed_ips", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(%{
- "configuration" => %{"default_client_allowed_ips" => "foobar"}
- })
-
- assert test_view =~ "is invalid"
-
- assert Floki.find(
- test_view,
- "#client_defaults_form_component_default_client_allowed_ips"
- ) == [
- {"textarea",
- [
- {"class", "textarea is-danger"},
- {"id", "client_defaults_form_component_default_client_allowed_ips"},
- {"name", "configuration[default_client_allowed_ips]"},
- {"placeholder", "0.0.0.0/0, ::/0"}
- ], ["\nfoobar"]}
- ]
- end
-
- test "prevents invalid persistent_keepalive", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@invalid_persistent_keepalive)
-
- assert test_view =~ "must be greater than or equal to 0"
-
- assert test_view =~ """
- \
- """
- end
-
- test "prevents invalid mtu", %{view: view} do
- test_view =
- view
- |> element("#client_defaults_form_component")
- |> render_submit(@invalid_mtu)
-
- assert test_view =~ "must be greater than or equal to 576"
-
- assert test_view =~ """
- \
- """
- end
- end
-
- describe "unauthenticated/settings default" do
- @tag :unauthed
- test "mount redirects to session path", %{unauthed_conn: conn} do
- path = ~p"/settings/client_defaults"
- expected_path = ~p"/"
- assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
- end
- end
-end
diff --git a/apps/web/test/web/live/setting_live/customization_test.exs b/apps/web/test/web/live/setting_live/customization_test.exs
deleted file mode 100644
index 6f0f09553..000000000
--- a/apps/web/test/web/live/setting_live/customization_test.exs
+++ /dev/null
@@ -1,86 +0,0 @@
-defmodule Web.SettingLive.CustomizationTest do
- use Web.ConnCase, async: true
-
- describe "logo" do
- setup %{admin_conn: conn} = context do
- Domain.Config.put_config!(:logo, context[:logo])
-
- path = ~p"/settings/customization"
- {:ok, view, html} = live(conn, path)
-
- {:ok, view: view, html: html}
- end
-
- @tag logo: nil
- test "show default", %{html: html} do
- assert html =~ ~s|value="Default" checked|
- end
-
- @tag logo: %{url: "test"}
- test "show url", %{html: html} do
- assert html =~ ~s|value="URL" checked|
- end
-
- @tag logo: %{data: "test", type: "test"}
- test "show upload", %{html: html} do
- assert html =~ ~s|value="Upload" checked|
- end
-
- test "click default radio", %{view: view} do
- assert view
- |> element("input[value=Default]")
- |> render_click() =~ ~s|