diff --git a/.gitignore b/.gitignore index d93acd5..01ecd05 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,11 @@ freedive-*.tar # Ignore assets that are produced by build tools. /priv/static/assets/ +/priv/static/favicon-*.* +/priv/static/robots-*.* +/priv/static/robots.txt.gz +/priv/static/images/logo-*.* +/priv/static/images/logo.svg.gz # Ignore digested assets cache. /priv/static/cache_manifest.json @@ -39,3 +44,5 @@ npm-debug.log *.db *.db-* +# FreeBSD package +*.pkg diff --git a/config/runtime.exs b/config/runtime.exs index 2ae8375..2c82427 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,15 +7,6 @@ import Config # any compile-time configuration in here, as it won't be applied. # The block below contains prod specific runtime configuration. -# ## Using releases -# -# If you use `mix release`, you need to explicitly enable the server -# by passing the PHX_SERVER=true when you start it: -# -# PHX_SERVER=true bin/freedive start -# -# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` -# script that automatically sets the env var above. if System.get_env("PHX_SERVER") do config :freedive, FreediveWeb.Endpoint, server: true end @@ -50,55 +41,26 @@ if config_env() == :prod do You can generate one by calling: mix phx.gen.secret """ - host = System.get_env("PHX_HOST") || "example.com" - port = String.to_integer(System.get_env("PORT") || "4000") + # config :freedive, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + + host = System.get_env("HOST") || "localhost" + bind = System.get_env("BIND") || "local4" + port = String.to_integer(System.get_env("PORT") || "5443") + ip = PhxConfigUtil.BindToIp.parse!(bind) config :freedive, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :freedive, FreediveWeb.Endpoint, - url: [host: host, port: 443, scheme: "https"], - http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. - # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0}, - port: port + url: [host: host, port: port, scheme: "https"], + https: [ + ip: ip, + port: port, + cipher_suite: :strong, + keyfile: System.get_env("TLS_KEY_PATH"), + certfile: System.get_env("TLS_CERT_PATH") ], secret_key_base: secret_key_base - # ## SSL Support - # - # To get SSL working, you will need to add the `https` key - # to your endpoint configuration: - # - # config :freedive, FreediveWeb.Endpoint, - # https: [ - # ..., - # port: 443, - # cipher_suite: :strong, - # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), - # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") - # ] - # - # The `cipher_suite` is set to `:strong` to support only the - # latest and more secure SSL ciphers. This means old browsers - # and clients may not be supported. You can set it to - # `:compatible` for wider support. - # - # `:keyfile` and `:certfile` expect an absolute path to the key - # and cert in disk or a relative path inside priv, for example - # "priv/ssl/server.key". For all supported SSL configuration - # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 - # - # We also recommend setting `force_ssl` in your config/prod.exs, - # ensuring no data is ever sent via http, always redirecting to https: - # - # config :freedive, FreediveWeb.Endpoint, - # force_ssl: [hsts: true] - # - # Check `Plug.SSL` for all available options in `force_ssl`. - # ## Configuring the mailer # # In production you need to configure the mailer to use a different adapter. diff --git a/freedive.env.sample b/freedive.env.sample new file mode 100644 index 0000000..cff49b5 --- /dev/null +++ b/freedive.env.sample @@ -0,0 +1,18 @@ +# Web server +HOST="localhost" +BIND="local4" +PORT=3443 + +TLS_KEY_PATH="<%= @data_dir %>/tls.key" +TLS_CERT_PATH="<%= @data_dir %>/tls.crt" + +# Cluster +# DNS_CLUSTER_QUERY="" + +# Features +REGISTER_ACCOUNT_ENABLE="false" + +# Phoenix +PHX_SERVER=true +SECRET_KEY_BASE="CHANGE-ME" +DATABASE_PATH="<%= @data_dir %>/freedive.db" diff --git a/mix.exs b/mix.exs index 8b21792..3b76755 100644 --- a/mix.exs +++ b/mix.exs @@ -5,11 +5,21 @@ defmodule Freedive.MixProject do [ app: :freedive, version: "0.1.0", + description: "Dive into FreeBSD", + homepage_url: "https://brew.bsd.cafe/hiway/freedive", + maintainer: "harshad@sharma.io", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps() + deps: deps(), + freebsd_pkg: freebsd_pkg() + ] + end + + defp freebsd_pkg do + [ + service_commands: ["init", "account_register", "password_reset"], ] end @@ -61,7 +71,9 @@ defmodule Freedive.MixProject do {:bandit, "~> 1.2"}, {:phx_tailwind_freebsd, "~> 0.2.1", runtime: Mix.env() == :dev}, {: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}, + {:phx_config_util, "~> 0.1.0"} ] end @@ -89,7 +101,14 @@ defmodule Freedive.MixProject do "tailwind freedive --minify", "esbuild freedive --minify", "phx.digest" - ] + ], + package: ["compile", "assets.deploy", "release --overwrite", "freebsd.pkg"] + ] + end + + def cli do + [ + preferred_envs: [package: :prod] ] end end diff --git a/mix.lock b/mix.lock index 9e2b673..fe5b1b9 100644 --- a/mix.lock +++ b/mix.lock @@ -24,8 +24,11 @@ "lucide_icons": {:hex, :lucide_icons, "1.1.1", "39e3ea54b554edd9253c2d9949a6171cd54609c86d9848d315ca9dd3c148e459", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "93c4c87bfc1080e159a43d81a494d1bb93c6f59c127bfc108f39158e6b77c4c2"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, + "mix_freebsd_pkg": {:git, "https://github.com/hiway/mix_freebsd_pkg.git", "4940f1a85a4951c2a16b21d8302b3df0c2c1cabc", []}, + "net_address": {:hex, :net_address, "0.3.1", "70832e5a35a2c9f2d6e5bcf43e3164b9c4f7515ca7cc8740d2d38aab39ffa0e9", [:mix], [], "hexpm", "eb20da348f1ad88fdfaaa4e8923a3906ac54b54cb67ac1731f86bce07cb2b081"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "opus": {:hex, :opus, "0.8.4", "9de6d0eb1b3bad69442059b54d473cf25fa3ea32905f3022eb5e892aff1c4a2f", [:mix], [{:retry, "~> 0.8", [hex: :retry, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "431b5361ac34af5d72921a5c9a09f4f61c3d0911e7de636af982bb5915a41c5e"}, "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, @@ -35,9 +38,11 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "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_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_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "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"}, "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"}, diff --git a/priv/freebsd/post-deinstall.sh.eex b/priv/freebsd/post-deinstall.sh.eex new file mode 100644 index 0000000..1a24852 --- /dev/null +++ b/priv/freebsd/post-deinstall.sh.eex @@ -0,0 +1 @@ +#!/bin/sh diff --git a/priv/freebsd/post-install.sh.eex b/priv/freebsd/post-install.sh.eex new file mode 100644 index 0000000..1a24852 --- /dev/null +++ b/priv/freebsd/post-install.sh.eex @@ -0,0 +1 @@ +#!/bin/sh diff --git a/priv/freebsd/pre-deinstall.sh.eex b/priv/freebsd/pre-deinstall.sh.eex new file mode 100644 index 0000000..1a24852 --- /dev/null +++ b/priv/freebsd/pre-deinstall.sh.eex @@ -0,0 +1 @@ +#!/bin/sh diff --git a/priv/freebsd/pre-install.sh.eex b/priv/freebsd/pre-install.sh.eex new file mode 100644 index 0000000..ceefc31 --- /dev/null +++ b/priv/freebsd/pre-install.sh.eex @@ -0,0 +1,24 @@ +#!/bin/sh + +ensure_user() { + group=$1 + user=$1 + + if pw groupshow $group 2>/dev/null; then + echo "Group '$group' already exists." + else + echo "Creating group '$group'." + pw groupadd $group + fi + + if id $user 2>/dev/null; then + echo "User '$user' already exists." + else + echo "Creating user '$user' with group '$group'." + pw useradd $user -g $group -s /usr/sbin/nologin -d /nonexistent + fi +} + +<%= if @user != "root" do %> +ensure_user "<%= @user %>" "<%= @user %>" +<% end %> diff --git a/priv/freebsd/service.sh.eex b/priv/freebsd/service.sh.eex new file mode 100644 index 0000000..b77e542 --- /dev/null +++ b/priv/freebsd/service.sh.eex @@ -0,0 +1,57 @@ +#!bin/sh + +# PROVIDE: <%= @name %> +# REQUIRE: LOGIN +# REQUIRE: DAEMON + +<%= @name %>_enable=${<%= @name %>_enable:-"NO"} +. /etc/rc.subr + +: ${<%= @name %>_env_file:="<%= @env_file %>"} + +name="<%= @name %>" +<%= @name %>_user="<%= @user %>" +<%= @name %>_group="<%= @group %>" +rcvar="<%= @name %>_enable" +command="<%= @app_dir %>/bin/<%= @name %>" +pidfile="<%= @run_dir %>/<%= @name %>.pid" +confdir="<%= @conf_dir %>" +logfile="<%= @log_dir %>/<%= @name %>.log" + +extra_commands="<%= Enum.join(@service_commands, " ") %>" + +procname=${command} +start_cmd="<%= @name %>_start" +stop_cmd="${command} stop" + +<%= for cmd <- @service_commands do %> +<%= cmd %>_cmd="<%= @app_dir %>/<%= cmd %>.sh" +<% end %> + +<%= @name %>_start() { + # Stop here if not running as root + if [ `id -u` -ne 0 ]; then + echo "You must be root to start <%= @name %>" + return 1 + fi + + : "${ERL_CRASH_DUMP:="<%= @run_dir %>/erl_crash.dump"}" + export ERL_CRASH_DUMP + + if [ -f ${pidfile} ]; then + echo "Pidfile ${pidfile} exists. Is <%= @name %> running?" + return 1 + fi + + if [ -f ${ERL_CRASH_DUMP} ]; then + echo "Removing old crash dump file ${ERL_CRASH_DUMP}" + rm -f ${ERL_CRASH_DUMP} + fi + + echo "Starting <%= @name %> as user <%= @user %>" + echo "Logs will be written to ${logfile}" + daemon -t ${name} -p ${pidfile} -f -H -o ${logfile} -u <%= @user %> ${command} start +} + +load_rc_config ${name} +run_rc_command "$1" diff --git a/priv/freebsd/service_account_register.sh.eex b/priv/freebsd/service_account_register.sh.eex new file mode 100644 index 0000000..4515f74 --- /dev/null +++ b/priv/freebsd/service_account_register.sh.eex @@ -0,0 +1,4 @@ +#!/bin/sh +# account_register + +<%= @app_dir %>/bin/<%= @name %> eval "Freedive.Release.account_create" diff --git a/priv/freebsd/service_init.sh.eex b/priv/freebsd/service_init.sh.eex new file mode 100644 index 0000000..8d37115 --- /dev/null +++ b/priv/freebsd/service_init.sh.eex @@ -0,0 +1,46 @@ +#!/bin/sh + +if [ -f <%= @data_dir %>/tls.key ] ; then + echo "[ok] TLS keys for https endpoint" +else + echo "[create] TLS keys for https endpoint" + cat > <%= @data_dir %>/request.txt <.local +[v3_req] +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = <%= @name %>.local +DNS.2 = localhost +IP.1 = 127.0.0.1 +INNER_EOF + openssl req -new -nodes -x509 -days 36500 -newkey rsa:2048 -keyout <%= @data_dir %>/tls.key -out <%= @data_dir %>/tls.crt -config <%= @data_dir %>/request.txt + rm <%= @data_dir %>/request.txt + chown -R <%= @user %>:<%= @group %> <%= @data_dir %> +fi + +# Check if the secret-key is already set in @env_file +if grep -q "CHANGE-ME" <%= @env_file %> ; then + echo "[create] Secret-key for web server" + SECRET_KEY="$( openssl rand -base64 128 | strings | grep -o '[[:alnum:]]' | head -n 64 | tr -d '\n'; echo )" + sed -i '' -e "s/CHANGE-ME/${SECRET_KEY}/g" <%= @env_file %> +else + echo "[ok] Secret-key for web server" +fi + +# Migrate database +echo "[migrate] Database" +<%= @app_dir %>/bin/<%= @name %> eval "Freedive.Release.migrate" diff --git a/priv/freebsd/service_password_reset.sh.eex b/priv/freebsd/service_password_reset.sh.eex new file mode 100644 index 0000000..cae193d --- /dev/null +++ b/priv/freebsd/service_password_reset.sh.eex @@ -0,0 +1,4 @@ +#!/bin/sh +# password_reset + +<%= @app_dir %>/bin/<%= @name %> eval "Freedive.Release.password_reset"