--- title: "Admin Setup" author: - "Mark Padgham" date: "`r Sys.Date()`" vignette: > %\VignetteIndexEntry{Admin setup} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set ( collapse = TRUE, warning = TRUE, message = TRUE, width = 120, comment = "#>", fig.retina = 2, fig.path = "README-" ) ``` This vignette describes the one-time infrastructure steps required to deploy `roreviewapi` on a Digital Ocean droplet, including the editor volunteer search feature introduced in v0.2. ## Prerequisites - SSH access to the Digital Ocean droplet - A [Postmark](https://postmarkapp.com) account with a verified sender address - Access to the AirTable base containing the editor-in-chief rotation table - Ability to request DNS records for your institution's domain ## Environment variables The following environment variables must be set in the `Dockerfile` before building the image. Each appears twice: once as an `ENV` declaration at the top of the file, and once in the `~/.Renviron` block that makes the value available to R at runtime. | Variable | Description | |---|---| | `GITHUB_PAT` | GitHub personal access token | | `POSTMARK_API_TOKEN` | Postmark server API token (from the Postmark dashboard) | | `POSTMARK_FROM` | Verified sender address registered with Postmark | | `AIRTABLE_API_KEY` | AirTable personal access token | | `AIRTABLE_BASE_ID` | ID of the AirTable base containing the EiC rotation table | | `ROREVIEWAPI_BASE_URL` | Public HTTPS base URL of the deployed API (`https://reviewbot.ropensci.org`) | | `PKGCHECK_TOKEN` | pkgcheck authentication token (set directly in `~/.Renviron`) | Replace each `` in `Dockerfile` with the real value before building. ## Postmark 1. Create a Postmark account at . 2. Add and verify a sender address under **Sender Signatures**. This address will appear as the `From:` address on all outgoing emails and must be set as `POSTMARK_FROM`. 3. Navigate to **Servers → Your Server → API Tokens** and copy the server API token. Set this as `POSTMARK_API_TOKEN` in the `Dockerfile`. ## AirTable 1. Generate a personal access token at with at least `data.records:read` scope on the relevant base. Set this as `AIRTABLE_API_KEY`. 2. Open the AirTable base and copy the base ID from the URL (`https://airtable.com/appXXXXXXXX/...`). Set this as `AIRTABLE_BASE_ID`. The `editor-in-chief-rotation` table within that base must have `period_start`, `period_end`, and `acting_eic_email` fields. ## Droplet preparation Create the persistent data directory that will be bind-mounted into the container. This directory holds the SQLite database and the notify-email cache file and must survive container rebuilds. ```bash sudo mkdir -p /srv/roreviewapi/data sudo chmod 700 /srv/roreviewapi/data ``` ## DNS Ask your DNS administrator to add an `A` record pointing your chosen subdomain to the droplet's IP address, for example: ``` review.example.org. IN A ``` Allow up to 24 hours for propagation, though it is usually much faster. ## TLS certificate Once the DNS record is live, obtain a Let's Encrypt certificate on the droplet. The `--standalone` method requires port 80 to be free (stop the running stack first if necessary): ```bash sudo apt install certbot sudo certbot certonly --standalone -d review.example.org ``` The certificate and key are written to `/etc/letsencrypt/live/review.example.org/`. Certbot installs a systemd timer that renews certificates automatically; confirm it is active: ```bash systemctl status certbot.timer ``` ### Troubleshooting: HTTP-01 challenge failed The HTTP-01 challenge works by certbot binding a temporary server to port 80. Two things can prevent this: **Port 80 occupied.** The Docker stack must be stopped before running certbot, otherwise nginx holds port 80 and the challenge fails immediately: ```bash docker-compose down sudo certbot certonly --standalone -d review.example.org docker-compose up -d ``` **Port 80 blocked by a firewall.** There are two independent firewalls to check: *Droplet firewall (ufw):* ```bash sudo ufw status ``` If ufw is active and port 80 is not listed, open it: ```bash sudo ufw allow 80/tcp sudo ufw allow 443/tcp ``` *Digital Ocean cloud firewall:* in the DO console under **Networking → Firewalls**, confirm there is an inbound rule allowing TCP port 80 from all sources. The cloud firewall sits in front of the droplet and blocks traffic before it reaches ufw, so both layers must allow port 80. To confirm DNS has propagated before retrying certbot, check that the A record resolves to the droplet IP: ```bash dig +short review.example.org ``` ## Deployment Set `NGINX_SERVER_NAME` in the environment before starting the stack — this value is substituted into `nginx.conf` at container startup: ```bash export NGINX_SERVER_NAME=review.example.org docker-compose up -d --build ``` The compose file: - Mounts `/srv/roreviewapi/data` into the plumber container at `/data/email`, and sets `ROREVIEWAPI_EMAIL_DB=/data/email/searches.sqlite` so the SQLite database persists across rebuilds. - Mounts `/etc/letsencrypt` read-only into the nginx container so the certificate is available. - Exposes ports 80 (HTTP → HTTPS redirect) and 443 (HTTPS). ## Editor search endpoints Four endpoints support the volunteer editor search workflow. All require the shared `secret` token. ### `GET /send_search` Fetches current editor email addresses from AirTable and GitHub, inserts a search record into the database, and dispatches personalised click-link emails via Postmark. The submission type (standard vs. stats) is determined automatically from the GitHub issue template. Parameters: `repourl`, `repo` (org/repo), `issue_id`, `secret`. ### `GET /click/` Records a volunteer response. Returns an HTTP 200 confirmation page, an "already used" page on duplicate clicks, or an "expired" page if the search has been deactivated. Sends a notification email to the current editor-in-chief on the first valid click. No authentication required — the token itself is the credential. ### `GET /list_searches` Returns a data frame of all active and inactive searches with recipient totals and click counts. Use this to find the `repourl` value needed to deactivate a search. Parameters: `secret`. ### `GET /deactivate_search` Marks the search inactive (preventing further click responses) and permanently deletes all recipient rows and the search record from the database. Parameters: `repourl`, `secret`. ## Notify email cache At startup, `serve_api()` fetches the current editor-in-chief email address from AirTable and writes it to `/data/email/notify_email.txt` (alongside the SQLite database). This cache is refreshed every 24 hours via a background timer. If the AirTable call fails, the existing cached value is preserved and an error is logged. The cached address is used as the notification recipient whenever a volunteer clicks a search link.