Compare commits
No commits in common. "master" and "v0.2.0" have entirely different histories.
1 changed files with 4 additions and 193 deletions
197
README.md
197
README.md
|
|
@ -4,21 +4,13 @@ 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 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 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.
|
I wanted something similar and decided to implement it myself.
|
||||||
The goal is maximum simplicity so that I can use it with my Hugo static site.
|
The goal is maximum simplicity.
|
||||||
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
|
||||||
|
|
||||||
## Table of Contents
|
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.
|
||||||
|
|
||||||
- [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
|
## Requirements
|
||||||
|
|
||||||
|
|
@ -32,8 +24,6 @@ The following environment variables are required:
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|------------------------------|----------------------------------------------------------------------------------------|
|
|------------------------------|----------------------------------------------------------------------------------------|
|
||||||
| `POSTGRES_CONNECTION_STRING` | PostgreSQL connection string (e.g. `postgres://user:password@localhost:5432/upvoters`) |
|
| `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
|
## Running locally
|
||||||
|
|
||||||
|
|
@ -91,185 +81,6 @@ Tests require a running PostgreSQL instance at `postgres://uprs:password123@loca
|
||||||
cargo test
|
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 }}
|
|
||||||
<div class="upvote">
|
|
||||||
<button id="upvote-btn" aria-label="Upvote">
|
|
||||||
<span id="upvote-chevron">⌃</span>
|
|
||||||
<span id="upvote-count">—</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<script>window.UPVOTE_API = "{{ .Site.Params.upvoteApi }}";</script>
|
|
||||||
<script src="/upvote.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
```
|
|
||||||
3. Styles for the button:
|
|
||||||
```css
|
|
||||||
.upvote {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: fit-content;
|
|
||||||
gap: 0.2rem;
|
|
||||||
margin-block: var(--spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#upvote-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: currentColor;
|
|
||||||
font-size: calc(var(--size) * 4);
|
|
||||||
line-height: 0.8;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
opacity: 0.4;
|
|
||||||
transition: opacity 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upvote-btn:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upvote-btn[voted] {
|
|
||||||
color: var(--color-primary);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upvote-chevron {
|
|
||||||
display: block;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: -0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upvote-count {
|
|
||||||
font-size: calc(var(--size) * 1.6);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](LICENSE).
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue