Compare commits

..

2 commits

8 changed files with 150 additions and 26 deletions

View file

@ -1,4 +1,4 @@
name: Publish Cargo Package
name: Publish Release
on:
push:
@ -11,8 +11,6 @@ jobs:
runs-on: ubuntu-22.04
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_FORGEJO_TOKEN: Bearer ${{ secrets.FORGEJO_CARGO_TOKEN }}
FORGEJO_CARGO_INDEX: ${{ github.server_url }}/${{ github.repository_owner }}/_cargo-index.git
steps:
- name: Check out repository
uses: https://data.forgejo.org/actions/checkout@v4
@ -29,17 +27,6 @@ jobs:
rustup default stable
cargo --version
- name: Configure Cargo registry
run: |
mkdir -p "$HOME/.cargo"
cat > "$HOME/.cargo/config.toml" <<EOF
[registries.forgejo]
index = "${FORGEJO_CARGO_INDEX}"
[net]
git-fetch-with-cli = true
EOF
- name: Verify tag matches package version
if: startsWith(github.ref, 'refs/tags/v')
run: |
@ -56,12 +43,17 @@ jobs:
exit 1
fi
- name: Check package can be published
- name: Build release binary
run: |
. "$HOME/.cargo/env"
cargo publish --dry-run --locked --registry forgejo
cargo build --release --locked
mv target/release/upvoters upvoters-linux-x86_64
- name: Publish package
run: |
. "$HOME/.cargo/env"
cargo publish --locked --registry forgejo
- name: Create release
uses: https://data.forgejo.org/actions/forgejo-release@v2
with:
direction: upload
tag: ${{ github.ref_name }}
release-notes: "Release ${{ github.ref_name }}"
files: upvoters-linux-x86_64
token: ${{ secrets.GITHUB_TOKEN }}

2
Cargo.lock generated
View file

@ -1961,7 +1961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uprs"
name = "upvoters"
version = "0.1.0"
dependencies = [
"anyhow",

View file

@ -1,7 +1,8 @@
[package]
name = "uprs"
name = "upvoters"
version = "0.1.0"
edition = "2024"
license = "MIT"
[dependencies]
anyhow = "1"

15
Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM rust:1.88-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
COPY src ./src
RUN touch src/main.rs && cargo build --release --locked
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/upvoters /usr/local/bin/upvoters
CMD ["upvoters"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Alex Selimov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,3 +1,86 @@
# uprs
# upvoters ☝️
Simple Rust backend for supporting anonymous like/upvote functionality in hugo sites.
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.
## 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.
## Requirements
- Rust (stable)
- PostgreSQL 16
## Configuration
The following environment variables are required:
| Variable | Description |
|------------------------------|----------------------------------------------------------------------------------------|
| `POSTGRES_CONNECTION_STRING` | PostgreSQL connection string (e.g. `postgres://user:password@localhost:5432/upvoters`) |
## Running locally
### With Docker Compose
Builds and starts both the database and app:
```sh
docker compose up --build
```
The server will be available at `http://localhost:3000`.
### Without Docker
Start the database:
```sh
docker compose up -d db
```
Run the server:
```sh
POSTGRES_CONNECTION_STRING=postgres://uprs:password123@localhost:5432/uprs cargo run
```
The server listens on port `3000`.
## API
| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/health` | Health check |
| `POST` | `/posts/{slug}/vote` | Upvote a post |
| `DELETE` | `/posts/{slug}/vote` | Remove a vote from a post |
| `GET` | `/posts/{slug}/votes` | Get vote count and whether the current user has voted |
Voter identity is tracked via a `voter_id` cookie. One is set automatically on first request.
### `GET /posts/{slug}/votes` response
```json
{
"vote_count": 42,
"voted": true
}
```
## Running tests
Tests require a running PostgreSQL instance at `postgres://uprs:password123@localhost:5432/uprs`.
```sh
cargo test
```
## License
MIT — see [LICENSE](LICENSE).

View file

@ -12,5 +12,17 @@ services:
- ./db/migrations:/docker-entrypoint-initdb.d # run initial schema
ports:
- "5432:5432"
app:
build: .
container_name: uprs_app
restart: always
environment:
POSTGRES_CONNECTION_STRING: postgres://uprs:password123@db:5432/uprs
ports:
- "3000:3000"
depends_on:
- db
volumes:
pgdata:

View file

@ -1,5 +1,5 @@
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use uprs::state::AppState;
use upvoters::state::AppState;
#[tokio::main]
async fn main() {
@ -14,7 +14,7 @@ async fn main() {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, uprs::app(AppState::new().await))
axum::serve(listener, upvoters::app(AppState::new().await))
.await
.unwrap();
}