diff --git a/README.md b/README.md index c6ce19b..bd17c2f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,21 @@ upvoters is a basic anonymous voting system that can be added to a blog. I recently reworked my personal blog with the Hugo Bear blog theme. I came across the [creator's (Herman Martinus) blogging site](https://herman.bearblog.dev/) which had this really cool anonymous upvote system. I wanted something similar and decided to implement it myself. -The goal is maximum simplicity. +The goal is maximum simplicity so that I can use it with my Hugo static site. +It's directly integrated in [my theme]("https://forge.alexselimov.com/aselimov/hugo-bearcub") which attempts to mimic Herman Martinus' bear blog style. +I've added steps on how I set it up in my theme [at the bottom of this README](#setting-it-up-with-hugo). -## Important Notes -This service does NOT have any authentication/authorization, so you should NOT serve this to the public web. -I'm using this by just serving it on the same VPS as my blog. +## Table of Contents +- [Requirements](#requirements) +- [Configuration](#configuration) +- [Running locally](#running-locally) +- [API](#api) +- [Running tests](#running-tests) +- [Actual deployment](#actual-deployment) +- [Setting it up with Hugo](#setting-it-up-with-hugo) +- [License](#license) ## Requirements @@ -24,6 +32,8 @@ The following environment variables are required: | Variable | Description | |------------------------------|----------------------------------------------------------------------------------------| | `POSTGRES_CONNECTION_STRING` | PostgreSQL connection string (e.g. `postgres://user:password@localhost:5432/upvoters`) | +| `ALLOWED_ORIGINS` | Comma-separated list of allowed CORS origins (e.g. `localhost:1313,example.com`) | +| `PORT` | Port to listen on (default: `3000`) | ## Running locally @@ -81,6 +91,185 @@ Tests require a running PostgreSQL instance at `postgres://uprs:password123@loca cargo test ``` +## Actual deployment + +Here are the instructions for how I deployed this service. You might be able to just run it through the docker-compose on a VPS but I already had a postgres server set up: + +1. Install dependencies (rust/postgres) (For debian server). +```bash +sudo apt update +sudo apt install -y postgresql postgresql-contrib +``` +2. Create `postgres` user to manage postgres and `upvoters` user to run the app: +```bash +# Create the system user that will run the app +sudo useradd --system --home /opt/upvoters --create-home upvoters + +# Create the PostgreSQL role with peer auth (no password needed) +sudo -u postgres psql -c "CREATE USER upvoters;" +sudo -u postgres psql -c "CREATE DATABASE upvoters OWNER upvoters;" + +# Enable peer auth for the upvoters role (append to pg_hba.conf) +echo "local upvoters upvoters peer" | sudo tee -a /etc/postgresql/16/main/pg_hba.conf +sudo systemctl reload postgresql +``` +3. Create the votes table as the `upvoters` user +```bash +sudo -u upvoters psql upvoters +``` +```sql +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE IF NOT EXISTS votes ( + slug text not null, + voter_id uuid not null, + created_at timestamptz NOT NULL DEFAULT timezone('utc'::text, now()), + primary key (slug, voter_id) +); +``` +4. Download the binary (You can also install rust and build yourself if you prefer). Replace `v0.2.0` with the latest release tag. +```bash +sudo curl https://forge.alexselimov.com/aselimov/upvoters/releases/download/v0.2.0/upvoters-linux-x86_64 --output /opt/upvoters/upvoters +sudo chmod +x /opt/upvoters/upvoters +sudo chown upvoters /opt/upvoters/upvoters +``` +5. Create a systemd service at /etc/systemd/system/upvoters.service +``` +[Unit] +Description=upvoters rust backend +After=network.target postgresql.service + +[Service] +User=upvoters +Group=upvoters +WorkingDirectory=/opt/upvoters +ExecStart=/opt/upvoters/upvoters +Restart=on-failure +# The below connection string works if you have peer auth enabled and have postgresql running on +# a socket +Environment='POSTGRES_CONNECTION_STRING=postgresql:///upvoters?host=/var/run/postgresql' +Environment='ALLOWED_ORIGINS=https://example.com' +# Specify a PORT if the default 3000 is already taken +Environment='PORT=3000' + +[Install] +WantedBy=multi-user.target +``` +6. Enable and start the service +```bash +sudo systemctl enable --now upvoters +``` +7. Test from VPS using curl +```bash +curl https://example.com/api/posts/tests/votes +``` + +## Setting it up with Hugo + +I set this up in my [Voting Bear Cub Theme](https://forge.alexselimov.com/aselimov/hugo-bearcub/src/branch/main). +The components are: +1. Javascript file to handle upvoting. This function is automatically called on page loads +```javascript + +(async function () { + // Define the slug + const slug = location.pathname.replace(/^\/|\/$/g, "").replaceAll("/", "-"); + const API = window.UPVOTE_API; + + const btn = document.getElementById("upvote-btn"); + const count = document.getElementById("upvote-count"); + + // On load fetch curent vote count and whether the button should be clicked on or off + const res = await fetch(`${API}/posts/${slug}/votes`, { + credentials: "include", + }); + const data = await res.json(); + + count.textContent = data.vote_count; + if (data.voted) btn.setAttribute("voted", true); + + btn.addEventListener("click", async () => { + const alreadyVoted = btn.hasAttribute("voted"); + + const method = alreadyVoted ? "DELETE" : "POST"; + const r = await fetch(`${API}/posts/${slug}/vote`, { + method, + credentials: "include", + }); + + if (r.ok) { + const updated = await fetch(`${API}/posts/${slug}/votes`, { + credentials: "include", + }); + const d = await updated.json(); + count.textContent = d.vote_count; + if (d.voted) btn.setAttribute("voted", false); + else btn.removeAttribute("voted"); + } + }); +})(); +``` +2. Button component in single.html template placed after the main post body. **Note:** I guard this behind a config check so users can use my theme without setting up upvoters. +```html +{{ if .Site.Params.upvotes }} +