Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

9 changed files with 186 additions and 155 deletions

200
README.md
View file

@ -1,36 +1,7 @@
# Freedive # Freedive
Dive into FreeBSD with `Freedive`! Dive into FreeBSD with `Freedive`!
Freedive emerges as an innovative standalone tool for FreeBSD, Freedive emerges as an innovative standalone tool for FreeBSD, offering a graphical user interface (GUI) that revolutionizes the way systems are managed, enabling both newcomers and experienced users to administer their FreeBSD installations with ease. This GUI, accessible through a mobile-friendly web interface, democratizes the use of FreeBSD by allowing beginners to engage with the operating system without the need to delve into the complexities of the command-line shell. Designed with responsiveness in mind, Freedive's interface adapts seamlessly to various screen sizes, ensuring that system administrators can perform essential tasks from virtually any device, be it a smartphone, tablet, or desktop. The intuitive design of Freedive's web interface lowers the entry barrier for managing FreeBSD systems, making it more approachable for users who may not be familiar with Unix-like environments. By providing a full spectrum of system management capabilities through its GUI, Freedive empowers users to perform tasks ranging from user account management to service configuration, all without writing a single line of shell code. Freedive stands as a testament to the versatility of FreeBSD, extending its appeal beyond the traditional tech-savvy audience to a broader range of users who can now leverage the power of FreeBSD with the convenience of a modern web-based interface.
offering a graphical user interface (GUI) that revolutionizes
the way systems are managed,
enabling both newcomers and experienced users
to administer their FreeBSD installations with ease.
This GUI, accessible through a mobile-friendly web interface,
democratizes the use of FreeBSD by allowing beginners
to engage with the operating system
without the need to delve into the complexities of the command-line shell.
Designed with responsiveness in mind,
Freedive's interface adapts seamlessly to various screen sizes,
ensuring that system administrators can perform essential tasks
from virtually any device, be it a smartphone, tablet, or desktop.
The intuitive design of Freedive's web interface lowers
the barrier to entry for managing FreeBSD systems,
making it more approachable for users
who may not be familiar with Unix-like environments.
By providing a full spectrum of
system management capabilities through its GUI,
Freedive empowers users to perform tasks
ranging from user account management to service configuration,
all without writing a single line of shell code.
Freedive stands as a testament to the versatility of FreeBSD,
extending its appeal beyond the traditional tech-savvy audience
to a broader range of users who can now leverage
the power of FreeBSD
with the convenience of a modern web-based interface.
## Features ## Features
Made by and for users of FreeBSD as their: Made by and for users of FreeBSD as their:
@ -68,24 +39,80 @@ Freedive works in immediate mode:
## Quick Steps ## Quick Steps
### Binaries
For a quick test you can just use the generated FreeBSD pkgs to try Freedive:
* [freedive-0.1.0.pkg](https://cdn.gyptazy.ch/files/amd64/freebsd/freedive/freedive-0.1.0.pkg) [hosted at gyptazy.ch]
### Get pre-built package ### Build & Deploy
#### Requirements
Building Freedive requires some additional packages like gmake, gcc and especially elixir (>= 1.16.0). This also requires you to switch to the latest ports/pkgs branch instead of the quarterly.
Try Freedive on FreeBSD 14.0-RELEASE and newer: This is not needed when you are already using the `latest` repository. However, switching to the latest ports/pkgs branch can be easily done with the following steps:
* amd64: [freedive-0.1.0.pkg](https://cdn.gyptazy.ch/files/amd64/freebsd/freedive/freedive-0.1.0.pkg) [hosted at gyptazy.ch] ```
* arm64: [freedive-0.1.0_arm64.pkg](https://cdn.gyptazy.ch/files/arm64/freebsd/freedive/freedive-0.1.0_arm64.pkg) [hosted at gyptazy.ch] mkdir -p /usr/local/etc/pkg/repos
echo "FreeBSD: { url: \"pkg+http://pkg.freebsd.org/\${ABI}/latest\" }" > /usr/local/etc/pkg/repos/FreeBSD.conf
pkg -y upgrade -f
```
If you are fully running on pkg instead of ports you need to copy a file which is referenced and used by Freedive. If you have
ports active on your system, this step can be skipped - if not, simply run the following commands:
```
mkdir -p /usr/ports/Keywords/
curl -O /usr/ports/Keywords/ https://raw.githubusercontent.com/freebsd/freebsd-ports/main/Keywords/sample.ucl
```
You may also want to setup your own doas account (passwordless) by running:
```
echo "permit nopass <USERNAME> as root" >> /usr/local/etc/doas.conf
```
Within the last step, the needed dependencies can be installed from the pkg repository:
```
pkg install git inotify-tools gmake elixir gcc doas
```
#### Building
Freedive can be simply build by running the following commands:
```
pkg install -y git inotify-tools gmake elixir gcc doas
git clone https://brew.bsd.cafe/hiway/freedive.git
mix setup
```
#### Packaging
If you also want to create and build a distributable `.pkg` file, simply run the following command afterwards:
```
mix package
```
This creates you the file `freedive-0.1.0.pkg` within your build directory.
### Install package ### Usage (compile)
After building, you can directly start Freedive. Freedive can be started by running `mix` or inside `IEx` by executing:
```
mix phx.server
or
iex -S mix phx.server
```
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
> Registering accounts via browser is enabled in dev envronment.
Visit [/users/register](http://localhost:4000/users/register) to create an account.
Then visit [/dev/mailbox](http://localhost:4000/dev/mailbox) to confirm the account.
Now, you can ue these credentials to log in and explore Freedive.
### Usage (pkg)
Another solution directly leads into using the built package. This can be installed by running: Another solution directly leads into using the built package. This can be installed by running:
``` ```
pkg install -U -y freedive-0.1.0.pkg pkg install -U -y freedive-0.1.0.pkg
``` ```
Next, edit the configuration at `/usr/local/etc/freedive/freedive.env` Afterwards, the configuration should be adjusted in `/usr/local/etc/freedive/freedive.env` where the following settings should be defined:
and define following settings:
``` ```
HOST="hostname" HOST="hostname"
@ -93,10 +120,10 @@ BIND="ip-to-bind"
PORT=3443 PORT=3443
``` ```
Ensure that the host is set to the hostname you'll use Ensure that the host is set to whatever hostname you'll use
to access the service from browser. to access the service from browser.
For example, if you bind to the Tailscale/Wireguard IP, For example, if you bind to the Tailscale/Wireguard IP,
use the hostname that points to this IP. use the hostname that'll point to this IP.
Port can be anything suitable in your environmet Port can be anything suitable in your environmet
that's open and accessible from your mobile/laptop. that's open and accessible from your mobile/laptop.
@ -124,100 +151,11 @@ Check logs
- `tail -f /var/log/freedive/freedive.log` - `tail -f /var/log/freedive/freedive.log`
Visit https://hostname:port from your browser and
Visit `https://hostname:port` from your browser and
log in with the account created above. log in with the account created above.
---
## Development
### Requirements
If you have `ports` active on your system, this step can be skipped.
If you have not installed `ports` (the `/usr/ports` directory is missing)
then you need to copy a file which is referenced and used
by Freedive when building its own package.
Run the following commands:
```
mkdir -p /usr/ports/Keywords/
curl -O /usr/ports/Keywords/ https://raw.githubusercontent.com/freebsd/freebsd-ports/main/Keywords/sample.ucl
```
You also want to setup your own doas account (passwordless) by running:
```
echo "permit nopass <USERNAME> as root" >> /usr/local/etc/doas.conf
```
Freedive will only function as expected if `doas` is installed
and configured to allow the user running the server
to execute commands as root without requiring a password.
### Development Environment
Install build tools and depedencies
```
pkg install -y ca_root_nss doas elixir gcc git gmake inotify-tools
```
Get code
```
git clone https://brew.bsd.cafe/hiway/freedive.git
cd freedive
```
Setup dev environment
```
mix setup
```
> It is safe to ignore warning about `mix_freebsd_pkg` requiring Elixir 1.6, it will go away
> when Elixir 1.6 becomes available in quarterly packages.
### Development Server
Freedive dev server can be started by running `mix` or inside `IEx` by executing:
```
mix phx.server
# or
iex -S mix phx.server
```
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
> Note:
> Registering accounts via browser is enabled in dev environment.
Visit [/users/register](http://localhost:4000/users/register) to create an account.
Then visit [/dev/mailbox](http://localhost:4000/dev/mailbox) to confirm the account.
Now, you can ue these credentials to log in and explore Freedive.
### Packaging
Create and build a distributable `.pkg` file that can be installed on target machines.
Run the following command in `freedive` directory:
```
mix package
```
This creates the file `freedive-0.x.x.pkg` within your project directory.
## Chat / Community ## Chat / Community
The chat and community is based in the Matrix channel hosted at the BSD.cafe:
The chat and community are on Matrix hosted at the BSD.cafe:
* #BSDCafe:bsd.cafe * #BSDCafe:bsd.cafe
You can join by clicking: https://matrix.to/#/#BSDCafe:bsd.cafe You can also join by simply clicking the following link: https://matrix.to/#/#BSDCafe:bsd.cafe

View file

@ -1,4 +1,7 @@
@import "bulma/css/bulma.min.css"; /* @import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities"; */
@import "bulma";
.size-256 { .size-256 {
height: 256px; height: 256px;

View file

@ -1,7 +1,3 @@
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
import "../css/app.css"
// If you want to use Phoenix channels, run `mix help phx.gen.channel` // If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below. // to get started and then uncomment the line below.
// import "./user_socket.js" // import "./user_socket.js"

75
assets/tailwind.config.js Normal file
View file

@ -0,0 +1,75 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
module.exports = {
content: [
"./js/**/*.js",
"../lib/freedive_web.ex",
"../lib/freedive_web/**/*.*ex"
],
theme: {
extend: {
colors: {
brand: "#FD4F00",
}
},
},
plugins: [
require("@tailwindcss/forms"),
// Allows prefixing tailwind classes with LiveView classes to add rules
// only when LiveView classes are applied, for example:
//
// <div class="phx-click-loading:animate-ping">
//
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 &"])),
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
// See your `CoreComponents.icon/1` for more information.
//
plugin(function({matchComponents, theme}) {
let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
let values = {}
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"],
["-micro", "/16/solid"]
]
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
let name = path.basename(file, ".svg") + suffix
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
})
})
matchComponents({
"hero": ({name, fullPath}) => {
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
let size = theme("spacing.6")
if (name.endsWith("-mini")) {
size = theme("spacing.5")
} else if (name.endsWith("-micro")) {
size = theme("spacing.4")
}
return {
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--hero-${name})`,
"mask": `var(--hero-${name})`,
"mask-repeat": "no-repeat",
"background-color": "currentColor",
"vertical-align": "middle",
"display": "inline-block",
"width": size,
"height": size
}
}
}, {values})
})
]
}

View file

@ -47,6 +47,18 @@ config :esbuild,
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
] ]
# Configure tailwind (the version is required)
config :tailwind,
version: "3.4.0",
freedive: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
# Configures Elixir's Logger # Configures Elixir's Logger
config :logger, :console, config :logger, :console,
format: "$time $metadata[$level] $message\n", format: "$time $metadata[$level] $message\n",

View file

@ -23,6 +23,7 @@ config :freedive, FreediveWeb.Endpoint,
secret_key_base: "gccoUoyi8/eCAM6QwMnufYsgYUtubBHBnmMQZ6xYpIIYI26Es4S/3VpIw6UKT0Ll", secret_key_base: "gccoUoyi8/eCAM6QwMnufYsgYUtubBHBnmMQZ6xYpIIYI26Es4S/3VpIw6UKT0Ll",
watchers: [ watchers: [
esbuild: {Esbuild, :install_and_run, [:freedive, ~w(--sourcemap=inline --watch)]}, esbuild: {Esbuild, :install_and_run, [:freedive, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:freedive, ~w(--watch)]}
] ]
# ## SSL Support # ## SSL Support

View file

@ -18,28 +18,28 @@ defmodule Freedive.Api.Command do
## Examples ## Examples
iex> Freedive.Api.Command.execute!("whoami", [], doas: true) iex> Freedive.execute!("whoami", [], doas: true)
"root" "root"
iex> Freedive.Api.Command.execute!("whoami", [], doas: "www") iex> Freedive.execute!("whoami", [], doas: "www")
"www" "www"
iex> Freedive.Api.Command.execute!("whoami", [], jail: "testjail") iex> Freedive.execute!("whoami", [], jail: "testjail")
"root" "root"
iex> Freedive.Api.Command.execute!("whoami", [], jail: "testjail", doas: "operator") iex> Freedive.execute!("whoami", [], jail: "testjail", doas: "operator")
"operator" "operator"
iex> Freedive.Api.Command.execute("hostname", [], jail: "testjail") iex> Freedive.execute("hostname", [], jail: "testjail")
{:ok, "testjail"} {:ok, "testjail"}
iex> Freedive.Api.Command.execute!("sysctl", ["-n", "security.jail.jailed"]) iex> Freedive.execute!("sysctl", ["-n", "security.jail.jailed"])
"0" "0"
iex> Freedive.Api.Command.execute!("sysctl", ["-n", "security.jail.jailed"], jail: "testjail") iex> Freedive.execute!("sysctl", ["-n", "security.jail.jailed"], jail: "testjail")
"1" "1"
iex> Freedive.Api.Command.execute!("printenv", ["FOO"], jail: "testjail", env: [{"FOO", "bar"}]) iex> Freedive.execute!("printenv", ["FOO"], jail: "testjail", env: [{"FOO", "bar"}])
"bar" "bar"
""" """
def execute!(command, args, opts \\ []), do: raise_on_error(execute(command, args, opts)) def execute!(command, args, opts \\ []), do: raise_on_error(execute(command, args, opts))
@ -49,28 +49,28 @@ defmodule Freedive.Api.Command do
## Examples ## Examples
iex> Freedive.Api.Command.execute("whoami", [], doas: true) iex> Freedive.execute("whoami", [], doas: true)
{:ok, "root"} {:ok, "root"}
iex> Freedive.Api.Command.execute("whoami", [], doas: "www") iex> Freedive.execute("whoami", [], doas: "www")
{:ok, "www"} {:ok, "www"}
iex> Freedive.Api.Command.execute("whoami", [], jail: "testjail") iex> Freedive.execute("whoami", [], jail: "testjail")
{:ok, "root"} {:ok, "root"}
iex> Freedive.Api.Command.execute("whoami", [], jail: "testjail", doas: "operator") iex> Freedive.execute("whoami", [], jail: "testjail", doas: "operator")
{:ok, "operator"} {:ok, "operator"}
iex> Freedive.Api.Command.execute("hostname", [], jail: "testjail") iex> Freedive.execute("hostname", [], jail: "testjail")
{:ok, "testjail"} {:ok, "testjail"}
iex> Freedive.Api.Command.execute("sysctl", ["-n", "security.jail.jailed"]) iex> Freedive.execute("sysctl", ["-n", "security.jail.jailed"])
{:ok, "0"} {:ok, "0"}
iex> Freedive.Api.Command.execute("sysctl", ["-n", "security.jail.jailed"], jail: "testjail") iex> Freedive.execute("sysctl", ["-n", "security.jail.jailed"], jail: "testjail")
{:ok, "1"} {:ok, "1"}
iex> Freedive.Api.Command.execute("printenv", ["FOO"], jail: "testjail", env: [{"FOO", "bar"}]) iex> Freedive.execute("printenv", ["FOO"], jail: "testjail", env: [{"FOO", "bar"}])
{:ok, "bar"} {:ok, "bar"}
""" """
@spec execute(String.t(), list(String.t()), Keyword.t()) :: @spec execute(String.t(), list(String.t()), Keyword.t()) ::

View file

@ -53,6 +53,7 @@ defmodule Freedive.MixProject do
{:floki, ">= 0.30.0", only: :test}, {:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"}, {:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
{:heroicons, {:heroicons,
github: "tailwindlabs/heroicons", github: "tailwindlabs/heroicons",
tag: "v2.1.1", tag: "v2.1.1",
@ -68,6 +69,7 @@ defmodule Freedive.MixProject do
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"}, {:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.2"}, {:bandit, "~> 1.2"},
{:phx_tailwind_freebsd, "~> 0.2.1", runtime: Mix.env() == :dev},
{:lucide_icons, "~> 1.1"}, {:lucide_icons, "~> 1.1"},
{:phx_component_helpers, "~> 1.4"}, {:phx_component_helpers, "~> 1.4"},
{:mix_freebsd_pkg, github: "hiway/mix_freebsd_pkg", runtime: Mix.env() == :dev}, {:mix_freebsd_pkg, github: "hiway/mix_freebsd_pkg", runtime: Mix.env() == :dev},
@ -86,6 +88,7 @@ defmodule Freedive.MixProject do
[ [
setup: [ setup: [
"deps.get", "deps.get",
"tailwind.install_freebsd",
"ecto.setup", "ecto.setup",
"assets.setup", "assets.setup",
"assets.build" "assets.build"
@ -93,9 +96,10 @@ defmodule Freedive.MixProject do
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"], "ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.setup": ["esbuild.install --if-missing"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
"assets.build": ["esbuild freedive"], "assets.build": ["tailwind freedive", "esbuild freedive"],
"assets.deploy": [ "assets.deploy": [
"tailwind freedive --minify",
"esbuild freedive --minify", "esbuild freedive --minify",
"phx.digest" "phx.digest"
], ],

View file

@ -39,10 +39,12 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phx_component_helpers": {:hex, :phx_component_helpers, "1.4.1", "dbbc8ed3082055a901e3918cb24ec43df64f0f3bb81d6865beaf114fb355569e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, ">= 4.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "cf499d28f2af3c8398230994c54d2ea86cd4759523693b6b75b9897cc7f57912"}, "phx_component_helpers": {:hex, :phx_component_helpers, "1.4.1", "dbbc8ed3082055a901e3918cb24ec43df64f0f3bb81d6865beaf114fb355569e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, ">= 4.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "cf499d28f2af3c8398230994c54d2ea86cd4759523693b6b75b9897cc7f57912"},
"phx_config_util": {:hex, :phx_config_util, "0.1.0", "185e3435e8fa0d18113d153ce6d40cc7e865ee7f1dfd27e66b76bc612ed681fd", [:mix], [{:net_address, "~> 0.3.1", [hex: :net_address, repo: "hexpm", optional: false]}], "hexpm", "fe0d303d9716875f4d586a706d39caf4a374dafb0e0adff63dd1e0cd25117133"}, "phx_config_util": {:hex, :phx_config_util, "0.1.0", "185e3435e8fa0d18113d153ce6d40cc7e865ee7f1dfd27e66b76bc612ed681fd", [:mix], [{:net_address, "~> 0.3.1", [hex: :net_address, repo: "hexpm", optional: false]}], "hexpm", "fe0d303d9716875f4d586a706d39caf4a374dafb0e0adff63dd1e0cd25117133"},
"phx_tailwind_freebsd": {:hex, :phx_tailwind_freebsd, "0.2.1", "23583bb200196f879fbedd49be69b4d2d7ad2137971ad9a060f2cf38358c14a8", [:mix], [], "hexpm", "04aabe4b93ba850ca9116ba0a0cf302cbc5b846d09d0e268213a4553a9c53b31"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"}, "retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"},
"swoosh": {:hex, :swoosh, "1.16.5", "5742f24c4d081671ebe87d8e7f6595cf75205d7f808cc5d55b09e4598b583413", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2324cf696b09ee52e5e1049dcc77880a11fe618a381e2df1c5ca5d69c380eb0"}, "swoosh": {:hex, :swoosh, "1.16.5", "5742f24c4d081671ebe87d8e7f6595cf75205d7f808cc5d55b09e4598b583413", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2324cf696b09ee52e5e1049dcc77880a11fe618a381e2df1c5ca5d69c380eb0"},
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},