diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61a7393 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# This file excludes paths from the Docker build context. +# +# By default, Docker's build context includes all files (and folders) in the +# current directory. Even if a file isn't copied into the container it is still sent to +# the Docker daemon. +# +# There are multiple reasons to exclude files from the build context: +# +# 1. Prevent nested folders from being copied into the container (ex: exclude +# /assets/node_modules when copying /assets) +# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) +# 3. Avoid sending files containing sensitive information +# +# More information on using .dockerignore is available here: +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +.dockerignore + +# Ignore git, but keep git HEAD and refs to access current commit hash if needed: +# +# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat +# d0b8727759e1e0e7aa3d41707d12376e373d5ecc +.git +!.git/HEAD +!.git/refs + +# Common development/test artifacts +/cover/ +/doc/ +/test/ +/tmp/ +.elixir_ls + +# Mix artifacts +/_build/ +/deps/ +*.ez + +# Generated on crash by the VM +erl_crash.dump + +# Static artifacts - These should be fetched and built inside the Docker image +/assets/node_modules/ +/priv/static/assets/ +/priv/static/cache_manifest.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..49868ad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian +# instead of Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20250317-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.18.3-erlang-27.3.1-debian-bullseye-20250317-slim +# +ARG ELIXIR_VERSION=1.18.3 +ARG OTP_VERSION=27.3.1 +ARG DEBIAN_VERSION=bullseye-20250317-slim + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +ARG DATABASE="/data/database" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && \ + apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates tini \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/putzplan ./ + +USER nobody + +ENV DATABASE=${DATABASE} + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +ENTRYPOINT ["/usr/bin/tini", "--"] + +CMD ["/app/bin/server"] diff --git a/Makefile b/Makefile index ef4298d..a5f5494 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ authelia-start: ${AUTHELIA_PID} ${AUTHELIA_PID}: ${AUTHELIA_CONFIG} mkdir -p ${AUTHELIA_HOME}/tmp/ cd ${AUTHELIA_HOME} + mkdir -p $(dir ${AUTHELIA_PID}) authelia --config ${AUTHELIA_CONFIG} &> ${AUTHELIA_LOG} & echo $$! > ${AUTHELIA_PID} diff --git a/config/config.exs b/config/config.exs index c4857cf..7097c4f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -51,7 +51,7 @@ config :putzplan, # Configures the endpoint config :putzplan, PutzplanWeb.Endpoint, - url: [host: "localhost"], + url: [host: "127.0.0.1"], adapter: Bandit.PhoenixAdapter, render_errors: [ formats: [html: PutzplanWeb.ErrorHTML, json: PutzplanWeb.ErrorJSON], @@ -99,6 +99,8 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +config :exqlite, force_build: true + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index a3ec2e6..157e7a5 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -3,8 +3,6 @@ config :live_debugger, browser_features?: true # Configure your database config :putzplan, Putzplan.Repo, - username: "postgres", - password: "postgres", hostname: "localhost", database: "tmp/putzplan_dev", stacktrace: true, diff --git a/config/prod.exs b/config/prod.exs index cdc42f2..5c9c4fe 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -5,7 +5,7 @@ import Config # manifest is generated by the `mix assets.deploy` task, # which you should run after static files are built and # before starting your production server. -config :putzplan, PutzplanWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" +config :putzplan, PutzplanWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json", server: true # Configures Swoosh API Client config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Putzplan.Finch diff --git a/config/runtime.exs b/config/runtime.exs index 93a4df4..b3b033d 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -21,20 +21,15 @@ if System.get_env("PHX_SERVER") do end if config_env() == :prod do - database_url = - System.get_env("DATABASE_URL") || + database = + System.get_env("DATABASE") || raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE + environment variable DATABASE is missing. """ - maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] - config :putzplan, Putzplan.Repo, - # ssl: true, - url: database_url, - pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), - socket_options: maybe_ipv6 + database: database, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1") # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you diff --git a/flake.nix b/flake.nix index eef0495..6ce8685 100644 --- a/flake.nix +++ b/flake.nix @@ -6,41 +6,94 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let + outputs = { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: let pkgs = nixpkgs.legacyPackages.${system}; - in - { + erl = pkgs.beam.interpreters.erlang_27; + erlangPackages = pkgs.beam.packagesWith erl; + elixir = erlangPackages.elixir; + in { + packages = let + version = "0.1.0"; + src = ./.; + mixFodDeps = erlangPackages.fetchMixDeps { + inherit version src; + pname = "putzplan"; + sha256 = "sha256-H8FFuNayJcFvESWlGYr6H6L5zSzAjqBixBmob5Gnoc4="; + }; + translatedPlatform = + pkgs.lib.getAttr + system + { + aarch64-darwin = "macos-arm64"; + aarch64-linux = "linux-arm64"; + armv7l-linux = "linux-armv7"; + x86_64-darwin = "macos-x64"; + x86_64-linux = "linux-x64"; + }; + in rec { + default = erlangPackages.mixRelease { + inherit version src mixFodDeps; + pname = "putzplan"; + + preInstall = '' + ln -s ${pkgs.tailwindcss}/bin/tailwindcss _build/tailwind-${translatedPlatform} + ln -s ${pkgs.esbuild}/bin/esbuild _build/esbuild-${translatedPlatform} + + ${elixir}/bin/mix assets.deploy + ${elixir}/bin/mix phx.gen.release + ''; + }; + dockerImage = pkgs.dockerTools.buildImage { + name = "putzplan"; + config = { + Entrypoint = "${default}/bin/putzplan"; + Cmd = "start"; + }; + }; + }; devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - erlang_25 - beam.packages.erlang_25.elixir_1_18 - lexical - beam.packages.erlang_25.elixir-ls - next-ls + buildInputs = with pkgs; + [ + erl + elixir + lexical + erlangPackages.elixir-ls + next-ls - sqlite - gnumake - authelia - ] - ++ lib.optionals stdenv.isLinux [ - # For ExUnit Notifier on Linux. - libnotify + gnumake + authelia + lazysql - # For file_system on Linux. - inotify-tools - ] - ++ lib.optionals stdenv.isDarwin ([ - # For ExUnit Notifier on macOS. - terminal-notifier + nix-output-monitor + ] + ++ lib.optionals stdenv.isLinux [ + # For ExUnit Notifier on Linux. + libnotify - # For file_system on macOS. - darwin.apple_sdk.frameworks.CoreFoundation - darwin.apple_sdk.frameworks.CoreServices - ]); + # For file_system on Linux. + inotify-tools + ] + ++ lib.optionals stdenv.isDarwin [ + # For ExUnit Notifier on macOS. + terminal-notifier + + # For file_system on macOS. + darwin.apple_sdk.frameworks.CoreFoundation + darwin.apple_sdk.frameworks.CoreServices + ]; shellHook = '' + export OIDC_CLIENT_ID="putzplan" + export OIDC_BASE_URL="http://127.0.0.1:9091" + export OIDC_CLIENT_SECRET_FILE="${pkgs.writeText "client_secret" "insecure_secret"}" + export OIDC_REDIRECT_URI="http://127.0.0.1:4000/auth" + # allows mix to work on the local directory mkdir -p .nix/{mix,hex} export MIX_HOME=$PWD/.nix/mix diff --git a/lib/putzplan/accounts/user.ex b/lib/putzplan/accounts/user.ex index 6540272..0821257 100644 --- a/lib/putzplan/accounts/user.ex +++ b/lib/putzplan/accounts/user.ex @@ -23,11 +23,11 @@ defmodule Putzplan.Accounts.User do strategies do oidc :oidc do - client_id "putzplan" - base_url "http://127.0.0.1:9091" - client_secret "insecure_secret" + client_id Putzplan.Secrets + base_url Putzplan.Secrets + client_secret Putzplan.Secrets nonce true - redirect_uri "http://127.0.0.1:4000/auth" + redirect_uri Putzplan.Secrets authorization_params scope: "profile email" end end diff --git a/lib/putzplan/release.ex b/lib/putzplan/release.ex new file mode 100644 index 0000000..614220f --- /dev/null +++ b/lib/putzplan/release.ex @@ -0,0 +1,28 @@ +defmodule Putzplan.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :putzplan + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/lib/putzplan/secrets.ex b/lib/putzplan/secrets.ex index a108ece..3a3ad9b 100644 --- a/lib/putzplan/secrets.ex +++ b/lib/putzplan/secrets.ex @@ -1,7 +1,64 @@ defmodule Putzplan.Secrets do + require Logger use AshAuthentication.Secret def secret_for([:authentication, :tokens, :signing_secret], Putzplan.Accounts.User, _opts, _ctx) do Application.fetch_env(:putzplan, :token_signing_secret) end + + def secret_for([:authentication, :strategies, :oidc, name], Putzplan.Accounts.User, _opts, _ctx) + when is_atom(name) do + name + |> Atom.to_string() + |> String.upcase() + |> secret_from_env() + |> dbg + end + + defp secret_from_env(name) do + name + |> from_file + |> case do + :not_set -> + from_env(name) + + other -> + other + end + |> case do + {:error, error} -> + Logger.error(error) + :error + + {:ok, _secret} = ok -> + ok + end + end + + defp from_file(name) do + env_name = "OIDC_" <> name <> "_FILE" + + with {:env, {:ok, value}} <- {:env, System.fetch_env(env_name)}, + {:file, {:ok, contents}} <- {:file, File.read(value)} do + {:ok, contents} + else + {:env, :error} -> + Logger.info("#{env_name} is not set trying OIDC_#{name}.") + :not_set + + {:file, _} -> + {:error, "Error reading secret file for #{name}."} + end + end + + defp from_env(name) do + env_name = "OIDC_#{name}" + + env_name + |> System.fetch_env() + |> case do + :error -> {:error, "#{env_name} is not set!"} + other -> other + end + end end diff --git a/lib/putzplan_web/components/layouts/app.html.heex b/lib/putzplan_web/components/layouts/app.html.heex index 3b3b607..c5e8a20 100644 --- a/lib/putzplan_web/components/layouts/app.html.heex +++ b/lib/putzplan_web/components/layouts/app.html.heex @@ -1,29 +1,3 @@ -
-
-
- - - -

- v{Application.spec(:phoenix, :vsn)} -

-
- -
-
<.flash_group flash={@flash} /> diff --git a/lib/putzplan_web/components/layouts/root.html.heex b/lib/putzplan_web/components/layouts/root.html.heex index 1798c32..1c58c4c 100644 --- a/lib/putzplan_web/components/layouts/root.html.heex +++ b/lib/putzplan_web/components/layouts/root.html.heex @@ -9,7 +9,7 @@ - <.live_title default="Putzplan" suffix=" ยท Phoenix Framework"> + <.live_title default="Putzplan"> {assigns[:page_title]} diff --git a/mix.exs b/mix.exs index 5fda815..a9fec15 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,6 @@ defmodule Putzplan.MixProject do {:phoenix, "~> 1.7.21"}, {:phoenix_ecto, "~> 4.5"}, {:ecto_sql, "~> 3.10"}, - {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 4.1"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_view, "~> 1.0"}, diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate new file mode 100755 index 0000000..7240526 --- /dev/null +++ b/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./putzplan eval Putzplan.Release.migrate diff --git a/rel/overlays/bin/migrate.bat b/rel/overlays/bin/migrate.bat new file mode 100755 index 0000000..6b72c38 --- /dev/null +++ b/rel/overlays/bin/migrate.bat @@ -0,0 +1 @@ +call "%~dp0\putzplan" eval Putzplan.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100755 index 0000000..d84dbfc --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./putzplan start diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat new file mode 100755 index 0000000..0206804 --- /dev/null +++ b/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\putzplan" start