feat: prepare first production deployment
This commit is contained in:
parent
9b51d0807b
commit
14987bb21b
|
|
@ -1,6 +1,10 @@
|
|||
# Copy this file to .env. Compose intentionally refuses to start without the
|
||||
# required values. Replace every credential before any public deployment.
|
||||
HTTP_PORT=4010
|
||||
# Bind the public Compose proxy to loopback when a reverse proxy runs on the
|
||||
# same host. The current VPN staging path needs an address reachable by its
|
||||
# verified tunnel topology, so choose this per deployment.
|
||||
HTTP_BIND_ADDRESS=0.0.0.0
|
||||
MAILPIT_PORT=8027
|
||||
MAILPIT_BIND_ADDRESS=127.0.0.1
|
||||
DOCKER_SOCKET_GID=REPLACE_WITH_DOCKER_SOCKET_NUMERIC_GID
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -168,6 +168,34 @@ set `app.host`, `app.scheme`, and `app.urlPort` to the public URL used in email
|
|||
links, and must set `app.mapTileUrl` to a tile service whose policy and capacity
|
||||
fit the deployment.
|
||||
|
||||
For a first Compose deployment, generate an ignored environment on the target
|
||||
Docker host. The command derives that host's Docker socket group, generates
|
||||
independent database, Phoenix, handover, cluster, and metrics secrets without
|
||||
printing them, writes mode `0600`, and refuses to replace an existing file:
|
||||
|
||||
```bash
|
||||
./scripts/init-production-env.sh whoneedhelp.com .env.production
|
||||
```
|
||||
|
||||
Configure the verified reverse-proxy source IP/CIDR and transactional SMTP
|
||||
provider in that file, then validate its structure and the production Compose
|
||||
render:
|
||||
|
||||
```bash
|
||||
./scripts/validate-production-env.sh .env.production whoneedhelp.com
|
||||
docker compose \
|
||||
--env-file .env.production \
|
||||
-f compose.yaml \
|
||||
-f compose.production.yaml \
|
||||
up -d --wait --build
|
||||
```
|
||||
|
||||
`compose.production.yaml` leaves local Mailpit stopped. Validation deliberately
|
||||
fails while the relay still points to Mailpit or a template marker remains.
|
||||
It does not claim to test DNS, certificates, actual mail delivery, the
|
||||
deployment's observed proxy source address, or capacity; verify those on the
|
||||
target host before opening registration.
|
||||
|
||||
Rotate all local application secrets and the existing local PostgreSQL role
|
||||
without printing the generated values:
|
||||
|
||||
|
|
|
|||
6
compose.production.yaml
Normal file
6
compose.production.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
services:
|
||||
# Public deployments use the configured transactional SMTP relay. Mailpit is
|
||||
# retained in the base file for local development but is not started by this
|
||||
# production override unless the operator explicitly enables local-mail.
|
||||
mailpit:
|
||||
profiles: [local-mail]
|
||||
|
|
@ -87,7 +87,7 @@ services:
|
|||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.web.forwardedheaders.trustedips=${TRAEFIK_TRUSTED_IPS:-127.0.0.1/32}
|
||||
ports:
|
||||
- "${HTTP_PORT:-4010}:80"
|
||||
- "${HTTP_BIND_ADDRESS:-0.0.0.0}:${HTTP_PORT:-4010}:80"
|
||||
depends_on:
|
||||
- docker-api-proxy
|
||||
networks: [docker-api, edge, ingress]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,53 @@ time objective, retention period, storage capacity, or high-availability model;
|
|||
those values require product policy and measurements from the eventual
|
||||
production environment.
|
||||
|
||||
## First production Compose environment
|
||||
|
||||
Run the initializer on the target Docker host after its final public hostname
|
||||
is known:
|
||||
|
||||
```bash
|
||||
./scripts/init-production-env.sh whoneedhelp.com .env.production
|
||||
```
|
||||
|
||||
The initializer reads `.env.example`, derives the numeric group of that host's
|
||||
Docker socket, generates independent random values for PostgreSQL,
|
||||
`SECRET_KEY_BASE`, handover codes, the BEAM release cookie, and metrics access,
|
||||
and writes an ignored mode-`0600` file. It does not print those values and
|
||||
refuses to overwrite an existing destination.
|
||||
|
||||
By default, the generated public proxy port binds to `127.0.0.1`, which is
|
||||
appropriate only when the verified reverse proxy reaches the application on
|
||||
the same host. Set `PRODUCTION_HTTP_BIND_ADDRESS` when generating the file, or
|
||||
edit `HTTP_BIND_ADDRESS` afterward, to match the observed target topology.
|
||||
Replace `TRAEFIK_TRUSTED_IPS` with the exact source IP/CIDR observed at Traefik;
|
||||
do not copy the temporary VPN value into an unrelated server.
|
||||
|
||||
Configure the transactional SMTP relay and sender accepted by that provider.
|
||||
Provider-specific auth, TLS, ports, and credentials can be supplied to the
|
||||
initializer through the documented `PRODUCTION_SMTP_*` environment values or
|
||||
edited in the resulting ignored file. Then run:
|
||||
|
||||
```bash
|
||||
./scripts/validate-production-env.sh .env.production whoneedhelp.com
|
||||
docker compose \
|
||||
--env-file .env.production \
|
||||
-f compose.yaml \
|
||||
-f compose.production.yaml \
|
||||
up -d --wait --build
|
||||
```
|
||||
|
||||
The production override keeps Mailpit behind its inactive `local-mail` profile,
|
||||
so public registration cannot appear to succeed while mail is only retained
|
||||
locally. The validator checks file ownership/mode, origin consistency,
|
||||
template markers, independent generated secrets, supported SMTP values, the
|
||||
generated database URL, and the final Compose render without printing secrets.
|
||||
It does not contact DNS, TLS, SMTP, the reverse proxy, or the application.
|
||||
After deployment, verify `/healthz/ready`, inspect all replica health and logs,
|
||||
register a unique address through the public browser, receive its message at
|
||||
the real mailbox, follow the HTTPS confirmation link, and remove only that
|
||||
run-scoped account.
|
||||
|
||||
## Compose database backup
|
||||
|
||||
Create a PostgreSQL 18 custom-format archive, validate its table of contents,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@
|
|||
|
||||
<.form :let={f} for={@password_changeset} action={~p"/users/settings"} id="update_password">
|
||||
<input type="hidden" name="action" value="update_password" />
|
||||
<input
|
||||
type="email"
|
||||
id="update_password_username"
|
||||
name="username"
|
||||
value={@current_scope.user.email}
|
||||
autocomplete="username"
|
||||
class="hidden"
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
readonly
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={f[:password]}
|
||||
|
|
|
|||
133
scripts/init-production-env.sh
Executable file
133
scripts/init-production-env.sh
Executable file
|
|
@ -0,0 +1,133 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
umask 077
|
||||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
domain=${1:-}
|
||||
target=${2:-"$ROOT/.env.production"}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 DOMAIN [OUTPUT_FILE]" >&2
|
||||
}
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "$domain" |
|
||||
grep -Eq '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$'; then
|
||||
echo "DOMAIN must be a lowercase ASCII DNS hostname without a scheme, port, or path." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command in awk docker grep mktemp openssl stat; do
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
echo "Required command is unavailable: $command" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -S /var/run/docker.sock ]; then
|
||||
echo "/var/run/docker.sock is unavailable; run this on the target Docker host." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -e "$target" ]; then
|
||||
echo "Refusing to overwrite existing deployment environment: $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
target_dir=$(dirname -- "$target")
|
||||
if [ ! -d "$target_dir" ]; then
|
||||
echo "Output directory does not exist: $target_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
postgres_password=$(openssl rand -hex 32)
|
||||
secret_key_base=$(openssl rand -hex 64)
|
||||
handover_secret=$(openssl rand -hex 64)
|
||||
release_cookie=$(openssl rand -hex 64)
|
||||
metrics_token=$(openssl rand -hex 32)
|
||||
docker_socket_gid=$(stat -c '%g' /var/run/docker.sock)
|
||||
|
||||
http_bind_address=${PRODUCTION_HTTP_BIND_ADDRESS:-127.0.0.1}
|
||||
trusted_proxy_ips=${PRODUCTION_TRAEFIK_TRUSTED_IPS:-REPLACE_WITH_VERIFIED_PROXY_IP_OR_CIDR}
|
||||
smtp_relay=${PRODUCTION_SMTP_RELAY:-REPLACE_WITH_TRANSACTIONAL_SMTP_RELAY}
|
||||
smtp_port=${PRODUCTION_SMTP_PORT:-587}
|
||||
smtp_username=${PRODUCTION_SMTP_USERNAME:-}
|
||||
smtp_password=${PRODUCTION_SMTP_PASSWORD:-}
|
||||
smtp_auth=${PRODUCTION_SMTP_AUTH:-always}
|
||||
smtp_tls=${PRODUCTION_SMTP_TLS:-always}
|
||||
smtp_ssl=${PRODUCTION_SMTP_SSL:-false}
|
||||
email_from_address=${PRODUCTION_EMAIL_FROM_ADDRESS:-"contact@$domain"}
|
||||
|
||||
tmp=$(mktemp "$target_dir/.production-env.XXXXXX")
|
||||
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
||||
chmod 600 "$tmp"
|
||||
|
||||
DOMAIN=$domain \
|
||||
HTTP_BIND_ADDRESS_VALUE=$http_bind_address \
|
||||
DOCKER_SOCKET_GID_VALUE=$docker_socket_gid \
|
||||
TRUSTED_PROXY_IPS_VALUE=$trusted_proxy_ips \
|
||||
POSTGRES_PASSWORD_VALUE=$postgres_password \
|
||||
DATABASE_URL_VALUE="ecto://postgres:$postgres_password@db/who_need_help" \
|
||||
SECRET_KEY_BASE_VALUE=$secret_key_base \
|
||||
HANDOVER_SECRET_VALUE=$handover_secret \
|
||||
RELEASE_COOKIE_VALUE=$release_cookie \
|
||||
METRICS_TOKEN_VALUE=$metrics_token \
|
||||
SMTP_RELAY_VALUE=$smtp_relay \
|
||||
SMTP_PORT_VALUE=$smtp_port \
|
||||
SMTP_USERNAME_VALUE=$smtp_username \
|
||||
SMTP_PASSWORD_VALUE=$smtp_password \
|
||||
SMTP_AUTH_VALUE=$smtp_auth \
|
||||
SMTP_TLS_VALUE=$smtp_tls \
|
||||
SMTP_SSL_VALUE=$smtp_ssl \
|
||||
EMAIL_FROM_ADDRESS_VALUE=$email_from_address \
|
||||
awk '
|
||||
BEGIN {
|
||||
replacement["HTTP_BIND_ADDRESS"] = ENVIRON["HTTP_BIND_ADDRESS_VALUE"]
|
||||
replacement["DOCKER_SOCKET_GID"] = ENVIRON["DOCKER_SOCKET_GID_VALUE"]
|
||||
replacement["TRAEFIK_TRUSTED_IPS"] = ENVIRON["TRUSTED_PROXY_IPS_VALUE"]
|
||||
replacement["PHX_HOST"] = ENVIRON["DOMAIN"]
|
||||
replacement["PHX_SCHEME"] = "https"
|
||||
replacement["PHX_URL_PORT"] = "443"
|
||||
replacement["WNH_DEBUG_BASE_URL"] = "https://" ENVIRON["DOMAIN"]
|
||||
replacement["WNH_BASE_URL"] = "https://" ENVIRON["DOMAIN"]
|
||||
replacement["POSTGRES_PASSWORD"] = ENVIRON["POSTGRES_PASSWORD_VALUE"]
|
||||
replacement["DATABASE_URL"] = ENVIRON["DATABASE_URL_VALUE"]
|
||||
replacement["SECRET_KEY_BASE"] = ENVIRON["SECRET_KEY_BASE_VALUE"]
|
||||
replacement["HANDOVER_SECRET"] = ENVIRON["HANDOVER_SECRET_VALUE"]
|
||||
replacement["RELEASE_COOKIE"] = ENVIRON["RELEASE_COOKIE_VALUE"]
|
||||
replacement["METRICS_TOKEN"] = ENVIRON["METRICS_TOKEN_VALUE"]
|
||||
replacement["SMTP_RELAY"] = ENVIRON["SMTP_RELAY_VALUE"]
|
||||
replacement["SMTP_PORT"] = ENVIRON["SMTP_PORT_VALUE"]
|
||||
replacement["SMTP_USERNAME"] = ENVIRON["SMTP_USERNAME_VALUE"]
|
||||
replacement["SMTP_PASSWORD"] = ENVIRON["SMTP_PASSWORD_VALUE"]
|
||||
replacement["SMTP_AUTH"] = ENVIRON["SMTP_AUTH_VALUE"]
|
||||
replacement["SMTP_TLS"] = ENVIRON["SMTP_TLS_VALUE"]
|
||||
replacement["SMTP_SSL"] = ENVIRON["SMTP_SSL_VALUE"]
|
||||
replacement["EMAIL_FROM_ADDRESS"] = ENVIRON["EMAIL_FROM_ADDRESS_VALUE"]
|
||||
replacement["CODEX_SESSION_ID"] = "not-configured"
|
||||
}
|
||||
{
|
||||
separator = index($0, "=")
|
||||
key = separator > 1 ? substr($0, 1, separator - 1) : ""
|
||||
|
||||
if (key in replacement) {
|
||||
print key "=" replacement[key]
|
||||
} else {
|
||||
print
|
||||
}
|
||||
}
|
||||
' "$ROOT/.env.example" >"$tmp"
|
||||
|
||||
mv "$tmp" "$target"
|
||||
chmod 600 "$target"
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
||||
|
||||
echo "Generated independent deployment secrets without printing them."
|
||||
echo "Created mode-0600 environment: $target"
|
||||
echo "Run scripts/validate-production-env.sh '$target' '$domain' after configuring the verified proxy and SMTP values."
|
||||
|
|
@ -78,8 +78,36 @@ echo "Checking crash dumps are excluded from the Docker build context"
|
|||
grep -Fx 'core' .dockerignore >/dev/null
|
||||
grep -Fx 'core.*' .dockerignore >/dev/null
|
||||
|
||||
echo "Checking production environment initialization and validation"
|
||||
production_env="$scan_dir/.env.production"
|
||||
PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
||||
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
||||
PRODUCTION_SMTP_PORT=587 \
|
||||
PRODUCTION_SMTP_USERNAME=quality-user \
|
||||
PRODUCTION_SMTP_PASSWORD=quality-password \
|
||||
PRODUCTION_SMTP_AUTH=always \
|
||||
PRODUCTION_SMTP_TLS=always \
|
||||
PRODUCTION_SMTP_SSL=false \
|
||||
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
||||
./scripts/init-production-env.sh help.test "$production_env" >/dev/null
|
||||
test "$(stat -c '%a' "$production_env")" = 600
|
||||
./scripts/validate-production-env.sh "$production_env" help.test >/dev/null
|
||||
if ./scripts/init-production-env.sh help.test "$production_env" >/dev/null 2>&1; then
|
||||
echo "Production environment initializer overwrote an existing file." >&2
|
||||
exit 1
|
||||
fi
|
||||
incomplete_production_env="$scan_dir/.env.production.incomplete"
|
||||
./scripts/init-production-env.sh help.test "$incomplete_production_env" >/dev/null
|
||||
if ./scripts/validate-production-env.sh \
|
||||
"$incomplete_production_env" help.test >/dev/null 2>&1; then
|
||||
echo "Production environment validator accepted unresolved deployment inputs." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Rendering every Docker Compose profile"
|
||||
docker compose --env-file .env.example -f compose.yaml config --quiet
|
||||
docker compose --env-file "$production_env" \
|
||||
-f compose.yaml -f compose.production.yaml config --quiet
|
||||
docker compose --env-file .env.example -f compose.yaml config --format json |
|
||||
jq --exit-status '
|
||||
. as $root
|
||||
|
|
@ -115,8 +143,14 @@ docker compose --env-file .env.example -f compose.yaml config --format json |
|
|||
and $root.networks.internal.internal == true
|
||||
and ($root.networks.egress.internal // false) == false
|
||||
and $root.services.db.security_opt == ["no-new-privileges:true"]
|
||||
and $root.services.proxy.ports[0].host_ip == "0.0.0.0"
|
||||
and $root.services.mailpit.ports[0].host_ip == "127.0.0.1"
|
||||
' >/dev/null
|
||||
HTTP_BIND_ADDRESS=127.0.0.1 \
|
||||
docker compose --env-file .env.example -f compose.yaml config --format json |
|
||||
jq --exit-status '
|
||||
.services.proxy.ports[0].host_ip == "127.0.0.1"
|
||||
' >/dev/null
|
||||
WEB_REPLICAS=1 WORKER_REPLICAS=1 \
|
||||
docker compose --env-file .env.example -f compose.yaml config --format json |
|
||||
jq --exit-status '
|
||||
|
|
|
|||
175
scripts/validate-production-env.sh
Executable file
175
scripts/validate-production-env.sh
Executable file
|
|
@ -0,0 +1,175 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
env_file=${1:-}
|
||||
expected_domain=${2:-}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 ENV_FILE EXPECTED_DOMAIN" >&2
|
||||
}
|
||||
|
||||
if [[ -z "$env_file" || -z "$expected_domain" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Deployment environment does not exist: $env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(stat -c '%a' "$env_file")" != 600 ]]; then
|
||||
echo "Deployment environment must have mode 0600: $env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(stat -c '%u' "$env_file")" != "$(id -u)" ]]; then
|
||||
echo "Deployment environment must be owned by the current operator." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read_value() {
|
||||
local key=$1
|
||||
|
||||
awk -v key="$key" '
|
||||
index($0, key "=") == 1 {
|
||||
print substr($0, length(key) + 2)
|
||||
found = 1
|
||||
}
|
||||
END { if (!found) exit 1 }
|
||||
' "$env_file"
|
||||
}
|
||||
|
||||
require_value() {
|
||||
local key=$1
|
||||
local value
|
||||
|
||||
if ! value=$(read_value "$key") || [[ -z "$value" ]]; then
|
||||
echo "$key is missing or empty in $env_file." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
reject_marker() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
|
||||
case "$value" in
|
||||
*REPLACE* | *GENERATE* | *example.com*)
|
||||
echo "$key still contains a template value." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
phx_host=$(require_value PHX_HOST)
|
||||
phx_scheme=$(require_value PHX_SCHEME)
|
||||
phx_url_port=$(require_value PHX_URL_PORT)
|
||||
base_url=$(require_value WNH_BASE_URL)
|
||||
debug_base_url=$(require_value WNH_DEBUG_BASE_URL)
|
||||
http_bind_address=$(require_value HTTP_BIND_ADDRESS)
|
||||
trusted_proxy_ips=$(require_value TRAEFIK_TRUSTED_IPS)
|
||||
postgres_password=$(require_value POSTGRES_PASSWORD)
|
||||
database_url=$(require_value DATABASE_URL)
|
||||
secret_key_base=$(require_value SECRET_KEY_BASE)
|
||||
handover_secret=$(require_value HANDOVER_SECRET)
|
||||
release_cookie=$(require_value RELEASE_COOKIE)
|
||||
metrics_token=$(require_value METRICS_TOKEN)
|
||||
smtp_relay=$(require_value SMTP_RELAY)
|
||||
smtp_port=$(require_value SMTP_PORT)
|
||||
smtp_auth=$(require_value SMTP_AUTH)
|
||||
smtp_tls=$(require_value SMTP_TLS)
|
||||
smtp_ssl=$(require_value SMTP_SSL)
|
||||
email_from_address=$(require_value EMAIL_FROM_ADDRESS)
|
||||
|
||||
[[ "$phx_host" == "$expected_domain" ]] || {
|
||||
echo "PHX_HOST does not match EXPECTED_DOMAIN." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$phx_scheme" == https && "$phx_url_port" == 443 ]] || {
|
||||
echo "Production PHX_SCHEME/PHX_URL_PORT must describe the public HTTPS origin." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$base_url" == "https://$expected_domain" ]] || {
|
||||
echo "WNH_BASE_URL does not match the public HTTPS origin." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$debug_base_url" == "$base_url" ]] || {
|
||||
echo "WNH_DEBUG_BASE_URL and WNH_BASE_URL must use the same deployment origin." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for pair in \
|
||||
"HTTP_BIND_ADDRESS:$http_bind_address" \
|
||||
"TRAEFIK_TRUSTED_IPS:$trusted_proxy_ips" \
|
||||
"POSTGRES_PASSWORD:$postgres_password" \
|
||||
"DATABASE_URL:$database_url" \
|
||||
"SECRET_KEY_BASE:$secret_key_base" \
|
||||
"HANDOVER_SECRET:$handover_secret" \
|
||||
"RELEASE_COOKIE:$release_cookie" \
|
||||
"METRICS_TOKEN:$metrics_token" \
|
||||
"SMTP_RELAY:$smtp_relay" \
|
||||
"EMAIL_FROM_ADDRESS:$email_from_address"
|
||||
do
|
||||
reject_marker "${pair%%:*}" "${pair#*:}"
|
||||
done
|
||||
|
||||
[[ "$smtp_relay" != mailpit ]] || {
|
||||
echo "SMTP_RELAY still targets local Mailpit; public registration needs a transactional relay." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$smtp_port" =~ ^[0-9]+$ ]] || {
|
||||
echo "SMTP_PORT must be numeric." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$smtp_auth" =~ ^(always|never|if_available)$ ]] || {
|
||||
echo "SMTP_AUTH has an unsupported value." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$smtp_tls" =~ ^(always|never|if_available)$ ]] || {
|
||||
echo "SMTP_TLS has an unsupported value." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$smtp_ssl" =~ ^(true|false|0|1)$ ]] || {
|
||||
echo "SMTP_SSL has an unsupported value." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$email_from_address" == *@* ]] || {
|
||||
echo "EMAIL_FROM_ADDRESS is not an email address." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
secrets=(
|
||||
"$postgres_password"
|
||||
"$secret_key_base"
|
||||
"$handover_secret"
|
||||
"$release_cookie"
|
||||
"$metrics_token"
|
||||
)
|
||||
|
||||
for ((left = 0; left < ${#secrets[@]}; left++)); do
|
||||
for ((right = left + 1; right < ${#secrets[@]}; right++)); do
|
||||
if [[ "${secrets[$left]}" == "${secrets[$right]}" ]]; then
|
||||
echo "Deployment secrets must be independent." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
expected_database_url="ecto://postgres:$postgres_password@db/who_need_help"
|
||||
[[ "$database_url" == "$expected_database_url" ]] || {
|
||||
echo "DATABASE_URL does not match the generated PostgreSQL role/password/database." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
docker compose \
|
||||
--env-file "$env_file" \
|
||||
--file "$ROOT/compose.yaml" \
|
||||
--file "$ROOT/compose.production.yaml" \
|
||||
config --quiet
|
||||
|
||||
echo "Production environment structure passed validation without printing secrets."
|
||||
echo "This does not test DNS, TLS, SMTP reachability/delivery, proxy source IPs, or server capacity."
|
||||
|
|
@ -12,6 +12,8 @@ defmodule WhoNeedHelpWeb.UserSettingsControllerTest do
|
|||
conn = get(conn, ~p"/users/settings")
|
||||
response = html_response(conn, 200)
|
||||
assert response =~ "Settings"
|
||||
assert response =~ ~s(id="update_password_username")
|
||||
assert response =~ ~s(autocomplete="username")
|
||||
end
|
||||
|
||||
test "redirects if user is not logged in" do
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user