Merge pull request #341 from firezone/backlog/150/download_link

backlog/150: Download link
This commit is contained in:
Jamil
2021-12-15 10:14:20 -08:00
committed by GitHub
56 changed files with 1956 additions and 1779 deletions

View File

@@ -27,4 +27,4 @@ page=$(curl -L -i -vvv -k https://localhost)
echo $page
echo "Testing for sign in button"
echo $page | grep "Sign in"
echo $page | grep '<button class="button" type="submit">Sign In</button>'

View File

@@ -3,6 +3,11 @@ defmodule FzCommon.FzString do
Utility functions for working with Strings.
"""
def sanitize_filename(str) when is_binary(str) do
str
|> String.replace(~r/[^a-zA-Z0-9]+/, "_")
end
def to_boolean(str) when is_binary(str) do
as_bool(String.downcase(str))
end

View File

@@ -3,6 +3,12 @@ defmodule FzCommon.FzStringTest do
alias FzCommon.FzString
describe "sanitize_filename/1" do
test "santizes sequential spaces" do
assert "Factory_Device" == FzString.sanitize_filename("Factory Device")
end
end
describe "to_boolean/1" do
test "converts to true" do
assert true == FzString.to_boolean("True")

View File

@@ -12,3 +12,8 @@ nav.navbar {
aside.aside {
overflow-y: auto;
}
pre {
background-color: $interface-000;
color: $interface-600;
}

View File

@@ -3,7 +3,10 @@
// its own CSS file.
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
@@ -12,9 +15,33 @@ import "@fontsource/fira-sans"
// Import dependencies
//
import "phoenix_html"
import {Socket} from "phoenix"
import {Socket, Presence} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import Hooks from "./hooks.js"
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,
user_agent: window.navigator.userAgent
})
// Presence
const presence = new Presence(notificationChannel)
// LiveView setup
const csrfToken = document
@@ -31,8 +58,41 @@ const liveSocket = new LiveSocket(
}
)
/* XXX: Refactor this into a LiveView. */
const sessionConnect = function (pres) {
let tbody = document.getElementById("sessions-table-body")
let rows = ""
pres.list((user_id, {metas: metas}) => {
if (tbody) {
metas.forEach(meta =>
rows +=
`<tr>
<td>${FormatTimestamp(meta.online_at)}</td>
<td>${FormatTimestamp(meta.last_signed_in_at)}</td>
<td>${meta.remote_ip}</td>
<td>${meta.user_agent}</td>
</tr>`
)
}
})
if (tbody && rows.length > 0) {
tbody.innerHTML = rows
}
}
// uncomment to connect if there are any LiveViews on the page
liveSocket.connect()
userSocket.connect()
// function to receive session updates
presence.onSync(() => sessionConnect(presence))
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()
@@ -40,7 +100,6 @@ liveSocket.connect()
window.liveSocket = liveSocket
// Notification dismiss
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {

View File

@@ -1,4 +1,5 @@
import moment from "moment"
import hljs from "highlight.js"
import {FormatTimestamp} from "./util.js"
const QRCode = require('qrcode')
@@ -19,12 +20,45 @@ const renderQrCode = function () {
}
}
/* 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 dropdown = document.getElementById(button.dataset.target)
document.addEventListener("click", e => {
let ancestor = e.target
do {
if (ancestor == button) return
ancestor = ancestor.parentNode
} while(ancestor)
dropdown.classList.remove("is-active")
})
button.addEventListener("click", e => {
dropdown.classList.toggle("is-active")
})
}
const highlightCode = function () {
hljs.highlightAll()
}
const formatTimestamp = function () {
let timestamp = this.el.dataset.timestamp
this.el.innerHTML = moment(timestamp).format("dddd, MMMM Do YYYY, h:mm:ss a z")
let t = this.el.dataset.timestamp
this.el.innerHTML = FormatTimestamp(t)
}
let Hooks = {}
Hooks.HighlightCode = {
mounted: highlightCode,
updated: highlightCode
}
Hooks.QrCode = {
mounted: renderQrCode,
updated: renderQrCode
@@ -33,5 +67,9 @@ Hooks.FormatTimestamp = {
mounted: formatTimestamp,
updated: formatTimestamp
}
Hooks.ToggleDropdown = {
mounted: toggleDropdown,
updated: toggleDropdown
}
export default Hooks

View File

@@ -0,0 +1,7 @@
import moment from "moment"
const FormatTimestamp = function (timestamp) {
return moment(timestamp).format("dddd, MMMM Do YYYY, h:mm:ss a z")
}
export { FormatTimestamp }

View File

@@ -44,18 +44,27 @@ $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-200;
$text-light: $interface-300;
$text-strong: $interface-100;
$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 */
$primary: $accent-500;
/* 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;

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
"buffer": "^6.0.3",
"buffer-from": "^1.1.1",
"glob": "^7.1.7",
"highlight.js": "^11.3.1",
"moment": "^2.29.1",
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
@@ -27,7 +28,9 @@
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"@creativebulma/bulma-tooltip": "^1.2.0",
"@fontsource/fira-mono": "^4.5.0",
"@fontsource/fira-sans": "^4.5.0",
"@fontsource/open-sans": "^4.5.2",
"@fortawesome/fontawesome-free": "^5.15.3",
"@mdi/font": "^6.5.95",
"admin-one-bulma-dashboard": "file:local_modules/admin-one-bulma-dashboard",

View File

@@ -0,0 +1,13 @@
<svg width="332" height="187" viewBox="0 0 332 187" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_712_107)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.2377 53C85.1003 69.5361 64.5377 106.185 71.794 120.983C56.8827 99.5955 77.1987 81.2158 65.2377 53Z" fill="#EF7A30"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.2521 84.8288C89.2352 93.2609 73.6506 113.018 81.9473 118.075C90.3493 123.197 89.3985 101.911 101 107.371C88.9206 103.263 93.9315 128.607 78.9644 125.234C61.836 121.374 83.1374 94.1419 76.2521 84.8288Z" fill="#7F3900"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 113.701C25.7272 94.0085 59.4085 125.609 75.3027 127.584C95.743 130.123 90.2982 106.5 100.529 107.38C91.1018 108.516 96.7606 133.222 75.6838 133.985C52.7304 134.817 29.83 99.8369 1.00004 113.701H1Z" fill="#331700"/>
<path d="M111.96 134H127.38V127.88H123.78V109.46H135.24V127.88H131.64V134H147V127.88H141.48V103.34H123.78V100.88C123.78 98.24 126.12 97.28 128.88 97.28C131.22 97.28 133.41 97.46 135.24 97.67C135.99 98.54 137.07 99.08 138.3 99.08C140.64 99.08 142.44 97.22 142.44 94.94C142.44 92.66 140.64 90.86 138.3 90.86C136.92 90.86 135.72 91.52 134.97 92.54C132.45 91.82 130.35 91.28 127.38 91.28C122.7 91.28 117.54 92.72 117.54 99.56V103.34H111.96V109.46H117.54V127.88H111.96V134ZM145.033 134H166.993V127.94H158.413V118.04C158.413 112.46 161.353 108.98 168.913 108.98V115.58H175.633V104.24C174.073 103.4 171.673 102.62 168.613 102.62C163.513 102.62 159.463 104.99 157.933 108.26V103.34H145.033V109.4H151.513V127.94H145.033V134ZM192.085 134.72C196.825 134.72 200.485 132.8 203.065 129.98L198.925 125.6C197.005 127.7 194.845 128.6 192.145 128.6C187.885 128.6 185.125 125.81 184.345 121.34H205.165C205.405 119.78 205.405 118.22 205.405 117.14C205.405 107.18 199.465 102.62 192.085 102.62C183.145 102.62 177.445 108.68 177.445 118.64C177.445 128.36 182.965 134.72 192.085 134.72ZM192.085 108.68C195.265 108.68 198.205 110.72 198.625 115.46H184.345C185.065 111.2 187.465 108.68 192.085 108.68ZM209.798 134H234.938V127.82H218.618L234.998 108.44V103.34H210.938V109.52H226.178L209.798 128.78V134ZM253.371 134.72C262.491 134.72 268.071 128.54 268.071 118.7C268.071 108.86 262.491 102.62 253.371 102.62C244.311 102.62 238.671 108.92 238.671 118.7C238.671 128.6 244.311 134.72 253.371 134.72ZM253.371 128.12C248.391 128.12 245.631 124.4 245.631 118.7C245.631 112.94 248.391 109.16 253.371 109.16C258.471 109.16 261.111 113 261.111 118.7C261.111 124.4 258.411 128.12 253.371 128.12ZM270.903 134H277.803V116.6C277.803 110.9 280.923 109.16 284.583 109.16C288.063 109.16 290.943 111.08 290.943 116.72V134H297.843V115.28C297.843 105.86 292.623 102.62 286.503 102.62C282.633 102.62 279.333 104.33 277.803 106.28V103.34H270.903V134ZM315.976 134.72C320.716 134.72 324.376 132.8 326.956 129.98L322.816 125.6C320.896 127.7 318.736 128.6 316.036 128.6C311.776 128.6 309.016 125.81 308.236 121.34H329.056C329.296 119.78 329.296 118.22 329.296 117.14C329.296 107.18 323.356 102.62 315.976 102.62C307.036 102.62 301.336 108.68 301.336 118.64C301.336 128.36 306.856 134.72 315.976 134.72ZM315.976 108.68C319.156 108.68 322.096 110.72 322.516 115.46H308.236C308.956 111.2 311.356 108.68 315.976 108.68Z" fill="#281303"/>
</g>
<defs>
<clipPath id="clip0_712_107">
<rect width="332" height="187" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -28,6 +28,7 @@ defmodule FzHttp.Application do
FzHttp.Vault,
FzHttpWeb.Endpoint,
{Phoenix.PubSub, name: FzHttp.PubSub},
FzHttpWeb.Presence,
FzHttp.ConnectivityCheckService
]
end
@@ -38,7 +39,8 @@ defmodule FzHttp.Application do
FzHttp.Repo,
FzHttp.Vault,
FzHttpWeb.Endpoint,
{Phoenix.PubSub, name: FzHttp.PubSub}
{Phoenix.PubSub, name: FzHttp.PubSub},
FzHttpWeb.Presence
]
end
end

View File

@@ -20,6 +20,10 @@ defmodule FzHttp.Devices do
Repo.all(from d in Device, where: d.user_id == ^user_id)
end
def count(user_id) do
Repo.one(from d in Device, where: d.user_id == ^user_id, select: count())
end
def get_device!(id), do: Repo.get!(Device, id)
def create_device(attrs \\ %{}) do
@@ -92,4 +96,41 @@ defmodule FzHttp.Devices do
|> Enum.map(fn field -> {field, Device.field(changeset, field)} end)
|> Map.new()
end
def as_config(device) do
wireguard_port = Application.fetch_env!(:fz_vpn, :wireguard_port)
"""
[Interface]
PrivateKey = #{device.private_key}
Address = #{ipv4_address(device)}/32, #{ipv6_address(device)}/128
#{dns_servers_config(device)}
[Peer]
PublicKey = #{device.server_public_key}
AllowedIPs = #{allowed_ips(device)}
Endpoint = #{endpoint(device)}:#{wireguard_port}
"""
end
defp dns_servers_config(device) when is_struct(device) do
dns_servers = dns_servers(device)
if dns_servers_empty?(dns_servers) do
""
else
"DNS = #{dns_servers}"
end
end
defp dns_servers_empty?(nil), do: true
defp dns_servers_empty?(dns_servers) when is_binary(dns_servers) do
len =
dns_servers
|> String.trim()
|> String.length()
len == 0
end
end

View File

@@ -56,6 +56,7 @@ defmodule FzHttpWeb do
quote do
use Phoenix.LiveView, layout: {FzHttpWeb.LayoutView, "live.html"}
import FzHttpWeb.LiveHelpers
alias Phoenix.LiveView.JS
@events_module Application.compile_env!(:fz_http, :events_module)

View File

@@ -0,0 +1,57 @@
defmodule FzHttpWeb.NotificationChannel do
@moduledoc """
Handles dispatching realtime notifications to users' browser sessions.
"""
use FzHttpWeb, :channel
alias FzHttp.Users
alias FzHttpWeb.Presence
@impl Phoenix.Channel
def join("notification:session", %{"user_agent" => user_agent, "token" => token}, socket) do
case Phoenix.Token.verify(socket, "channel auth", token, max_age: 86_400) do
{:ok, user_id} ->
socket =
socket
|> assign(:current_user, Users.get_user!(user_id))
|> assign(:user_agent, user_agent)
send(self(), :after_join)
{:ok,
socket
|> assign(:current_user, Users.get_user!(user_id))}
{:error, _} ->
{: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,
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 = [Integer.to_string(socket.assigns.current_user.id)]
Presence.list(socket)
|> Map.take(ids_to_show)
end
end

View File

@@ -0,0 +1,11 @@
defmodule FzHttpWeb.Presence do
@moduledoc """
Provides presence tracking to channels and processes.
See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
docs for more details.
"""
use Phoenix.Presence,
otp_app: :fz_http,
pubsub_server: FzHttp.PubSub
end

View File

@@ -1,8 +1,11 @@
defmodule FzHttpWeb.UserSocket do
use Phoenix.Socket
alias FzHttp.Users
## Channels
# channel "room:*", FzHttpWeb.RoomChannel
channel "notification:session", FzHttpWeb.NotificationChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
@@ -15,8 +18,19 @@ defmodule FzHttpWeb.UserSocket do
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(_params, socket, _connect_info) do
{:ok, socket}
def connect(%{"token" => token}, socket, connect_info) do
ip = get_ip_address(connect_info)
case Phoenix.Token.verify(socket, "user auth", token, max_age: 86_400) do
{:ok, user_id} ->
{:ok,
socket
|> assign(:current_user, Users.get_user!(user_id))
|> assign(:remote_ip, ip)}
{:error, _} ->
:error
end
end
# Socket id's are topics that allow you to identify all sockets for a given user:
@@ -31,4 +45,35 @@ defmodule FzHttpWeb.UserSocket do
# Returning `nil` makes this socket anonymous.
# def id(_socket), do: nil
def id(socket), do: "user_socket:#{socket.assigns.current_user.id}"
defp get_ip_address(%{peer_data: %{address: address}}) do
convert_ip(address)
address
|> Tuple.to_list()
|> Enum.join(".")
end
defp get_ip_address(%{x_headers: headers_list}) do
header = Enum.find(headers_list, fn {key, _val} -> key == "x-real-ip" end)
case header do
{_key, value} -> value
_ -> nil
end
end
# IPv4
defp convert_ip({_, _, _, _} = address) do
address
|> Tuple.to_list()
|> Enum.join(".")
end
# IPv6
defp convert_ip(address) do
address
|> Tuple.to_list()
|> Enum.join(":")
end
end

View File

@@ -4,10 +4,26 @@ defmodule FzHttpWeb.DeviceController do
"""
use FzHttpWeb, :controller
import FzCommon.FzString, only: [sanitize_filename: 1]
alias FzHttp.Devices
plug :redirect_unauthenticated
def index(conn, _params) do
conn
|> redirect(to: Routes.device_index_path(conn, :index))
end
def download_config(conn, %{"id" => device_id}) do
device = Devices.get_device!(device_id)
filename = "#{sanitize_filename(device.name)}.conf"
content_type = "text/plain"
conn
|> send_download(
{:binary, Devices.as_config(device)},
filename: filename,
content_type: content_type
)
end
end

View File

@@ -7,7 +7,7 @@ defmodule FzHttpWeb.Endpoint do
end
socket "/socket", FzHttpWeb.UserSocket,
websocket: true,
websocket: [connect_info: [:peer_data, :x_headers]],
longpoll: false
socket "/live", Phoenix.LiveView.Socket,

View File

@@ -1,6 +1,6 @@
<div>
<.form let={f} for={@changeset} id="account-edit" phx-target={@myself} phx-submit="save">
<div class="content">
<div class="block">
<p>Change email or enter new password below.</p>
</div>
@@ -37,7 +37,7 @@
<hr>
<div class="content">
<div class="block">
<p>Enter your current password to make these changes.</p>
</div>

View File

@@ -1,11 +1,11 @@
<%= if @live_action == :edit do %>
<%= live_modal(
FzHttpWeb.AccountLive.FormComponent,
return_to: Routes.account_show_path(@socket, :show),
title: "Edit Account",
id: "user-#{@current_user.id}",
user: @current_user,
action: @live_action) %>
FzHttpWeb.AccountLive.FormComponent,
return_to: Routes.account_show_path(@socket, :show),
title: "Edit Account",
id: "user-#{@current_user.id}",
user: @current_user,
action: @live_action) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
@@ -13,44 +13,63 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">
Details
</p>
</header>
<div class="level">
<div class="level-left">
<h4 class="title is-4">Details</h4>
</div>
<div class="card-content">
<div class="content">
<%= render FzHttpWeb.SharedView, "user_details.html", user: @current_user %>
</div>
</div>
<footer class="card-footer">
<div class="field is-grouped card-footer-item">
<p class="control">
<%= live_patch(to: Routes.account_show_path(@socket, :edit), class: "button is-primary") do %>
<span class="icon is-small">
<i class="fas fa-edit"></i>
</span>
<span>Change Email or Password</span>
<% end %>
</p>
<p class="control">
<%# This is purposefully a synchronous form in order to easily clear the session %>
<%= form_for @changeset, Routes.user_path(@socket, :delete), [id: "delete-account", method: :delete], fn _f -> %>
<%= submit(class: "button is-danger", data: [confirm: "Are you sure?"]) do %>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Your Account</span>
<% end %>
<% end %>
</p>
</div>
</footer>
</div>
<div class="level-right">
<%= live_patch(to: Routes.account_show_path(@socket, :edit), class: "button") do %>
<span class="icon is-small">
<i class="mdi mdi-pencil"></i>
</span>
<span>Change Email or Password</span>
<% end %>
</div>
</div>
<%= render FzHttpWeb.SharedView, "user_details.html", user: @current_user %>
</section>
<section class="section is-main-section">
<h4 class="title is-4">
Active Sessions
</h4>
<div class="block">
<p>
Your active Firezone web sessions. Each row corresponds to an open browser
tab connected to Firezone.
</p>
</div>
<div class="block">
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<thead>
<th>Came Online</th>
<th>Last Signed In</th>
<th>Remote IP</th>
<th>User Agent</th>
</thead>
<tbody id="sessions-table-body">
</tbody>
</table>
</div>
</section>
<section class="section is-main-section">
<h4 class="title is-4">
Danger Zone
</h4>
<%# This is purposefully a synchronous form in order to easily clear the session %>
<%= form_for @changeset, Routes.user_path(@socket, :delete), [id: "delete-account", method: :delete], fn _f -> %>
<%= submit(class: "button is-danger", data: [confirm: "Are you sure?"]) do %>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Your Account</span>
<% end %>
<% end %>
</section>

View File

@@ -3,12 +3,14 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="content">
<div class="block">
<p>
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.
</p>
</div>
<div class="block">
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<thead>
<tr>

View File

@@ -11,7 +11,7 @@ defmodule FzHttpWeb.ConnectivityCheckLive.Index do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)
|> assign(:page_title, "Connectivity Checks")}
|> assign(:page_title, "WAN Connectivity Checks")}
end
defp load_data(_params, socket) do

View File

@@ -3,11 +3,11 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="content">
<div class="block">
<%= render FzHttpWeb.SharedView, "devices_table.html", devices: @devices, socket: @socket %>
</div>
<button class="button is-primary" phx-click="create_device">
<button class="button" phx-click="create_device">
Add Device
</button>
</section>

View File

@@ -11,7 +11,7 @@ defmodule FzHttpWeb.DeviceLive.Index do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)
|> assign(:page_title, "All Devices")}
|> assign(:page_title, "Devices")}
end
def handle_event("create_device", _params, socket) do

View File

@@ -8,104 +8,125 @@
action: @live_action) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Devices -> #{@page_title}" %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="tile is-ancestor is-flex-wrap-wrap">
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">Details</p>
</header>
<div class="card-content">
<div class="content">
<dl>
<dt><strong>User</strong></dt>
<dd><%= link(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %></dd>
<dt><strong>Name</strong></dt>
<dd><%= @device.name %></dd>
<dt><strong>Interface IP</strong></dt>
<dd>
<%= FzHttp.Devices.ipv4_address(@device) %>,
<%= FzHttp.Devices.ipv6_address(@device) %>
</dd>
<dt><strong>Allowed IPs</strong></dt>
<dd><%= @allowed_ips %></dd>
<dt><strong>DNS Servers</strong></dt>
<dd><%= @dns_servers || "None" %></dd>
<dt><strong>Endpoint</strong></dt>
<dd><%= @endpoint %></dd>
<dt><strong>Public key</strong></dt>
<dd class="code"><%= @device.public_key %></dd>
<dt><strong>Private key</strong></dt>
<dd class="code"><%= @device.private_key %></dd>
<dt><strong>Server public key</strong></dt>
<dd class="code"><%= @device.server_public_key %></dd>
</dl>
</div>
</div>
<footer class="card-footer">
<div class="field is-grouped card-footer-item">
<p class="control">
<%= live_patch(to: Routes.device_show_path(@socket, :edit, @device), class: "button is-primary") do %>
<span class="icon is-small">
<i class="fas fa-edit"></i>
</span>
<span>Edit</span>
<% end %>
</p>
<p class="control">
<button class="button is-danger"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will remove all data associated with this device.">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete</span>
</button>
</p>
</div>
</footer>
</div>
<div class="level">
<div class="level-left">
<h4 class="title is-4">Details</h4>
</div>
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">Config</p>
</header>
<div class="card-content">
<h6 class="is-6 title">
Add the following to your WireGuard configuration file:
</h6>
<pre><code id="wg-conf">
[Interface]
PrivateKey = <%= @device.private_key %>
Address = <%= FzHttp.Devices.ipv4_address(@device) %>/32, <%= FzHttp.Devices.ipv6_address(@device) %>/128
<%= @dns_servers %>
<div class="level-right">
<%= live_patch(to: Routes.device_show_path(@socket, :edit, @device), class: "button") do %>
<span class="icon">
<i class="mdi mdi-pencil"></i>
</span>
<span>Edit</span>
<% end %>
</div>
</div>
[Peer]
PublicKey = <%= @device.server_public_key %>
AllowedIPs = <%= @allowed_ips %>
Endpoint = <%= @endpoint %>:<%= @wireguard_port %></code></pre>
<hr>
<h6 class="is-6 title">
Or scan the QR code with your mobile phone:
</h6>
<div class="has-text-centered">
<canvas id="qr-canvas" phx-hook="QrCode"></canvas>
</div>
</div>
</div>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<tbody>
<tr>
<td><strong>User</strong></td>
<td><%= link(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %></td>
</tr>
<tr>
<td><strong>Name</strong></td>
<td><%= @device.name %></td>
</tr>
<tr>
<td><strong>Interface IP</strong></td>
<td>
<%= FzHttp.Devices.ipv4_address(@device) %>,
<%= FzHttp.Devices.ipv6_address(@device) %>
</td>
</tr>
<tr>
<td><strong>Allowed IPs</strong></td>
<td><%= @allowed_ips %></td>
</tr>
<tr>
<td><strong>DNS Servers</strong></td>
<td><%= @dns_servers || "None" %></td>
</tr>
<tr>
<td><strong>Endpoint</strong></td>
<td><%= @endpoint %></td>
</tr>
<tr>
<td><strong>Public key</strong></td>
<td class="code"><%= @device.public_key %></td>
</tr>
<tr>
<td><strong>Private key</strong></td>
<td class="code"><%= @device.private_key %></td>
</tr>
<tr>
<td><strong>Server public key</strong></td>
<td class="code"><%= @device.server_public_key %></td>
</tr>
</tbody>
</table>
</section>
<section class="section is-main-section">
<div class="level">
<div class="level-left">
<h4 class="title is-4">WireGuard Configuration</h4>
</div>
<div class="level-right">
<%= link(
to: Routes.device_path(@socket, :download_config, @device),
class: "button") do %>
<span class="icon is-small">
<i class="mdi mdi-download"></i>
</span>
<span>Download Configuration</span>
<% end %>
</div>
</div>
<div class="block">
Install the
<a href="https://www.wireguard.com/install/">
official WireGuard client
</a>
for your device, then use the below WireGuard configuration to connect.
</div>
<div class="columns">
<div class="column">
<pre><code id="wg-conf" class="language-toml"><%= @config %></code></pre>
</div>
<div class="column has-text-centered">
<canvas id="qr-canvas" phx-hook="QrCode">
Generating QR code...
</canvas>
</div>
</div>
</section>
<section class="section is-main-section">
<h4 class="title is-4">
Danger Zone
</h4>
<button class="button is-danger"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data.">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Device <%= @device.name %></span>
</button>
</section>

View File

@@ -6,19 +6,19 @@ defmodule FzHttpWeb.DeviceLive.Show do
alias FzHttp.{Devices, Users}
@impl true
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)}
end
@impl true
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
@impl Phoenix.LiveView
def handle_event("delete_device", %{"device_id" => device_id}, socket) do
device = Devices.get_device!(device_id)
@@ -46,39 +46,18 @@ defmodule FzHttpWeb.DeviceLive.Show do
device = Devices.get_device!(id)
if device.user_id == socket.assigns.current_user.id do
assign(
socket,
socket
|> assign(
device: device,
user: Users.get_user!(device.user_id),
page_title: device.name,
allowed_ips: Devices.allowed_ips(device),
dns_servers: dns_servers(device),
dns_servers: Devices.dns_servers(device),
endpoint: Devices.endpoint(device),
wireguard_port: Application.fetch_env!(:fz_vpn, :wireguard_port)
config: Devices.as_config(device)
)
else
not_authorized(socket)
end
end
defp dns_servers(device) when is_struct(device) do
dns_servers = Devices.dns_servers(device)
if dns_servers_empty?(dns_servers) do
""
else
"DNS = #{dns_servers}"
end
end
defp dns_servers_empty?(nil), do: true
defp dns_servers_empty?(dns_servers) when is_binary(dns_servers) do
len =
dns_servers
|> String.trim()
|> String.length()
len == 0
end
end

View File

@@ -6,22 +6,24 @@ defmodule FzHttpWeb.ModalComponent do
@impl true
def render(assigns) do
~L"""
<div id="<%= @myself %>" class="modal is-active"
~H"""
<div
id={@myself}
class="modal is-active"
phx-capture-click="close"
phx-window-keydown="close"
phx-key="escape"
phx-target="<%= @myself %>"
phx-target={@myself}
phx-page-loading>
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title"><%= @opts[:title] %></p>
<button class="delete" aria-label="close" phx-click="close" phx-target="<%= @myself %>"></button>
<button class="delete" aria-label="close" phx-click="close" phx-target={@myself}></button>
</header>
<section class="modal-card-body">
<div class="content">
<%= live_component(@socket, @component, @opts) %>
<div class="block">
<%= live_component(@component, @opts) %>
</div>
</section>
<footer class="modal-card-foot">

View File

@@ -1,7 +1,7 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<div class="content">
<div class="block">
<p>
<span class="icon">
<i class="mdi mdi-information-outline"></i>
@@ -14,17 +14,17 @@
<div class="tile is-ancestor">
<div class="tile is-parent">
<%= live_component(
@socket,
FzHttpWeb.RuleLive.RuleListComponent,
title: "Allowlist",
header_icon: "mdi mdi-arrow-decision-outline",
id: :allowlist,
current_user: @current_user) %>
</div>
<div class="tile is-parent">
<%= live_component(
@socket,
FzHttpWeb.RuleLive.RuleListComponent,
title: "Denylist",
header_icon: "mdi mdi-alert-octagon",
id: :denylist,
current_user: @current_user) %>
</div>

View File

@@ -8,6 +8,6 @@ defmodule FzHttpWeb.RuleLive.Index do
{:ok,
socket
|> assign_defaults(params, session)
|> assign(:page_title, "Rules")}
|> assign(:page_title, "Egress Rules")}
end
end

View File

@@ -8,7 +8,7 @@ defmodule FzHttpWeb.RuleLive.RuleListComponent do
@events_module Application.compile_env!(:fz_http, :events_module)
@impl true
@impl Phoenix.LiveComponent
def update(assigns, socket) do
{:ok,
socket
@@ -34,7 +34,7 @@ defmodule FzHttpWeb.RuleLive.RuleListComponent do
end
end
@impl true
@impl Phoenix.LiveComponent
def handle_event("delete_rule", %{"rule_id" => rule_id}, socket) do
rule = Rules.get_rule!(rule_id)

View File

@@ -1,6 +1,9 @@
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title"><%= @title %></p>
<p class="card-header-title">
<span class="icon"><i class={@header_icon}></i></span>
<%= @title %>
</p>
</header>
<div class="card-content">
<.form let={f} for={@changeset} id={"#{@action}-form"} phx-target={@myself} phx-submit="add_rule">

View File

@@ -3,47 +3,37 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="tile is-ancestor is-flex-wrap-wrap">
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">Device</p>
</header>
<div class="card-content">
<div class="content">
<%= live_component(
@socket,
FzHttpWeb.SettingLive.FormComponent,
label_text: "Allowed IPs",
placeholder: nil,
changeset: @changesets["default.device.allowed_ips"],
help_text: @help_texts.allowed_ips,
id: :allowed_ips_form_component) %>
<div class="block">
<p>
Configure Firezone-wide default settings.
</p>
</div>
<hr>
<h4 class="title is-4">Device Defaults</h4>
<%= live_component(
@socket,
FzHttpWeb.SettingLive.FormComponent,
label_text: "DNS Servers",
placeholder: nil,
changeset: @changesets["default.device.dns_servers"],
help_text: @help_texts.dns_servers,
id: :dns_servers_form_component) %>
<div class="block">
<%= live_component(
FzHttpWeb.SettingLive.FormComponent,
label_text: "Allowed IPs",
placeholder: nil,
changeset: @changesets["default.device.allowed_ips"],
help_text: @help_texts.allowed_ips,
id: :allowed_ips_form_component) %>
<hr>
<%= live_component(
FzHttpWeb.SettingLive.FormComponent,
label_text: "DNS Servers",
placeholder: nil,
changeset: @changesets["default.device.dns_servers"],
help_text: @help_texts.dns_servers,
id: :dns_servers_form_component) %>
<%= live_component(
@socket,
FzHttpWeb.SettingLive.FormComponent,
label_text: "Endpoint",
placeholder: @endpoint_placeholder,
changeset: @changesets["default.device.endpoint"],
help_text: @help_texts.endpoint,
id: :endpoint_form_component) %>
</div>
</div>
</div>
</div>
<%= live_component(
FzHttpWeb.SettingLive.FormComponent,
label_text: "Endpoint",
placeholder: @endpoint_placeholder,
changeset: @changesets["default.device.endpoint"],
help_text: @help_texts.endpoint,
id: :endpoint_form_component) %>
</div>
</section>

View File

@@ -1,4 +1,4 @@
<div>
<div class="block">
<.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-change="change" phx-submit="save">
<div class="field">
<%= label f, :value, @label_text, class: "label" %>

View File

@@ -0,0 +1,13 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<h4 class="title is-4">Security Settings</h4>
<div class="block">
<p>
Manage security-related settings.
</p>
</div>
</section>

View File

@@ -0,0 +1,14 @@
defmodule FzHttpWeb.SettingLive.Security do
@moduledoc """
Manages security LiveView
"""
use FzHttpWeb, :live_view
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign(:page_title, "Security Settings")
|> assign_defaults(params, session)}
end
end

View File

@@ -1,7 +1,7 @@
<div>
<.form let={f} for={@changeset} id="user-form" phx-target={@myself} phx-submit="save">
<%= if @action == :edit do %>
<div class="content">
<div class="block">
<p>Change user email or enter new password below.</p>
</div>
<% end %>

View File

@@ -12,13 +12,15 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="content">
<div class="block">
<table class="table is-hoverable is-bordered is-striped is-fullwidth">
<thead>
<tr>
<th class="is-6">Email</th>
<th class="is-6">Devices</th>
<th>Email</th>
<th>Devices</th>
<th>Last Signed In</th>
<th>Created</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
@@ -33,13 +35,23 @@
phx-hook="FormatTimestamp">
</td>
<td id={"user-#{user.id}-inserted-at"}
data-timestamp={user.inserted_at}
phx-hook="FormatTimestamp">
</td>
<td id={"user-#{user.id}-updated-at"}
data-timestamp={user.updated_at}
phx-hook="FormatTimestamp">
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%= live_patch(to: Routes.user_index_path(@socket, :new), class: "button is-primary") do %>
<%= live_patch(to: Routes.user_index_path(@socket, :new), class: "button") do %>
Add User
<% end %>
</section>

View File

@@ -8,75 +8,55 @@
action: @live_action) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "User #{@user.email}" %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Users -> #{@user.email}" %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="tile is-ancestor is-flex-wrap-wrap">
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">
Details
</p>
</header>
<div class="card-content">
<div class="content">
<%= render FzHttpWeb.SharedView, "user_details.html", user: @user %>
</div>
</div>
<footer class="card-footer">
<div class="field is-grouped card-footer-item">
<p class="control">
<%= live_patch(to: Routes.user_show_path(@socket, :edit, @user), class: "button is-primary") do %>
<span class="icon is-small">
<i class="fas fa-edit"></i>
</span>
<span>Change Email or Password</span>
<% end %>
</p>
<p class="control">
<button
class="button is-danger"
data-confirm="Are you sure? This will permanently delete this user, all associated devices and instantly drop any active VPN sessions associated to this user."
phx-click="delete_user"
phx-value-user_id={@user.id}>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete User</span>
</button>
</p>
</div>
</footer>
</div>
<div class="level">
<div class="level-left">
<h4 class="title is-4">Details</h4>
</div>
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">
Devices
</p>
</header>
<div class="card-content">
<%= if length(@devices) > 0 do %>
<%= render FzHttpWeb.SharedView, "devices_table.html", devices: @devices, socket: @socket %>
<% else %>
No devices.
<% end %>
</div>
<footer class="card-footer">
<div class="field is-grouped card-footer-item">
<p class="control">
<button class="button is-primary" phx-value-user_id={@user.id} phx-click="create_device">
Add Device
</button>
</p>
</div>
</footer>
</div>
<div class="level-right">
<%= live_patch(to: Routes.user_show_path(@socket, :edit, @user), class: "button") do %>
<span class="icon is-small">
<i class="mdi mdi-pencil"></i>
</span>
<span>Change Email or Password</span>
<% end %>
</div>
</div>
<%= render FzHttpWeb.SharedView, "user_details.html", user: @user %>
</section>
<section class="section is-main-section">
<h4 class="title is-4">Devices</h4>
<div class="block">
<%= if length(@devices) > 0 do %>
<%= render FzHttpWeb.SharedView, "devices_table.html", devices: @devices, socket: @socket %>
<% else %>
No devices.
<% end %>
</div>
<button class="button" phx-value-user_id={@user.id} phx-click="create_device">
Add Device
</button>
</section>
<section class="section is-main-section">
<h4 class="title is-4">Danger Zone</h4>
<button
class="button is-danger"
data-confirm="Are you sure? This will permanently delete this user, all associated devices and instantly drop any active VPN sessions associated to this user."
phx-click="delete_user"
phx-value-user_id={@user.id}>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete User</span>
</button>
</section>

View File

@@ -22,6 +22,7 @@ defmodule FzHttpWeb.Router do
pipe_through :browser
get "/", DeviceController, :index
get "/devices/:id/dl", DeviceController, :download_config
resources "/session", SessionController, only: [:new, :create, :delete], singleton: true
live "/users", UserLive.Index, :index
@@ -37,10 +38,12 @@ defmodule FzHttpWeb.Router do
live "/settings/default", SettingLive.Default, :default
live "/account", AccountLive.Show, :show
live "/account/edit", AccountLive.Show, :edit
live "/settings/security", SettingLive.Security, :security
live "/connectivity_checks", ConnectivityCheckLive.Index, :index
live "/settings/account", AccountLive.Show, :show
live "/settings/account/edit", AccountLive.Show, :edit
live "/diagnostics/connectivity_checks", ConnectivityCheckLive.Index, :index
get "/sign_in/:token", SessionController, :create
delete "/user", UserController, :delete

View File

@@ -1,5 +1,5 @@
<%= if !is_nil(get_flash(@conn, :info)) or !is_nil(get_flash(@conn, :error)) do %>
<div class="container flash-squeeze">
<div class="block flash-squeeze">
<%= if get_flash(@conn, :info) do %>
<div class="notification is-info">
<button title="Dismiss notification" class="delete"></button>

View File

@@ -15,15 +15,20 @@
</head>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns">
<div class="column is-4 is-offset-4">
<%= @inner_content %>
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen">
<div class="block">
<div class="has-text-centered">
<img src={Routes.static_path(@conn, "/images/logo-text.svg")} alt="firez.one">
</div>
</div>
<%= @inner_content %>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
</html>

View File

@@ -1,10 +1,9 @@
<!DOCTYPE html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title], prefix: "Firezone • " %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}></script>
@@ -17,6 +16,15 @@
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<!-- User Socket -->
<%= tag :meta, name: "user-token", content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %>
<!-- Notification Channel -->
<%= tag :meta, name: "channel-token", content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id) %>
<!-- CSRF -->
<%= csrf_meta_tag() %>
</head>
<body>
<div id="app">
@@ -67,20 +75,20 @@
<p class="menu-label">Configuration</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.user_index_path(@conn, :index), class: nav_class(@conn.request_path, "users")) do %>
<%= link(to: Routes.user_index_path(@conn, :index), class: nav_class(@conn, ~r"/users")) do %>
<span class="icon"><i class="mdi mdi-account-group"></i></span>
<span class="menu-item-label">Users</span>
<% end %>
</li>
<li>
<%= link(to: Routes.device_index_path(@conn, :index), class: nav_class(@conn.request_path, "devices")) do %>
<%= link(to: Routes.device_index_path(@conn, :index), class: nav_class(@conn, ~r"/devices")) do %>
<span class="icon"><i class="mdi mdi-laptop"></i></span>
<span class="menu-item-label">Devices</span>
<% end %>
</li>
<li>
<%= link(to: Routes.rule_index_path(@conn, :index), class: nav_class(@conn.request_path, "rules")) do %>
<span class="icon"><i class="mdi mdi-plus-network"></i></span>
<%= link(to: Routes.rule_index_path(@conn, :index), class: nav_class(@conn, ~r"/rules")) do %>
<span class="icon"><i class="mdi mdi-traffic-light"></i></span>
<span class="menu-item-label">Rules</span>
<% end %>
</li>
@@ -88,24 +96,32 @@
<p class="menu-label">Settings</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.setting_default_path(@conn, :default), class: nav_class(@conn.request_path, "defaults")) do %>
<%= link(to: Routes.setting_default_path(@conn, :default), class: nav_class(@conn, ~r"/settings/default")) do %>
<span class="icon"><i class="mdi mdi-cog"></i></span>
<span class="menu-item-label">Defaults</span>
<% end %>
</li>
<li>
<%= link(to: Routes.account_show_path(@conn, :show), class: nav_class(@conn.request_path, "account")) do %>
<%= link(to: Routes.account_show_path(@conn, :show), class: nav_class(@conn, ~r"/settings/account")) do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span class="menu-item-label">Account</span>
<% end %>
</li>
<!-- XXX: Future settings pane
<li>
<%= link(to: Routes.setting_security_path(@conn, :security), class: nav_class(@conn, ~r"/settings/security")) do %>
<span class="icon"><i class="mdi mdi-lock"></i></span>
<span class="menu-item-label">Security</span>
<% end %>
</li>
-->
</ul>
<p class="menu-label">Diagnostics</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.connectivity_check_index_path(@conn, :index), class: nav_class(@conn.request_path, "connectivity_checks")) do %>
<%= link(to: Routes.connectivity_check_index_path(@conn, :index), class: nav_class(@conn, ~r"/diagnostics/connectivity_checks")) do %>
<span class="icon"><i class="mdi mdi-access-point"></i></span>
<span class="menu-item-label">Connectivity Checks</span>
<span class="menu-item-label">WAN Connectivity</span>
<% end %>
</li>
</ul>
@@ -120,7 +136,7 @@
<div class="level-left">
<div class="level-item">
<%= link(to: "mailto:" <> feedback_recipient()) do %>
Click here to leave feedback
Leave us feedback!
<% end %>
</div>
</div>

View File

@@ -1,10 +1,11 @@
<!-- Use https://admin-one.justboil.me/?style=light-dark#/lock-screen for inspiration -->
<h3 class="title">Sign In</h3>
<h3 class="is-3 title">Sign In</h3>
<hr>
<%= form_for @changeset, Routes.session_path(@conn, :create), fn f -> %>
<%= if assigns[:changeset] && @changeset.action do %>
<div>
<div class="block">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
@@ -31,7 +32,7 @@
<div class="field">
<div class="control">
<%= submit "Sign in", class: "button is-primary" %>
<%= submit "Sign In", class: "button" %>
</div>
</div>
<% end %>

View File

@@ -4,16 +4,20 @@
<th>Name</th>
<th>WireGuard IP</th>
<th>Public key</th>
<th>Created</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<%= for device <- @devices do %>
<tr>
<td>
<%= link(device.name, to: Routes.device_show_path(@socket, :show, device)) %>
<%= live_patch(device.name, to: Routes.device_show_path(@socket, :show, device)) %>
</td>
<td class="code"><%= FzHttp.Devices.ipv4_address(device) %>, <%= FzHttp.Devices.ipv6_address(device) %></td>
<td class="code"><%= device.public_key %></td>
<td id={"device-#{device.id}-inserted-at"} data-timestamp={device.inserted_at} phx-hook="FormatTimestamp">…</td>
<td id={"device-#{device.id}-updated-at"} data-timestamp={device.updated_at} phx-hook="FormatTimestamp">…</td>
</tr>
<% end %>
</tbody>

View File

@@ -1,14 +1,43 @@
<dl>
<dt>
<strong>Email</strong>
</dt>
<dd><%= @user.email %></dd>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<tbody>
<tr>
<td><strong>Email</strong></td>
<td><%= @user.email %></td>
</tr>
<dt><strong>Last signed in</strong></dt>
<dd
id="last-signed-in-at"
data-timestamp={@user.last_signed_in_at}
phx-hook="FormatTimestamp">
</dd>
</dl>
<tr>
<td><strong>Last Signed In</strong></td>
<td
id="last-signed-in-at"
data-timestamp={@user.last_signed_in_at}
phx-hook="FormatTimestamp">
</td>
</tr>
<tr>
<td><strong>Created</strong></td>
<td
id="created-at"
data-timestamp={@user.inserted_at}
phx-hook="FormatTimestamp">
</td>
</tr>
<tr>
<td><strong>Updated</strong></td>
<td
id="created-at"
data-timestamp={@user.updated_at}
phx-hook="FormatTimestamp">
</td>
</tr>
<tr>
<td><strong>Number of Devices</strong></td>
<td><%= FzHttp.Devices.count(@user.id) %></td>
</tr>
</tbody>
</table>

View File

@@ -30,19 +30,15 @@ defmodule FzHttpWeb.LayoutView do
@doc """
Generate class for nav links
"""
def nav_class(request_path, section) do
top_level =
request_path
|> String.split("/", trim: true)
|> List.first("devices")
def nav_class(%{request_path: "/"} = _conn, ~r"devices") do
"is-active has-icon"
end
active =
if top_level == section do
"is-active"
else
""
end
Enum.join([active, "has-icon"], " ")
def nav_class(%{request_path: request_path} = _conn, regex) do
if String.match?(request_path, regex) do
"is-active has-icon"
else
"has-icon"
end
end
end

View File

@@ -67,9 +67,9 @@ defmodule FzHttp.MixProject do
{:inflex, "~> 2.1"},
{:plug, "~> 1.12.1"},
{:postgrex, "~> 0.15.10"},
{:phoenix_html, "~> 3.0.3"},
{:phoenix_html, "~> 3.1.0"},
{:phoenix_live_reload, "~> 1.3", only: :dev},
{:phoenix_live_view, "~> 0.16.3"},
{:phoenix_live_view, "~> 0.17.5"},
{:gettext, "~> 0.18"},
{:jason, "~> 1.2"},
{:telemetry, "~> 0.4.3"},

View File

@@ -62,7 +62,7 @@ defmodule FzHttp.ConnectivityChecksTest do
response_body: "some updated response_body",
response_code: 500,
response_headers: %{"updated" => "response headers"},
url: "https://ping.firez.one/6.6.6"
url: "https://ping-dev.firez.one/6.6.6"
}
assert {:ok, %ConnectivityCheck{} = connectivity_check} =
@@ -71,7 +71,7 @@ defmodule FzHttp.ConnectivityChecksTest do
assert connectivity_check.response_body == "some updated response_body"
assert connectivity_check.response_code == 500
assert connectivity_check.response_headers == %{"updated" => "response headers"}
assert connectivity_check.url == "https://ping.firez.one/6.6.6"
assert connectivity_check.url == "https://ping-dev.firez.one/6.6.6"
end
test "update_connectivity_check/2 with invalid data returns error changeset" do

View File

@@ -0,0 +1,46 @@
defmodule FzHttpWeb.NotificationChannelTest do
use FzHttpWeb.ChannelCase, async: true
alias FzHttp.UsersFixtures
alias FzHttpWeb.NotificationChannel
describe "channel join" do
setup _tags do
user = UsersFixtures.user()
socket =
FzHttpWeb.UserSocket
|> socket(user.id, %{remote_ip: "127.0.0.1"})
%{
user: user,
socket: socket,
token: Phoenix.Token.sign(socket, "channel auth", user.id)
}
end
test "joins channel with valid token", %{token: token, socket: socket, user: user} do
payload = %{
"token" => token,
"user_agent" => "test"
}
{:ok, _, test_socket} =
socket
|> subscribe_and_join(NotificationChannel, "notification:session", payload)
assert test_socket.assigns.current_user.id == user.id
end
test "prevents joining with invalid token", %{token: _token, socket: socket, user: _user} do
payload = %{
"token" => "foobar",
"user_agent" => "test"
}
assert {:error, %{reason: "unauthorized"}} ==
socket
|> subscribe_and_join(NotificationChannel, "notification:session", payload)
end
end
end

View File

@@ -41,7 +41,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do
path = Routes.device_show_path(conn, :show, device)
{:ok, _view, html} = live(conn, path)
assert html =~ "#{device.name}"
assert html =~ "<p class=\"card-header-title\">Details</p>"
assert html =~ "<h4 class=\"title is-4\">Details</h4>"
end
test "opens modal", %{authed_conn: conn, device: device} do
@@ -220,7 +220,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do
{:ok, view, _html} = live(conn, path)
view
|> element("button", "Delete")
|> element("button", "Delete Device #{device.name}")
|> render_click()
_flash = assert_redirected(view, Routes.device_index_path(conn, :index))

View File

@@ -0,0 +1,21 @@
defmodule FzHttpWeb.SettingLive.SecurityTest do
use FzHttpWeb.ConnCase, async: true
describe "authenticated mount" do
test "loads the active sessions table", %{authed_conn: conn} do
path = Routes.setting_security_path(conn, :security)
{:ok, _view, html} = live(conn, path)
assert html =~ "<h4 class=\"title is-4\">Security Settings</h4>"
end
end
describe "unauthenticated mount" do
test "redirects to not authorized", %{unauthed_conn: conn} do
path = Routes.setting_security_path(conn, :security)
expected_path = Routes.session_path(conn, :new)
assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
end
end
end

View File

@@ -9,15 +9,16 @@ defmodule FzHttpWeb.LayoutViewTest do
# import Phoenix.HTML
describe "nav_class/2" do
test "it computes nav class for root route" do
assert LayoutView.nav_class("/", "devices") == "is-active has-icon"
assert LayoutView.nav_class(%{request_path: "/"}, ~r"devices") == "is-active has-icon"
end
test "it computes nav class for account route" do
assert LayoutView.nav_class("/account", "account") == "is-active has-icon"
assert LayoutView.nav_class(%{request_path: "/account"}, ~r"account") ==
"is-active has-icon"
end
test "it defaults to has-icon" do
assert LayoutView.nav_class("Blah", "foo") == " has-icon"
assert LayoutView.nav_class(%{request_path: "Blah"}, ~r"foo") == "has-icon"
end
end
end

View File

@@ -22,7 +22,7 @@ defmodule FzHttpWeb.ChannelCase do
using do
quote do
# Import conveniences for testing with channels
use Phoenix.ChannelTest
import Phoenix.ChannelTest
import FzHttp.TestHelpers
# The default endpoint for testing

View File

@@ -16,7 +16,7 @@ defmodule FzHttp.ConnectivityChecksFixtures do
response_body: "some response_body",
response_code: 142,
response_headers: %{"Content-Type" => "text/plain"},
url: "https://ping.firez.one/0.0.0+git.0.deadbeef0"
url: "https://ping-dev.firez.one/0.0.0+git.0.deadbeef0"
})
|> ConnectivityChecks.create_connectivity_check()

View File

@@ -14,7 +14,7 @@
"db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"},
"earmark_parser": {:hex, :earmark_parser, "1.4.18", "e1b2be73eb08a49fb032a0208bf647380682374a725dfb5b9e510def8397f6f2", [:mix], [], "hexpm", "114a0e85ec3cf9e04b811009e73c206394ffecfcc313e0b346de0d557774ee97"},
"ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"},
"ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"},
"ecto_sql": {:hex, :ecto_sql, "3.7.1", "8de624ef50b2a8540252d8c60506379fbbc2707be1606853df371cf53df5d053", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b42a32e2ce92f64aba5c88617891ab3b0ba34f3f3a503fa20009eae1a401c81"},
@@ -41,11 +41,11 @@
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"},
"phoenix": {:hex, :phoenix, "1.6.4", "bc9a757f0a4eac88e1e3501245a6259e74d30970df8c072836d755608dbc4c7d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b6cb3f31e3ea1049049852703eca794f7afdb0c1dc111d8f166ba032c103a80"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.0.4", "232d41884fe6a9c42d09f48397c175cd6f0d443aaa34c7424da47604201df2e1", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ce17fd3cf815b2ed874114073e743507704b1f5288bb03c304a77458485efc8b"},
"phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.16.4", "5692edd0bac247a9a816eee7394e32e7a764959c7d0cf9190662fc8b0cd24c97", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "754ba49aa2e8601afd4f151492c93eb72df69b0b9856bab17711b8397e43bba0"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.5", "63f52a6f9f6983f04e424586ff897c016ecc5e4f8d1e2c22c2887af1c57215d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5586e6a3d4df71b8214c769d4f5eb8ece2b4001711a7ca0f97323c36958b0e3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},