+ WireGuard configuration for device <%= @device.name %> shown below. + Scan the QR code or copy and paste the configuration into your WireGuard application. +
+diff --git a/apps/fz_http/assets/css/main.scss b/apps/fz_http/assets/css/main.scss index 93aa817c7..a1205ab41 100644 --- a/apps/fz_http/assets/css/main.scss +++ b/apps/fz_http/assets/css/main.scss @@ -17,3 +17,7 @@ pre { background-color: $interface-000; color: $interface-600; } + +.dropdown-menu.is-large { + width: 26rem; +} diff --git a/apps/fz_http/assets/js/auth.js b/apps/fz_http/assets/js/auth.js new file mode 100644 index 000000000..e8f48d83e --- /dev/null +++ b/apps/fz_http/assets/js/auth.js @@ -0,0 +1,19 @@ +import css from "../css/app.scss" + +/* Application fonts */ +import "@fontsource/fira-sans" +import "@fontsource/open-sans" +import "@fontsource/fira-mono" + +import "phoenix_html" + +// Notification dismiss +document.addEventListener('DOMContentLoaded', () => { + (document.querySelectorAll('.notification .delete') || []).forEach(($delete) => { + const $notification = $delete.parentNode + + $delete.addEventListener('click', () => { + $notification.parentNode.removeChild($notification) + }) + }) +}) diff --git a/apps/fz_http/assets/js/device_config.js b/apps/fz_http/assets/js/device_config.js new file mode 100644 index 000000000..7b90b5656 --- /dev/null +++ b/apps/fz_http/assets/js/device_config.js @@ -0,0 +1,6 @@ +import {renderQrCode} from "./qrcode.js" + +window.addEventListener('DOMContentLoaded', () => { + console.log('loaded') + renderQrCode() +}) diff --git a/apps/fz_http/assets/js/hooks.js b/apps/fz_http/assets/js/hooks.js index eb36aff56..d4e12e2b0 100644 --- a/apps/fz_http/assets/js/hooks.js +++ b/apps/fz_http/assets/js/hooks.js @@ -1,47 +1,24 @@ import hljs from "highlight.js" import {FormatTimestamp} from "./util.js" +import {renderQrCode} from "./qrcode.js" -const QRCode = require('qrcode') - -const renderQrCode = function () { - let canvas = document.getElementById('qr-canvas') - let conf = document.getElementById('wg-conf') - - if (canvas && conf) { - QRCode.toCanvas(canvas, conf.innerHTML, { - errorCorrectionLevel: 'H', - margin: 0, - width: 200, - height: 200 - - }, function (error) { - if (error) alert('QRCode Encode Error: ' + error) - }) - } -} - -/* XXX: Sad we have to write custom JS for this. Keep an eye on - * https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html - * in case a toggleClass function is implemented. The toggle() - * function listed there automatically adds display: none which is not - * what we want in this case. - */ const toggleDropdown = function () { - const button = this.el + const button = ctx.el const dropdown = document.getElementById(button.dataset.target) document.addEventListener("click", e => { let ancestor = e.target do { - if (ancestor == button) return + if (ancestor == button || ancestor == dropdown) return ancestor = ancestor.parentNode } while(ancestor) dropdown.classList.remove("is-active") }) + button.addEventListener("click", e => { - dropdown.classList.toggle("is-active") + dropdown.classList.add("is-active") }) } @@ -54,7 +31,20 @@ const formatTimestamp = function () { this.el.innerHTML = FormatTimestamp(t) } +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 @@ -67,9 +57,5 @@ Hooks.FormatTimestamp = { mounted: formatTimestamp, updated: formatTimestamp } -Hooks.ToggleDropdown = { - mounted: toggleDropdown, - updated: toggleDropdown -} export default Hooks diff --git a/apps/fz_http/assets/js/qrcode.js b/apps/fz_http/assets/js/qrcode.js new file mode 100644 index 000000000..54b0f729b --- /dev/null +++ b/apps/fz_http/assets/js/qrcode.js @@ -0,0 +1,20 @@ +const QRCode = require('qrcode') + +const renderQrCode = function () { + let canvas = document.getElementById('qr-canvas') + let conf = document.getElementById('wg-conf') + + if (canvas && conf) { + QRCode.toCanvas(canvas, conf.innerHTML, { + errorCorrectionLevel: 'H', + margin: 0, + width: 200, + height: 200 + + }, function (error) { + if (error) alert('QRCode Encode Error: ' + error) + }) + } +} + +export { renderQrCode } diff --git a/apps/fz_http/assets/webpack.config.js b/apps/fz_http/assets/webpack.config.js index 5bccc2b42..7002b6f7d 100644 --- a/apps/fz_http/assets/webpack.config.js +++ b/apps/fz_http/assets/webpack.config.js @@ -12,16 +12,18 @@ module.exports = (env, options) => ({ ] }, entry: { - './js/app.js': glob.sync('./vendor/**/*.js').concat([ + 'app': glob.sync('./vendor/**/*.js').concat([ // Local JS files to include in the bundle './js/hooks.js', './js/app.js', './node_modules/admin-one-bulma-dashboard/src/js/main.js' - ]) + ]), + 'auth': ['./js/auth.js'], + 'device_config': ['./js/device_config.js'] }, output: { path: path.resolve(__dirname, '../priv/static/js'), - filename: 'app.js', + filename: '[name].js', publicPath: '/js/' }, module: { diff --git a/apps/fz_http/lib/fz_http/devices.ex b/apps/fz_http/lib/fz_http/devices.ex index cc98a278e..fae570914 100644 --- a/apps/fz_http/lib/fz_http/devices.ex +++ b/apps/fz_http/lib/fz_http/devices.ex @@ -4,12 +4,15 @@ defmodule FzHttp.Devices do """ import Ecto.Query, warn: false - alias FzCommon.NameGenerator + alias FzCommon.{FzCrypto, NameGenerator} alias FzHttp.{ConnectivityChecks, Devices.Device, Repo, Settings, Users.User} @ipv4_prefix "10.3.2." @ipv6_prefix "fd00:3:2::" + # Device configs can be viewable for 10 minutes + @config_token_expires_in_sec 600 + def list_devices do Repo.all(Device) end @@ -24,6 +27,15 @@ defmodule FzHttp.Devices do Repo.one(from d in Device, where: d.user_id == ^user_id, select: count()) end + def get_device!(config_token: config_token) do + now = DateTime.utc_now() + + Repo.one!( + from d in Device, + where: d.config_token == ^config_token and d.config_token_expires_at > ^now + ) + end + def get_device!(id), do: Repo.get!(Device, id) def create_device(attrs \\ %{}) do @@ -113,6 +125,17 @@ defmodule FzHttp.Devices do """ end + def create_config_token(device) do + expires_at = DateTime.add(DateTime.utc_now(), @config_token_expires_in_sec, :second) + + config_token_attrs = %{ + config_token: FzCrypto.rand_token(6), + config_token_expires_at: expires_at + } + + update_device(device, config_token_attrs) + end + defp dns_servers_config(device) when is_struct(device) do dns_servers = dns_servers(device) diff --git a/apps/fz_http/lib/fz_http/devices/device.ex b/apps/fz_http/lib/fz_http/devices/device.ex index a95c85435..1ea9ca7f7 100644 --- a/apps/fz_http/lib/fz_http/devices/device.ex +++ b/apps/fz_http/lib/fz_http/devices/device.ex @@ -31,6 +31,8 @@ defmodule FzHttp.Devices.Device do field :remote_ip, EctoNetwork.INET field :address, :integer, read_after_writes: true field :last_seen_at, :utc_datetime_usec + field :config_token, :string + field :config_token_expires_at, :utc_datetime_usec belongs_to :user, User @@ -69,7 +71,9 @@ defmodule FzHttp.Devices.Device do :private_key, :user_id, :name, - :public_key + :public_key, + :config_token, + :config_token_expires_at ]) end diff --git a/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex index b3394a3e6..b4ff91695 100644 --- a/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex +++ b/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex @@ -7,7 +7,7 @@ defmodule FzHttpWeb.DeviceController do import FzCommon.FzString, only: [sanitize_filename: 1] alias FzHttp.Devices - plug :redirect_unauthenticated + plug :redirect_unauthenticated, except: [:config] def index(conn, _params) do conn @@ -16,6 +16,23 @@ defmodule FzHttpWeb.DeviceController do def download_config(conn, %{"id" => device_id}) do device = Devices.get_device!(device_id) + render_download(conn, device) + end + + def download_shared_config(conn, %{"config_token" => config_token}) do + device = Devices.get_device!(config_token: config_token) + render_download(conn, device) + end + + def config(conn, %{"config_token" => config_token}) do + device = Devices.get_device!(config_token: config_token) + + conn + |> put_root_layout({FzHttpWeb.LayoutView, "device_config.html"}) + |> render("config.html", config: Devices.as_config(device), device: device) + end + + defp render_download(conn, device) do filename = "#{sanitize_filename(FzHttpWeb.Endpoint.host())}.conf" content_type = "text/plain" diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex index d277e6b82..87aa7a229 100644 --- a/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex @@ -86,14 +86,72 @@
+ WireGuard configuration for device <%= @device.name %> shown below. + Scan the QR code or copy and paste the configuration into your WireGuard application. +
+<%= @config %>
+