#!/usr/bin/env bash set -Eeuo pipefail umask 077 SCRIPT_DIRECTORY="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" PROJECT_ROOT="$(cd -- "$SCRIPT_DIRECTORY/.." && pwd -P)" SECRETS_DIRECTORY="$PROJECT_ROOT/secrets" CALENDAR_FILE="$SECRETS_DIRECTORY/calendar_ical_urls.json" TOKEN_FILE="$SECRETS_DIRECTORY/organizer_access_token.txt" ENVIRONMENT_FILE="$PROJECT_ROOT/.env" ENVIRONMENT_EXAMPLE="$PROJECT_ROOT/.env.example" MODE="" ROTATE_TOKEN=0 usage() { printf '%s\n' "Usage: $0 [--mode trusted-lan|bearer] [--rotate-token]" >&2 } while (($#)); do case "$1" in --mode) (($# >= 2)) || { usage; exit 2; } MODE="$2" shift 2 ;; --mode=*) MODE="${1#*=}" shift ;; --rotate-token) ROTATE_TOKEN=1 shift ;; -h|--help) usage exit 0 ;; *) usage exit 2 ;; esac done if [[ -n "$MODE" && "$MODE" != "trusted-lan" && "$MODE" != "bearer" ]]; then printf '%s\n' "Access mode must be trusted-lan or bearer." >&2 exit 2 fi if [[ ! -f "$ENVIRONMENT_EXAMPLE" ]]; then printf '%s\n' "Missing .env.example. Run this script from an intact release bundle." >&2 exit 1 fi mkdir -p -- "$SECRETS_DIRECTORY" chmod 700 -- "$SECRETS_DIRECTORY" if [[ ! -e "$ENVIRONMENT_FILE" ]]; then cp -- "$ENVIRONMENT_EXAMPLE" "$ENVIRONMENT_FILE" chmod 600 -- "$ENVIRONMENT_FILE" printf '%s\n' "Created .env from .env.example." else printf '%s\n' "Preserved the existing .env file." fi replace_calendars=1 if [[ -f "$CALENDAR_FILE" ]]; then read -r -p "A calendar secret already exists. Replace it? [y/N] " calendar_answer case "${calendar_answer,,}" in y|yes) replace_calendars=1 ;; *) replace_calendars=0 ;; esac fi if ((replace_calendars)); then calendar_pattern='^https://calendar[.]google[.]com/calendar/ical/[A-Za-z0-9._~%+@-]+/(private-[A-Za-z0-9._~%+@-]+|public)/basic[.]ics$' calendar_urls=() printf '%s\n' "Enter private Google Calendar iCal URLs. Input is hidden; submit a blank entry when finished." while true; do printf 'Calendar URL #%d: ' "$(( ${#calendar_urls[@]} + 1 ))" >&2 IFS= read -r -s calendar_url printf '\n' >&2 [[ -n "$calendar_url" ]] || break if [[ ! "$calendar_url" =~ $calendar_pattern ]]; then printf '%s\n' "That entry is not a supported private Google Calendar basic.ics HTTPS URL; it was not saved." >&2 unset calendar_url continue fi calendar_urls+=("$calendar_url") unset calendar_url done if ((${#calendar_urls[@]} == 0)); then printf '%s\n' "At least one calendar URL is required when creating the calendar secret." >&2 exit 1 fi calendar_temporary="$(mktemp "$SECRETS_DIRECTORY/.calendar-XXXXXXXX")" cleanup_calendar() { [[ -n "${calendar_temporary:-}" && -e "$calendar_temporary" ]] && rm -f -- "$calendar_temporary" } trap cleanup_calendar EXIT HUP INT TERM chmod 600 -- "$calendar_temporary" { printf '[' for calendar_index in "${!calendar_urls[@]}"; do ((calendar_index == 0)) || printf ',' printf '"%s"' "${calendar_urls[$calendar_index]}" done printf ']\n' } >"$calendar_temporary" mv -f -- "$calendar_temporary" "$CALENDAR_FILE" calendar_temporary="" trap - EXIT HUP INT TERM chmod 600 -- "$CALENDAR_FILE" printf 'Saved %d calendar source(s) without displaying their URLs.\n' "${#calendar_urls[@]}" unset calendar_urls else chmod 600 -- "$CALENDAR_FILE" printf '%s\n' "Preserved the existing calendar secret." fi has_existing_token=0 [[ -s "$TOKEN_FILE" ]] && has_existing_token=1 if [[ -z "$MODE" ]]; then default_mode="trusted-lan" ((has_existing_token)) && default_mode="bearer" read -r -p "Access mode [trusted-lan/bearer] (default: $default_mode) " selected_mode MODE="${selected_mode:-$default_mode}" if [[ "$MODE" != "trusted-lan" && "$MODE" != "bearer" ]]; then printf '%s\n' "Access mode must be trusted-lan or bearer." >&2 exit 2 fi fi if [[ "$MODE" == "trusted-lan" ]]; then if ((ROTATE_TOKEN)); then printf '%s\n' "--rotate-token can only be used with bearer mode." >&2 exit 2 fi if ((has_existing_token)); then read -r -p "This will erase the current access token and disable bearer authentication. Continue? [y/N] " erase_answer case "${erase_answer,,}" in y|yes) ;; *) printf '%s\n' "Configuration cancelled before changing the access mode." >&2; exit 1 ;; esac fi token_temporary="$(mktemp "$SECRETS_DIRECTORY/.token-XXXXXXXX")" chmod 600 -- "$token_temporary" : >"$token_temporary" mv -f -- "$token_temporary" "$TOKEN_FILE" chmod 600 -- "$TOKEN_FILE" printf '%s\n' "Configured trusted-LAN mode with an empty token file." elif ((has_existing_token)) && ((ROTATE_TOKEN == 0)); then chmod 600 -- "$TOKEN_FILE" printf '%s\n' "Preserved the existing bearer token. Use --rotate-token to replace it." else token_temporary="$(mktemp "$SECRETS_DIRECTORY/.token-XXXXXXXX")" cleanup_token() { [[ -n "${token_temporary:-}" && -e "$token_temporary" ]] && rm -f -- "$token_temporary" } trap cleanup_token EXIT HUP INT TERM chmod 600 -- "$token_temporary" if command -v openssl >/dev/null 2>&1; then openssl rand -base64 48 2>/dev/null | tr -d '\r\n=' | tr '+/' '-_' >"$token_temporary" elif command -v base64 >/dev/null 2>&1 && [[ -r /dev/urandom ]]; then dd if=/dev/urandom bs=48 count=1 2>/dev/null | base64 | tr -d '\r\n=' | tr '+/' '-_' >"$token_temporary" else printf '%s\n' "Bearer mode requires openssl, or base64 with /dev/urandom." >&2 exit 1 fi [[ -s "$token_temporary" ]] || { printf '%s\n' "Secure token generation failed." >&2; exit 1; } mv -f -- "$token_temporary" "$TOKEN_FILE" token_temporary="" trap - EXIT HUP INT TERM chmod 600 -- "$TOKEN_FILE" printf '%s\n' "Generated and stored a new bearer token. The token was not displayed." fi printf '%s\n' "Configuration is ready. Review .env, then run: docker compose up -d --build"