diff --git a/README.md b/README.md index c2716c6..1ec756a 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,400 @@ -# lapp +# L@pp -list app +L@pp is a small, mobile-friendly shared-list application built with Flask. It is +designed primarily for grocery lists, but works just as well for chores, packing +lists, or any other lightweight checklist. -install -1. clone repo -2. create subfolder "instance" to store database file -3. for an initial instance, run `.venv/bin/python initialize_data.py` -4. add the below config to your apache2 enabled site +Users can create and share lists, add items with quantities and categories, and +check items off from a phone-friendly interface. The included web app can also +be installed as a Progressive Web App (PWA). -## Secrets and configuration +## Features -LAPP stores its private configuration in `instance/secrets.yaml`. On first -startup, missing values are generated automatically and the file permissions are -set to `0600`. +- Multiple lists per user +- List sharing between registered users +- Item quantities and units, including input such as `Milk 2 cartons` +- Grocery categories with icons +- Reusable suggestions for previously completed items +- Bulk item entry +- Responsive, installable PWA interface +- User registration with group invitation keys +- Administrator approval for new accounts +- Session-based JSON API for companion clients +- SQLite by default, with other SQLAlchemy database URLs supported -The file contains the Flask session key, API application key, database URI, -initial account credentials, initial group secrets, and the Fernet key used by -`test.py`. To change the database, edit `runtime.database_uri` in this file. +## Requirements -Keep `runtime.secret_key` stable: changing it logs users out and invalidates -existing auto-login cookies. `runtime.application_key` is used by -`/api/validate_key`. +- Python 3.9 or newer +- `pip` and Python virtual-environment support -The entire `instance/` directory is ignored by Git. Back up `secrets.yaml` -securely and never commit or share it. +## Quick start -## Initial data secrets +Clone the repository and enter its directory: -`initialize_data.py` creates initial users and groups only when the database has -no users yet. Set the values under `initial_data` in `instance/secrets.yaml` -before running it, or use the securely generated defaults. The script prints the -location of the credentials when it creates the initial data. +```bash +git clone +cd lapp +``` + +Create a virtual environment and install the dependencies: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -r requirement.txt +``` + +Initialize the database with an administrator, a regular user, two groups, and +some example lists: + +```bash +python initialize_data.py +``` + +The command creates `instance/secrets.yaml` and `instance/app.db`. It does not +print the generated passwords or registration keys; open +`instance/secrets.yaml` locally to retrieve them. The seeded usernames are +`admin` and `ignace`. + +Start the development server: + +```bash +python lapp.py +``` + +Then open . The built-in server runs in debug mode and is +intended for local development only. + +Running the application without `initialize_data.py` creates an empty database +schema, but no users or registration groups. + +## Configuration and secrets + +Private configuration lives in `instance/secrets.yaml`. On first use, missing +values are generated automatically and the file is restricted to mode `0600`. +A generated file has this structure: + +```yaml +runtime: + secret_key: + application_key: + database_uri: sqlite:///app.db +initial_data: + admin_password: + admin_group: + user_password: + user_group: +fernet: + key: +``` + +For SQLite, relative database paths are resolved inside Flask's `instance/` +directory. To use another database, set `runtime.database_uri` to a compatible +SQLAlchemy URL and install the corresponding database driver. + +Keep `runtime.secret_key` stable. Replacing it invalidates active sessions and +the seven-day auto-login cookie. The `runtime.application_key` is checked by +`POST /api/validate_key`. + +The complete `instance/` directory is ignored by Git. Back it up securely and +never commit or share its contents. In particular, back up both `app.db` and +`secrets.yaml` together when using the default SQLite setup. + +### Customizing the initial accounts + +To choose credentials instead of using generated values: + +1. Generate the configuration and empty schema without seeding accounts: + + ```bash + python -c 'from lapp import create_app; create_app()' + ``` + +2. Edit the values under `initial_data` in `instance/secrets.yaml`. +3. Run `python initialize_data.py`. + +Initialization is deliberately idempotent: if the database already contains a +user, no seed data is added. It also refuses to initialize a partially populated +database. + +## How accounts and sharing work + +Registration requires a group key. The two keys created during initialization +are stored as `initial_data.admin_group` and `initial_data.user_group`. A newly +registered account remains pending until an administrator approves it at +`/admin/`. + +After signing in, a user can create, activate, rename, share, or delete their own +lists. A shared list is visible and editable by every selected user. Only the +owner can change the list itself or its sharing settings. + +Administrators can create additional registration groups from `/groups`. + +## Item input + +L@pp extracts simple quantities and units from item text. For example: + +| Input | Label | Quantity | Unit | +| --- | --- | ---: | --- | +| `Apples 4` | Apples | 4 | `x` | +| `Milk 2 cartons` | Milk | 2 | `cartons` | +| `3 kg potatoes` | Potatoes | 3 | `kg` | + +Cleaning a list moves checked items into its suggestion history. Those items can +then be added again quickly from the add-items screen. + +## API overview + +API requests use form-encoded fields and return JSON. Authentication is stored +in the Flask session cookie, so API clients must retain cookies after login. + +| Method | Endpoint | Purpose | +| --- | --- | --- | +| `POST` | `/api/validate_key` | Validate the configured application key (`key`) | +| `POST` | `/api/login` | Sign in with `user` and `password` | +| `POST` | `/api/register` | Register with `user` and a group key in `password` | +| `POST` | `/api/load_lists` | Load lists available to the signed-in user | +| `POST` | `/api/load_items` | Load active items for `listid` | + +Responses use a numeric `status` field: `1` means success, `0` means failure, +and registration uses `2` to indicate that approval is pending. + +Example login with a cookie jar: + +```bash +curl -c cookies.txt \ + -X POST \ + -d 'user=ignace' \ + -d 'password=YOUR_PASSWORD' \ + http://127.0.0.1:5001/api/login + +curl -b cookies.txt \ + -X POST \ + http://127.0.0.1:5001/api/load_lists +``` + +## Production deployment with Nginx and Gunicorn + +The following example targets Debian or Ubuntu, installs L@pp in `/srv/lapp`, +and serves it from `lists.example.com`. Substitute your own installation path +and domain where necessary. + +Gunicorn loads the existing Flask application factory as +`lapp:create_app()`. The repository's `lapp.wsgi` file is only needed for a +mod_wsgi deployment and is not used here. + +### 1. Install the system packages + +```bash +sudo apt update +sudo apt install nginx python3 python3-venv +``` + +Create a dedicated, unprivileged service account: + +```bash +sudo useradd --system \ + --home /srv/lapp \ + --shell /usr/sbin/nologin \ + lapp +``` + +Place or clone the repository at `/srv/lapp`, then install the application and +Gunicorn: + +```bash +cd /srv/lapp + +sudo python3 -m venv .venv +sudo .venv/bin/pip install --upgrade pip +sudo .venv/bin/pip install -r requirement.txt +sudo .venv/bin/pip install 'gunicorn>=23,<24' +``` + +Gunicorn should run as the service account, not as `root`, and should only be +reachable through Nginx. + +### 2. Initialize the application + +Create the private instance directory with permissions that allow the service +account to maintain the SQLite database: + +```bash +sudo install -d \ + -o lapp \ + -g www-data \ + -m 0750 \ + /srv/lapp/instance + +sudo -u lapp /srv/lapp/.venv/bin/python \ + /srv/lapp/initialize_data.py +``` + +View the generated passwords and registration keys locally: + +```bash +sudo -u lapp sed -n '1,120p' \ + /srv/lapp/instance/secrets.yaml +``` + +Back up `instance/app.db` and `instance/secrets.yaml` together. Do not expose +the `instance/` directory through Nginx. + +### 3. Run Gunicorn with systemd + +Create `/etc/systemd/system/lapp.service`: + +```ini +[Unit] +Description=L@pp service +After=network.target + +[Service] +Type=simple +User=lapp +Group=www-data +WorkingDirectory=/srv/lapp + +RuntimeDirectory=lapp +RuntimeDirectoryMode=0750 +UMask=0007 + +ExecStart=/srv/lapp/.venv/bin/gunicorn \ + --workers 2 \ + --bind unix:/run/lapp/lapp.sock \ + --access-logfile - \ + --error-logfile - \ + lapp:create_app() + +Restart=on-failure +RestartSec=5 +PrivateTmp=true +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target +``` + +One or two workers is a sensible starting point for this small SQLite +application. Adding many workers can increase SQLite write contention. + +Enable and start the service: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now lapp +sudo systemctl status lapp +``` + +Follow its logs or verify that its socket exists: + +```bash +sudo journalctl -u lapp -f +sudo ls -l /run/lapp/lapp.sock +``` + +### 4. Configure Nginx + +Create `/etc/nginx/sites-available/lapp`: + +```nginx +server { + listen 80; + listen [::]:80; + + server_name lapp.suy.nl; + + client_max_body_size 2m; + + location /static/ { + alias /srv/lapp/static/; + expires 7d; + add_header Cache-Control "public"; + } + + location / { + proxy_pass http://unix:/run/lapp/lapp.sock; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_read_timeout 60s; + } +} +``` + +Enable the site and validate the configuration before reloading Nginx: + +```bash +sudo ln -s /etc/nginx/sites-available/lapp \ + /etc/nginx/sites-enabled/lapp + +sudo nginx -t +sudo systemctl reload nginx +``` + +The application should now be available at `http://lists.example.com`. + +### 5. Enable HTTPS + +Once the domain's DNS records point to the server, obtain a TLS certificate +with your preferred ACME client. For example, using Certbot: + +```bash +sudo apt install certbot python3-certbot-nginx +sudo certbot --nginx -d lists.example.com +``` + +Do not expose L@pp publicly over plain HTTP: it handles passwords and persistent +login cookies. + +### Updating a deployment + +Back up the database and secrets first. Then update the source and dependencies +and restart Gunicorn: + +```bash +cd /srv/lapp +sudo .venv/bin/pip install -r requirement.txt +sudo systemctl restart lapp +sudo systemctl status lapp +``` + +See the official [Flask Gunicorn deployment guide](https://flask.palletsprojects.com/en/stable/deploying/gunicorn/), +[Gunicorn documentation](https://docs.gunicorn.org/en/stable/run.html), and +[Nginx proxy module documentation](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) +for additional configuration options. + +## Utility scripts + +- `initialize_data.py` safely seeds a new database. +- `show_db.py` prints all database records for local diagnostics. Its output + includes password hashes and group secrets, so treat it as sensitive. +- `update_db.py` contains historical, manual migration snippets. Review and + adapt it before running it against any database. +- `test.py` is a Fernet encryption example, not an automated test suite. + +## Project structure + +```text +lapp.py Application factory and blueprint registration +auth.py Login, registration, logout, and password changes +admin.py Account approval +groups.py Registration-group management +lists.py List creation, ownership, and sharing +items.py Item entry, categories, quantities, and completion +api.py Session-based JSON API +models.py SQLAlchemy models +templates/ Jinja templates +static/ CSS, JavaScript, icons, and PWA manifest +instance/ Local secrets and database (generated, ignored by Git) +``` + +## Development notes + +There is currently no automated test suite or migration framework. Before +upgrading an existing installation, back up the database and secrets, then test +the change on a copy of the data. diff --git a/requirement.txt b/requirement.txt index f306269..33afe14 100644 --- a/requirement.txt +++ b/requirement.txt @@ -3,3 +3,4 @@ Flask-Login>=0.6,<1.0 Flask-SQLAlchemy>=3.1,<4.0 cryptography>=42,<46 PyYAML>=6.0,<7.0 +gunicorn