diff --git a/README.md b/README.md index c371254..0385052 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,336 @@ -# web_server +# My Web -Dockerized multi-service personal website. +![screenshot](vue/public/img/screenshot.png) -## Untracked Files Requiring Manual Setup +Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting. -These files are git-ignored and must be created or obtained manually before running the stack. +This website is self-hosted on my Raspberry Pi. Any interference and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :). + +## Architecture + +All services run in Docker containers orchestrated by Docker Compose behind Nginx as a reverse proxy on a single bridge network. + +``` +vue ── Frontend build (outputs dist to shared volume) +nginx (80, 443) ── Frontend SPA + Reverse Proxy +backend (8080) ── Go API (GraphQL + REST) +db (5432) ── PostgreSQL 16 +icecast2 (8000) ── Audio Streaming (Icecast2 + Liquidsoap) +gitea (3000) ── Self-Hosted Git +quartz (8080) ── Obsidian Notes Publisher (Quartz v4.4.0) +searxng (8080) ── Meta Search Engine +hasura (8080) ── Hasura GraphQL Engine +certbot ── SSL Certificate Management (disabled in dev) +``` + +## Tech Stack + +**Frontend** - Vue 3, Vite, Tailwind CSS v4, Pinia, Vue Router, markdown-it (wikilinks + KaTeX), Rust/WASM + +**Backend** - Go (Gin), gqlgen (GraphQL), GORM, PostgreSQL, JWT auth, WebSockets + +**Integrations** - Spotify API, Steam API, Anthropic Claude API, Icecast2 + +**Infrastructure** - Docker Compose, Nginx, Let's Encrypt (Certbot), Gitea + Act Runner + +## Features + +- Spotify integration (currently playing, recently played) +- Steam integration (online status, recent games) +- Obsidian note viewer via Quartz +- Live radio streaming via Icecast2 + Liquidsoap +- Real-time chat over WebSockets with image/video uploads +- Blog with admin panel (CRUD) +- Activity and rowing session tracking +- Fan shrines (GTO, Evangelion, Demoman, Skip Skip Benben) +- Self-hosted Git (Gitea) with CI/CD and commit feed on homepage +- Claude AI integration +- Printable CV with role-specific sections +- SearXNG meta search engine +- Hasura GraphQL console +- Landing page with animated stamps section +- Route transitions (slide/fade) and performance optimizations (gzip, WOFF2 fonts, lazy loading) + +## Pages + +| Route | Description | +| -------------- | ------------------------------------- | +| `/` | Landing page | +| `/stp` | Home dashboard with grid layout | +| `/admin` | Admin panel (authenticated) | +| `/cv` | Curriculum Vitae (printable) | +| `/bookmarks` | Bookmarks | +| `/notes/:path` | Obsidian note viewer (via Quartz) | +| `/shrines` | Fan shrine index + individual shrines | + +## API + +### GraphQL + +**Endpoint:** `POST /api/graphql` +**Playground:** `GET /api/graphql` (when `GQL_PLAYGROUND=true`) + +| Operation | Type | Description | +| --- | --- | --- | +| `users` | Query | Get all users | +| `user(id)` | Query | Get user by ID | +| `me` | Query | Get authenticated user | +| `posts` | Query | Get all posts | +| `post(id)` | Query | Get post by ID | +| `activities` | Query | Get all activities | +| `favorites` | Query | Get all favorites | +| `rowingSessions` | Query | Get all rowing sessions | +| `messages` | Query | Get all messages | +| `spotifyListening` | Query | Currently playing track | +| `spotifyRecent` | Query | Recently played tracks | +| `giteaFeed` | Query | Latest Gitea activity | +| `steamStatus` | Query | Steam online status | +| `login` | Mutation | Authenticate user | +| `logout` | Mutation | Logout | +| `refreshToken` | Mutation | Refresh auth token | +| `createPost` / `updatePost` / `deletePost` | Mutation | Post CRUD (admin) | +| `createUser` / `deleteUser` | Mutation | User management (admin) | +| `setUserAdmin` | Mutation | Toggle admin status | +| `createFavorite` | Mutation | Add favorite (admin) | +| `createActivity` | Mutation | Add activity (admin) | + +### REST Endpoints + +**Auth** +| Method | Path | Description | +| --- | --- | --- | +| POST | `/api/auth/login` | Login (rate limited: 5/min) | +| POST | `/api/auth/refresh` | Refresh token | +| GET | `/api/auth/check` | Check token validity | +| POST | `/api/auth/logout` | Logout | + +**Spotify** +| Method | Path | Description | +| --- | --- | --- | +| GET | `/api/spotify/callback` | OAuth callback | +| GET | `/api/spotify/listening` | Currently playing | +| GET | `/api/spotify/recent` | Recently played | + +**Public** +| Method | Path | Description | +| --- | --- | --- | +| GET | `/api/favorites` | Get all favorites | +| GET | `/api/rowing` | Get all rowing sessions | +| GET | `/api/activity` | Get all activities | +| GET | `/api/posts` | Get all posts | +| GET | `/api/posts/:id` | Get post by ID | +| GET | `/api/user` | Get all users | +| GET | `/api/user/:id` | Get user by ID | + +**Protected (auth required)** +| Method | Path | Description | +| --- | --- | --- | +| POST | `/api/messages/upload` | Upload message file (rate limited: 5/min) | + +**Admin (auth + admin required)** +| Method | Path | Description | +| --- | --- | --- | +| POST | `/api/favorites` | Create favorite | +| POST | `/api/rowing` | Create rowing session | +| POST | `/api/activity` | Create activity | +| POST | `/api/posts` | Create post | +| PUT | `/api/posts/:id` | Update post | +| DELETE | `/api/posts/:id` | Delete post | +| POST | `/api/user` | Create user | +| PUT | `/api/user/:id` | Update user | +| DELETE | `/api/user/:id` | Delete user | +| PATCH | `/api/user/:id/admin` | Set/unset admin | +| POST | `/api/radio/upload` | Upload radio song | +| GET | `/api/radio/songs` | List radio songs | +| DELETE | `/api/radio/songs/:filename` | Delete radio song | +| PATCH | `/api/radio/songs/:filename/disable` | Disable radio song | +| PATCH | `/api/radio/songs/:filename/enable` | Enable radio song | + +**WebSocket** +| Method | Path | Description | +| --- | --- | --- | +| GET | `/api/ws` | WebSocket chat (10s ping keepalive) | + +### Nginx Proxy Routes + +| Route | Target | Notes | +| --- | --- | --- | +| `/api` | backend:8080 | API (rate limited: 30r/s) | +| `/radio` | icecast:8000 | Audio streaming | +| `/gitea` | gitea:3000 | Git service | +| `/hasura` | hasura:8080 | GraphQL console + WebSocket | +| `/notes` | quartz:8080 | Obsidian notes | +| `/searxng` | searxng:8080 | Search engine | +| `/uploads` | local alias | User-uploaded files | + +### Deprecated Endpoints + +**Backend note API** (`GET /api/notes/*path`) - The backend has a REST endpoint that serves note files directly from the mounted Obsidian vault. This is superseded by the Quartz service which now handles note rendering at `/notes/`. The backend endpoint still exists in code but is no longer the primary note serving path. + +## Setup ### `.env` -Environment variables used by all services. No example file is provided — see `docker-compose.yml` for the full list of referenced variables (database credentials, hostnames, secrets, Spotify OAuth, Gitea tokens, etc.). +Create a `.env` file in the project root. All services read from this file. -### `gitea/config/app.ini` +| Variable | Description | +| --- | --- | +| `POSTGRES_USER` | PostgreSQL username | +| `POSTGRES_PASSWORD` | PostgreSQL password | +| `POSTGRES_DB` | Main app database name | +| `POSTGRES_PORT` | PostgreSQL port (typically `5432`) | +| `POSTGRES_HOST` | PostgreSQL hostname (use `db` for Docker) | +| `GITEA_HOST` | Gitea hostname (use `gitea` for Docker) | +| `GITEA_PORT` | Gitea HTTP port (typically `3000`) | +| `GITEA_INTERNAL_TOKEN` | Gitea internal API token (generate with `gitea generate secret INTERNAL_TOKEN`) | +| `GITEA_LFS_JWT_SECRET` | Gitea LFS JWT secret (generate with `gitea generate secret LFS_JWT_SECRET`) | +| `GITEA_OAUTH2_JWT_SECRET` | Gitea OAuth2 JWT secret (generate with `gitea generate secret JWT_SECRET`) | +| `POSTGRES_GITEA_DB` | Gitea database name | +| `UPTIMEKUMA_HOST` | Uptime Kuma hostname (planned) | +| `UPTIMEKUMA_PORT` | Uptime Kuma port (planned) | +| `SEARXNG_HOST` | SearXNG hostname (use `searxng` for Docker) | +| `SEARXNG_PORT` | SearXNG port (typically `8080`) | +| `SEARXNG_SECRET_KEY` | SearXNG secret key (random hex string) | +| `WALLABAG_HOST` | Wallabag hostname (planned) | +| `WALLABAG_PORT` | Wallabag port (planned) | +| `QUARTZ_HOST` | Quartz hostname (use `quartz` for Docker) | +| `QUARTZ_PORT` | Quartz port (typically `8080`) | +| `GITEA_RUNNER_HOST` | Gitea runner hostname | +| `GITEA_RUNNER_NAME` | Gitea runner display name | +| `GITEA_RUNNER_REGISTRATION_TOKEN` | Token to register Gitea Actions runner | +| `BACKEND_PORT` | Backend port (typically `8080`) | +| `BACKEND_HOST` | Backend hostname (use `backend` for Docker) | +| `BACKEND_SECRET` | JWT signing secret | +| `BACKEND_ENDPOINT` | API path prefix (typically `/api`) | +| `OBSIDIAN_DIR` | Absolute path to Obsidian vault on host machine | +| `SPOTIFY_CLIENT_ID` | Spotify app client ID | +| `SPOTIFY_CLIENT_SECRET` | Spotify app client secret | +| `SPOTIFY_REDIRECT_URI` | Spotify OAuth redirect (e.g. `https://www./api/spotify/callback`) | +| `SPOTIFY_AUTH_STATE` | Arbitrary state string for Spotify OAuth | +| `ICECAST_SOURCE_PASSWORD` | Icecast source connection password | +| `ICECAST_RELAY_PASSWORD` | Icecast relay password | +| `ICECAST_ADMIN_USER` | Icecast admin username | +| `ICECAST_ADMIN_PASSWORD` | Icecast admin password | +| `ICECAST_HOST` | Icecast hostname (use `icecast` for Docker) | +| `ICECAST_PORT` | Icecast port (typically `8000`) | +| `ICECAST_MOUNT` | Icecast mount point (e.g. `/stream`) | +| `LIQUIDSOAP_HARBOR_MOUNT` | Liquidsoap live input mount (e.g. `/live`) | +| `LIQUIDSOAP_HARBOR_PORT` | Liquidsoap harbor port (e.g. `8005`) | +| `DOMAIN` | Production domain name | +| `EMAIL` | Email for Let's Encrypt registration | +| `CLAUDE_API_KEY` | Anthropic Claude API key | +| `STEAM_API_KEY` | Steam Web API key | +| `STEAM_ID` | Steam user ID | +| `HASURA_GRAPHQL_ADMIN_SECRET` | Hasura admin secret | +| `HASURA_HOST` | Hasura hostname (use `hasura` for Docker) | +| `HASURA_PORT` | Hasura port (typically `8080`) | +| `SEED_DB` | Set to `true` to seed test data on startup | -Gitea application config. Copy from the template and fill in secrets: +### Gitea Config + +Copy from the template and fill in secrets: ```sh cp gitea/config/app.ini.template gitea/config/app.ini ``` -Populate `LFS_JWT_SECRET`, `SECRET_KEY`, `INTERNAL_TOKEN`, `JWT_SECRET`, and the database `PASSWD`. +Populate `LFS_JWT_SECRET`, `SECRET_KEY`, `INTERNAL_TOKEN`, `JWT_SECRET`, and the database `PASSWD`. Alternatively, the Gitea entrypoint generates `app.ini` from the template using environment variables. -### `searxng/settings.yml` +### SearXNG Config -SearXNG settings. Copy from the template: +Copy from the template: ```sh cp searxng/settings.yml.template searxng/settings.yml ``` -The template uses environment variable substitution (`${BASE_URL}`, `${SEARXNG_SECRET_KEY}`) at container build time, so this file is generated by the Dockerfile's `entrypoint.sh`. If running outside Docker, fill in values manually. +The Docker entrypoint handles environment variable substitution (`${BASE_URL}`, `${SEARXNG_SECRET_KEY}`) automatically, so manual setup is only needed when running outside Docker. -### `certbot/conf/` and `certbot/www/` +### Spotify Token Setup -Let's Encrypt certificate storage. In production, certbot populates these automatically on first run. For local/dev use, either: +1. Create a Spotify app at [developer.spotify.com](https://developer.spotify.com/dashboard) and set the redirect URI to `https://www./api/spotify/callback` +2. Set `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI`, and `SPOTIFY_AUTH_STATE` in `.env` +3. Start the stack, then visit the Spotify auth URL logged by the backend on startup to authorize the app +4. After authorization, Spotify redirects to the callback endpoint which stores tokens at `/backend/token/spotify_token.json` +5. Tokens are refreshed automatically; the file persists across container restarts via volume mount -- Use dev mode (`docker-compose.dev.yml`) which skips SSL, or -- Place self-signed certs in `certbot/conf/live/localhost/` (`fullchain.pem`, `privkey.pem`). +### Obsidian Notes Setup -### `backend/token/` +1. Set `OBSIDIAN_DIR` in `.env` to the absolute path of your Obsidian vault on the host machine +2. The vault is mounted read-only into the Quartz container at `/quartz/content` +3. Quartz builds a static site from the vault on startup and serves it at `/notes/` +4. The backend also mounts the vault at `/backend/notes` (legacy, see deprecated endpoints above) -Directory where the backend persists Spotify OAuth tokens (`spotify_token.json`). Created automatically at runtime — no manual setup needed, but the directory is git-ignored so it won't exist on a fresh clone. Docker mounts `./backend/token/:/backend/token` so the directory is created by Docker. +### SSL Certificates (Certbot) -### `icecast2/fallback_music/` +**Initial setup (production):** -MP3 files used as fallback music for the Icecast2/Liquidsoap radio stream. Place at least one `.mp3` file here. A `.gitkeep` is tracked to preserve the directory. +1. Set `DOMAIN` and `EMAIL` in `.env` +2. On first run, Nginx starts with `nginx_setup.conf.template` which only serves the ACME challenge route at `/.well-known/acme-challenge/` +3. Certbot requests a certificate via `certbot certonly --webroot` +4. Once the certificate is issued to `certbot/conf/live//`, restart Nginx — it will detect the certs and switch to the full `nginx.conf.template` +5. Certbot checks for renewal every 12 hours automatically -### `gitea-runner/act_runner` +**Dev mode:** Nginx generates a self-signed certificate for localhost automatically. -The Gitea Actions runner binary. Download from [Gitea's releases](https://gitea.com/gitea/act_runner/releases) for your platform and place in `gitea-runner/`. +### Icecast Radio -### `gitea-runner/.runner` +Place at least one `.mp3` file in `icecast2/fallback_music/`. Liquidsoap plays these as fallback when no live source is connected. Connect a live source to port 8001 on the `LIQUIDSOAP_HARBOR_MOUNT`. -Runner registration state file. Generated automatically when `gitea-runner/run.sh` runs for the first time (requires `GITEA_RUNNER_REGISTRATION_TOKEN` in `.env`). +### Gitea Runner + +1. Download the `act_runner` binary from [Gitea releases](https://gitea.com/gitea/act_runner/releases) and place in `gitea-runner/` +2. Set `GITEA_RUNNER_REGISTRATION_TOKEN` in `.env` +3. The runner registers automatically on first startup + +## Dev Mode + +Run the full stack over plain HTTP without SSL certificates: + +```sh +docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build +``` + +This: +- Uses an HTTP-only Nginx config with all routing (SPA, backend proxy, radio, gitea, etc.) +- Generates a self-signed certificate for localhost +- Disables certbot +- Seeds the database with test data (`SEED_DB=true`) +- Enables GraphQL playground and introspection +- Enables Hasura console and dev mode + +Visit `http://localhost` to test. + +### Frontend only (hot reload) + +```sh +cd vue && npm run dev +``` + +Vite dev server proxies `/api` to `localhost:8080`, `/gitea` to `localhost:3000`, `/radio` to `localhost:8000`. + +## Untracked Files + +These files are git-ignored and must be created manually: + +| File | Notes | +| --- | --- | +| `.env` | See setup section above | +| `gitea/config/app.ini` | Copy from `app.ini.template` or let entrypoint generate it | +| `searxng/settings.yml` | Copy from `settings.yml.template` or let entrypoint generate it | +| `certbot/conf/`, `certbot/www/` | Created automatically by certbot; use dev mode to skip | +| `backend/token/` | Created automatically by Docker volume mount | +| `icecast2/fallback_music/*.mp3` | Place at least one MP3 file | +| `gitea-runner/act_runner` | Download from Gitea releases | +| `gitea-runner/.runner` | Generated on first runner startup | + +## Future Ideas + +- More Rust to WASM +- ML for chatboards +- Cache requests +- Design more webpages +- Calendar to show radio times +- Nice smooth function background and transitions +- Design shrines +- Redis (not really but practical experience) diff --git a/readme.md b/readme.md deleted file mode 100644 index b4bf257..0000000 --- a/readme.md +++ /dev/null @@ -1,143 +0,0 @@ -# My Web - -## Important TODO - -- Get a new background - -![screenshot](vue/public/img/screenshot.png) - -Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting. - -This website is currently self hosted on my Raspberry Pi. Any interference and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :). - -## Architecture - -All services run in Docker containers orchestrated by Docker Compose: - -``` -vue ── Frontend build (outputs dist to shared volume) -nginx (80, 443) ── Frontend SPA + Reverse Proxy -backend (8080) ── Go API -db (5432) ── PostgreSQL 16 -icecast2 (8000) ── Audio Streaming -gitea (3000) ── Self-Hosted Git -certbot ── SSL Certificate Management -``` - -## Tech Stack - -**Frontend** - Vue 3, Vite, Tailwind CSS v4, Pinia, Vue Router, markdown-it (wikilinks + KaTeX), Rust/WASM - -**Backend** - Go (Gin), gqlgen (GraphQL), GORM, PostgreSQL, JWT auth, WebSockets - -**Integrations** - Spotify API, Steam API, Anthropic Claude API, Icecast2 - -**Infrastructure** - Docker Compose, Nginx, Let's Encrypt (Certbot), Gitea + Act Runner - -## Features - -- Spotify integration (currently playing, recently played) -- Steam integration (online status, recent games) -- Obsidian note viewer with wikilink and LaTeX support -- Live radio streaming via Icecast2 -- Real-time chat over WebSockets with image/video uploads -- Blog with admin panel (CRUD) -- Activity and rowing session tracking -- Fan shrines (GTO, Evangelion, Demoman, Skip Skip Benben) -- Self-hosted Git (Gitea) with CI/CD and commit feed on homepage -- Claude AI integration -- Printable CV with role-specific sections -- Landing page with animated stamps section -- Route transitions (slide/fade) and performance optimizations (gzip, WOFF2 fonts, lazy loading) - -## Pages - -| Route | Description | -| -------------- | ------------------------------------- | -| `/` | Landing page | -| `/stp` | Home dashboard with grid layout | -| `/admin` | Admin panel (authenticated) | -| `/cv` | Curriculum Vitae (printable) | -| `/bookmarks` | Bookmarks | -| `/notes/:path` | Obsidian note viewer | -| `/shrines` | Fan shrine index + individual shrines | - -## API - -The primary API is **GraphQL** at `POST /api/graphql` (with a playground at `GET /api/graphql`). Queries cover posts, users, favorites, activities, rowing sessions, Spotify (currently playing, recently played), Gitea feed, and messages. - -REST endpoints handle auth (`/auth/*`), Spotify OAuth (`/spotify/*`), file uploads (`/messages/upload`), note serving (`/notes/*`), and WebSocket chat (`/api/ws`). Steam data (online status, recent games) is also available via the GraphQL API. - -Protected endpoints require JWT authentication via `/auth/login` (tokens set as HTTP-only cookies). - -## Local Testing (Dev Mode) - -Run the full stack over plain HTTP without SSL certificates: - -``` -docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build -``` - -This uses an HTTP-only nginx config with all routing (SPA, backend proxy, radio, gitea) and disables certbot. Visit `http://localhost` to test. - -## Future Ideas - -- More Rust to WASM -- ML for chatboards -- Cache requests -- Design more webpages -- Calendar to show radio times -- Nice smooth function background and transitions -- Design shrines -- Redis (not really but practical experience) - -## .env - -These environment variables are found in the `.env` file. The use of environment variables can be found by reading the code so the security of the variable names are not significant. - -``` -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_DB= -POSTGRES_PORT= -POSTGRES_HOST= - -GITEA_HOST= -GITEA_PORT= -POSTGRES_GITEA_DB= - - -BACKEND_PORT= -BACKEND_HOST= -BACKEND_SECRET= -BACKEND_ENDPOINT= - -CLAUDE_API_KEY= - -SEED_DB= - -OBSIDIAN_DIR= - -SPOTIFY_CLIENT_ID= -SPOTIFY_CLIENT_SECRET= -SPOTIFY_REDIRECT_URI= -SPOTIFY_AUTH_STATE= - -STEAM_API_KEY= -STEAM_ID= - -ICECAST_SOURCE_PASSWORD= -ICECAST_RELAY_PASSWORD= -ICECAST_ADMIN_USER= -ICECAST_ADMIN_PASSWORD= -ICECAST_HOST= -ICECAST_PORT= -ICECAST_MOUNT= - -DOMAIN= -EMAIL= - -GITEA_LFS_JWT_SECRET= -GITEA_INTERNAL_TOKEN= -GITEA_OAUTH2_JWT_SECRET= -```