284 lines
17 KiB
Markdown
284 lines
17 KiB
Markdown
# LinkLog
|
|
|
|
LinkLog is a Firefox extension and Python web service for saving links with a title, comment, timestamp, and tracking parameters removed. The service stores links in SQLite and can publish them through plugins, including Mastodon.
|
|
|
|
For full transparency: The author used vibe coding to create this software.
|
|
|
|
## Project Layout
|
|
|
|
```text
|
|
backend/ FastAPI application, services, database, and tests
|
|
frontend/ Jinja templates and browser-side assets
|
|
webextension/ Firefox Manifest V3 extension
|
|
Logo.svg Source logo artwork used by the web and extension interfaces
|
|
Dockerfile Backend container image
|
|
docker-compose.yml App with traefik reverse proxy hooks
|
|
REQUIREMENTS.md Product requirements
|
|
VIBE/ Conversation and prompt logs
|
|
```
|
|
|
|
## Requirements
|
|
|
|
For local development:
|
|
|
|
- macOS, Linux, or Windows with Python 3.11+
|
|
- Firefox for installing the extension
|
|
- Docker Desktop and Docker Compose v2 for the container workflow
|
|
|
|
The backend currently uses FastAPI, uvicorn, SQLite, and Pydantic. `httpx2` is included for the Starlette-compatible test client.
|
|
Jinja2 is included for server-rendered HTML templates.
|
|
The backend version is `0.1.0` and is exposed through the FastAPI/OpenAPI metadata. It can be overridden with `LINKLOG_VERSION`.
|
|
LinkLog is licensed under the GNU General Public License, version 3 or any later version. See [LICENSE](LICENSE).
|
|
|
|
## Local Installation
|
|
|
|
From the repository root:
|
|
|
|
```sh
|
|
python3.11 -m venv .venv
|
|
. .venv/bin/activate
|
|
python -m pip install --upgrade pip
|
|
python -m pip install -r backend/requirements.txt
|
|
```
|
|
|
|
On Windows PowerShell, activate the environment with:
|
|
|
|
```powershell
|
|
.venv\Scripts\Activate.ps1
|
|
```
|
|
|
|
The SQLite database is created automatically at `backend/data/linklog.db` when the application starts. On a fresh installation, open `/setup` and create the first administrator. No default user accounts are created by the application. The setup page also collects SMTP settings and requires a successful test email before creating the administrator.
|
|
|
|
The database schema is versioned with SQLite `PRAGMA user_version`. Application startup applies all pending migrations in order, so updating the application does not require deleting an existing database. New schema changes should be added as a new numbered migration in `backend/app/database.py`; existing migration entries must remain unchanged.
|
|
## Run The Backend
|
|
|
|
Activate the virtual environment, then run uvicorn from the repository root:
|
|
|
|
```sh
|
|
. .venv/bin/activate
|
|
PYTHONPATH=. uvicorn backend.app.main:app --reload --host 127.0.0.1 --port 8000
|
|
```
|
|
|
|
Open these URLs:
|
|
|
|
- Public feed: <http://localhost:8000/>
|
|
- User feed: <http://localhost:8000/alice>
|
|
- Profile settings: <http://localhost:8000/profile>
|
|
- Labels: <http://localhost:8000/labels>
|
|
- About: <http://localhost:8000/about>
|
|
- Admin page: <http://localhost:8000/admin>
|
|
- Web login: <http://localhost:8000/login>
|
|
- Health check: <http://localhost:8000/health>
|
|
- OpenAPI documentation: <http://localhost:8000/docs>
|
|
|
|
The browser extension requires a backend URL to be entered during setup; it does not assume a default server.
|
|
|
|
The supplied `Logo.svg` is bundled as `frontend/static/logo.svg` for web pages and `webextension/logo.svg` for the Firefox popup and settings page.
|
|
The visible LinkLog brand text uses the Google Foundry `Asset` font when available, with local fallbacks in the Firefox extension.
|
|
|
|
## Regenerate Logo Assets
|
|
|
|
`LinkLog.svg` is the source logo. Install ImageMagick, then regenerate the web logo, extension logo, and Firefox toolbar icons with:
|
|
|
|
```sh
|
|
make logos
|
|
```
|
|
|
|
The generated files are `frontend/static/logo.svg`, `webextension/logo.svg`, and `webextension/icon-16.png`, `icon-32.png`, `icon-48.png`, and `icon-96.png`. Override the ImageMagick executable with `make MAGICK=magick logos` when needed.
|
|
|
|
Create an installable Firefox XPI bundle with:
|
|
|
|
```sh
|
|
make xpi
|
|
```
|
|
|
|
This creates `XPI/unsigned/LinkLog-0.1.0.xpi` from the `webextension/` package and excludes macOS metadata and minified artifacts. The version is read from `webextension/manifest.json`. The `XPI/signed/` directory is reserved for signed release bundles.
|
|
|
|
## Releases
|
|
|
|
Releases run in Gitea Actions when a `v*` tag is pushed. The Docker release version comes from `frontend/version.json`; the Firefox plugin version comes from `webextension/manifest.json`. CI also requires both to match `LINKLOG_VERSION`'s default in `backend/app/core/config.py`.
|
|
|
|
The signed XPI is produced manually and must be checked into `XPI/signed/LinkLog-<version>.xpi` before creating the tag. The workflow validates the embedded manifest, publishes the XPI and `webextension/updates.json` as Gitea release assets, and publishes Docker images to `git.kolkman.org/olaf/link-log:<version>` and `:latest`.
|
|
|
|
The extension's `update_url` points at the stable raw repository URL `https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json`. Update `webextension/updates.json` with each signed XPI version and commit it together with the XPI. The release page provides a direct install link at `https://git.kolkman.org/olaf/Link-Log/releases/download/v<version>/LinkLog-<version>.xpi`.
|
|
|
|
The workflow requires Gitea Actions secrets named `REGISTRY_USERNAME`, `REGISTRY_TOKEN`, and `RELEASE_TOKEN`. `REGISTRY_TOKEN` is a Gitea access token with permission to push packages; `RELEASE_TOKEN` needs permission to create releases and upload release assets.
|
|
|
|
Every push to `main` also runs `.gitea/workflows/development.yml` and publishes the current Docker image as `git.kolkman.org/olaf/link-log:development`. The workflow uses `REGISTRY_USERNAME` and the Gitea access token in `REGISTRY_TOKEN`, and can also be started manually from Gitea Actions.
|
|
|
|
Appending a username to the root URL, such as `/alice`, opens that user's public feed and profile information.
|
|
|
|
Configuration APIs require a bearer token returned by the login endpoint. Users authenticate with their email address; the username remains the public presentation identity used in profiles and feed URLs. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account.
|
|
|
|
Users can change their password from the profile page. The current password is required, new passwords must contain at least 8 characters, and the endpoint is `PUT /api/user/password`.
|
|
|
|
Users can configure a time-based one-time password from the profile page using an authenticator app. The profile displays a provisioning secret and authenticator URI during setup, then requires a current six-digit code to enable or disable OTP. When OTP is enabled, both the web login and Firefox extension settings login require the code. The TOTP secret is never returned by the profile API after setup.
|
|
|
|
On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the `mastodon.social` instance and the `From my #LinkLog: ` post prefix.
|
|
|
|
## Run Tests
|
|
|
|
```sh
|
|
. .venv/bin/activate
|
|
PYTHONPATH=. pytest backend/tests -q
|
|
```
|
|
|
|
To ensure warnings are clean as well:
|
|
|
|
```sh
|
|
PYTHONPATH=. PYTHONWARNINGS=error pytest backend/tests -q
|
|
```
|
|
|
|
## Install The Firefox Extension For Development
|
|
|
|
The extension is an unpacked Firefox extension. No build step is required.
|
|
The toolbar uses square PNG icons generated from the bundled dark-background logo; `logo.svg` remains available for the popup and settings branding.
|
|
The manifest includes stable Firefox extension metadata and references the packaged PNG icons for toolbar and add-on installation.
|
|
|
|
1. Start the backend locally.
|
|
2. Open Firefox and visit `about:debugging#/runtime/this-firefox`.
|
|
3. Select **Load Temporary Add-on**.
|
|
4. Choose `webextension/manifest.json`.
|
|
5. Open the LinkLog extension options and enter:
|
|
- Backend URL: the URL of your LinkLog server, such as `http://localhost:8000`
|
|
- Username: `alice`
|
|
- Password: `secret123`
|
|
6. Save the settings and login.
|
|
7. Open a webpage, select the LinkLog toolbar button, review the title and URL, add a comment, and submit it.
|
|
|
|
When the extension settings page has a valid session, it shows `<username> logged in at <backend URL>` and a **Sign out** button instead of the login form. Signing out revokes the token and returns the form.
|
|
|
|
Temporary extensions are removed when Firefox restarts. Reload the extension from `about:debugging` after changing its files.
|
|
|
|
## Docker
|
|
|
|
Create the Docker environment file before starting the stack:
|
|
|
|
```sh
|
|
cp .env.example .env
|
|
```
|
|
|
|
Edit `.env` and replace `LINKLOG_SECRET_KEY` with a long random value. Docker Compose automatically reads `.env` from the repository root. The committed `.env.example` contains safe defaults and placeholders; the real `.env` is ignored by Git.
|
|
|
|
The main configurable values are:
|
|
|
|
| Variable | Purpose | Default |
|
|
| --- | --- | --- |
|
|
| `LINKLOG_SECRET_KEY` | token signing/security secret | required in Docker |
|
|
| `LINKLOG_DATABASE_PATH` | SQLite file path inside the container | `/app/backend/data/linklog.db` |
|
|
| `LINKLOG_TOKEN_EXPIRY_DAYS` | access-token lifetime | `30` |
|
|
| `LINKLOG_PUBLIC_URL` | Public hostname used by Traefik and expanded to a callback URL by the backend | `localhost` |
|
|
| `LINKLOG_SMTP_HOST` | SMTP server hostname; empty disables delivery in local development | empty |
|
|
| `LINKLOG_SMTP_PORT` | SMTP server port | `587` |
|
|
| `LINKLOG_SMTP_USERNAME` | SMTP login username | empty |
|
|
| `LINKLOG_SMTP_PASSWORD` | SMTP login password | empty |
|
|
| `LINKLOG_SMTP_FROM` | Sender address for verification mail | `LinkLog <no-reply@localhost>` |
|
|
| `LINKLOG_SMTP_USE_TLS` | Use STARTTLS for SMTP | `true` |
|
|
| `LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS` | Verification-link lifetime | `24` |
|
|
| `LINKLOG_PASSWORD_RESET_EXPIRY_HOURS` | Password-reset-link lifetime | `1` |
|
|
| `LINKLOG_TRACKING_PARAMS` | comma-separated tracking parameters (stripped from logged URLs) | built-in list |
|
|
| `APP_PORT` | direct host port for FastAPI | `8000` |
|
|
|
|
New users created by an administrator are email-unverified and cannot sign in until they follow the verification link sent to their address. The link is valid for `LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS` hours and is handled by `/api/auth/verify-email`. Configure `LINKLOG_SMTP_HOST`, `LINKLOG_SMTP_FROM`, and the SMTP credentials for delivery; local development may leave the SMTP host empty, in which case accounts remain pending verification and no message is sent.
|
|
|
|
On a fresh installation, the setup form is prefilled from the `LINKLOG_SMTP_*` environment values when available. After saving, the values stored in the database are used for subsequent setup-page loads and mail delivery.
|
|
|
|
When a verified user enters the wrong password, LinkLog keeps the response generic and sends a password-reset link to that account's email address when SMTP is configured. Reset links expire after `LINKLOG_PASSWORD_RESET_EXPIRY_HOURS` hours, can be used once, and revoke existing sessions after the password is changed.
|
|
|
|
The full set of supported variables is listed in `.env.example`. Application variables are passed into the container by Compose; Docker and Traefik variables are used by Compose itself.
|
|
|
|
Build and start the application:
|
|
|
|
```sh
|
|
docker compose up --build
|
|
```
|
|
|
|
The services are available at:
|
|
|
|
- Direct application port: <http://localhost:8000/>
|
|
|
|
|
|
The SQLite database is stored in the named Docker volume `linklog_data`, mounted at `/app/backend/data`.
|
|
The application runs as a non-root user and reports container health through `/health`;
|
|
|
|
Stop the stack without deleting its database:
|
|
|
|
```sh
|
|
docker compose down
|
|
```
|
|
|
|
Stop the stack and delete the named database volume:
|
|
|
|
```sh
|
|
docker compose down -v
|
|
```
|
|
|
|
For a real deployment, start with the docker-compose-example.yaml file. replace the router rule, configure TLS, protect the Traefik dashboard, avoid exposing the direct application port, and provide production secrets and authentication. The included Compose file is a local/prototype deployment scaffold, not a production security configuration.
|
|
|
|
## Mastodon Configuration
|
|
|
|
After logging in, open <http://localhost:8000/profile>, enter the Mastodon instance, and select **Connect Mastodon**. LinkLog registers an OAuth application on that instance, opens Mastodon authorization, and stores the returned per-user access token after the callback. The requested scopes are `read:accounts` and `write:statuses`. Posts use the configured prefix followed directly by the title, an optional comment, a `from: URL` line when a title exists, and tags on the final line, with blank lines between sections.
|
|
|
|
- **Instance**: hostname or URL such as `mastodon.social` or `https://mastodon.social`
|
|
- **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: `
|
|
|
|
`LINKLOG_PUBLIC_URL` is the public hostname used by Traefik and the backend. Use `localhost` for local development or a hostname such as `linklog.kolkman.org` for a deployment. The backend adds `http://` for localhost and `https://` for other hostnames when constructing OAuth and email links. Existing manually entered access tokens remain compatible with the plugin configuration API.
|
|
|
|
LinkLog caches the OAuth application credentials per Mastodon server in the persistent SQLite `app_settings` table, so subsequent connections do not register a new application on every attempt. If the server rate-limits application registration, the profile page reports the upstream `429` response and the user can retry after the server's cooldown.
|
|
|
|
Enable the plugin from the admin API or the admin page. New links are saved first and then posted to the configured instance at `/api/v1/statuses`. A Mastodon network failure does not undo the saved link.
|
|
|
|
## Useful API Calls
|
|
|
|
Login:
|
|
|
|
```sh
|
|
curl -X POST http://localhost:8000/api/auth/login \\
|
|
-H 'Content-Type: application/json' \\
|
|
-d '{"email":"alice@example.com","password":"secret123"}'
|
|
```
|
|
|
|
Submit a link using the returned access token:
|
|
|
|
```sh
|
|
curl -X POST http://localhost:8000/api/links \\
|
|
-H 'Content-Type: application/json' \\
|
|
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\
|
|
-d '{"title":"Example","url":"https://example.com","comment":"Worth reading"}'
|
|
```
|
|
|
|
The public feed is available at:
|
|
|
|
```text
|
|
GET http://localhost:8000/api/public/feed
|
|
```
|
|
|
|
When a request includes a valid bearer token, entries owned by that authenticated user include edit permission and show an inline **Edit** action in the feed. The update endpoint is `PUT /api/links/{link_id}` and rejects edits from other users.
|
|
Feed items also expose `is_owner`; it is `true` only for entries owned by the authenticated user and `false` for anonymous viewers or other users.
|
|
|
|
Links support zero to ten tags. Tags are trimmed, deduplicated case-insensitively, and retain their original casing for display. The Firefox capture popup shows existing server tags as checkboxes and accepts new comma-separated tags. The home page displays tags and provides a case-insensitive tag filter; editing a link replaces its complete tag set.
|
|
The inline link editor also allows multiple existing tags to be selected and new tags to be entered. New tag values receive a leading `#` automatically, and the interface prevents saving more than ten tags.
|
|
The installation seeds these available tags: `#Internet`, `#Cybersecurity`, `#Fediverse`, `#Food`, `#Photography`, `#Music`, and `#AI`.
|
|
Users can create, edit, and delete their own labels from `/labels`. Administrators can delete any label from `/admin`; system-seeded labels have no user owner.
|
|
|
|
Administrators can enable one or more backend themes from `/admin`: Plain Day, Plain Night, Catppuccin Latte, Catppuccin Frappe, Catppuccin Macchiato, Catppuccin Mocha, Dracula, Nord, and Solarized. Visitors can choose among enabled themes; their choice is stored locally in the browser. At least one theme must remain enabled.
|
|
|
|
Admin plugin requests must include the administrator's token:
|
|
|
|
```sh
|
|
curl http://localhost:8000/api/admin/plugins \\
|
|
-H 'Authorization: Bearer YOUR_ADMIN_ACCESS_TOKEN'
|
|
```
|
|
|
|
Administrators can manage user privileges from the admin page. The `Administrator` checkbox sends `PUT /api/admin/users/{user_id}` with `{"is_admin": true}` or `{"is_admin": false}`. The API rejects any change that would leave the system without an administrator.
|
|
|
|
## Troubleshooting
|
|
|
|
- **`ModuleNotFoundError`**: activate `.venv` and rerun `python -m pip install -r backend/requirements.txt`.
|
|
- **Jinja2 import error in Docker**: rebuild the image with `docker compose up --build`; the runtime dependency is declared in `backend/requirements.txt`.
|
|
- **Extension cannot login**: confirm the backend is running, use the exact backend origin without a trailing API path, and check the browser console for blocked requests.
|
|
- **Port 8000 is occupied**: run uvicorn with another port and update the extension backend URL, for example `--port 8001`.
|
|
- **Port 80 or 8080 is occupied**: change the host-side ports in `docker-compose.yml`.
|
|
- **Stale development data**: stop the backend and remove `backend/data/linklog.db`, or run `docker compose down -v` for the container volume.
|
|
- **Schema migration issue**: inspect the database version with `sqlite3 backend/data/linklog.db 'PRAGMA user_version;'` and restart the application to apply pending migrations.
|