Files
JQC_features/README.md
T

231 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# JQC Features Site
Single-page, modern site for LT Services' JQC program. Numbered sections; each
topic shows title only until clicked, then expands to reveal text, links,
photos, or videos.
Stack: **Flask + MySQL + Gunicorn + systemd + Nginx** on Ubuntu 24.04.
```
app.py Flask app + Section/Topic models + routes
config.py env-driven config (DB + demo contact URL)
schema.sql MySQL DDL + seed content (safe to re-run)
templates/index.html
static/css/style.css
static/js/main.js accordion expand/collapse
gunicorn.conf.py
deploy/jqc-features.service
deploy/nginx.conf
.env.example
```
---
## Deploy
### 1. Database (run once)
```bash
sudo mysql < schema.sql
sudo mysql -e "CREATE USER IF NOT EXISTS 'jqc_features'@'127.0.0.1' IDENTIFIED BY 'CHANGE_ME';
GRANT ALL PRIVILEGES ON jqc_features.* TO 'jqc_features'@'127.0.0.1';
FLUSH PRIVILEGES;"
```
`schema.sql` is idempotent — re-running updates seed content in place, never
duplicates rows.
### 2. Code
```bash
sudo useradd -r -s /usr/sbin/nologin jqcweb # service account
sudo mkdir -p /opt/jqc-features
sudo rsync -a ./ /opt/jqc-features/ # copy project here
cd /opt/jqc-features
sudo python3 -m venv venv
sudo ./venv/bin/pip install -r requirements.txt
sudo cp .env.example .env # then edit .env with the real DB_PASSWORD
sudo chown -R jqcweb:jqcweb /opt/jqc-features
```
### 3. Service (systemd — separate from DB step)
```bash
sudo cp deploy/jqc-features.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now jqc-features
sudo systemctl status jqc-features # confirm active (running)
curl -s http://127.0.0.1:8000/healthz # -> {"status":"ok"}
```
### 4. Nginx
```bash
# edit server_name in deploy/nginx.conf first
sudo cp deploy/nginx.conf /etc/nginx/sites-available/jqc-features
sudo ln -s /etc/nginx/sites-available/jqc-features /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# TLS:
sudo certbot --nginx -d features.ltservicesinc.com
```
---
## Adding photos, videos, or links to a topic
Content lives in the `topic` table. No code change needed — update rows, then
the page picks them up on next load.
Text only (default):
```sql
UPDATE topic SET body_html='<p>New copy with <strong>bold</strong>.</p>'
WHERE slug='comprehensive-checklists';
```
Add a photo:
```sql
UPDATE topic SET media_type='image',
media_url='/static/img/checklist.jpg',
media_caption='Timestamped inspection photo'
WHERE slug='comprehensive-checklists';
```
(Put the file in `static/img/` — create the folder — or use any absolute URL,
e.g. a Cloudflare R2 link.)
Add an uploaded video:
```sql
UPDATE topic SET media_type='video', media_url='/static/vid/demo.mp4'
WHERE slug='real-time-issue-tracking';
```
Embed a YouTube clip:
```sql
UPDATE topic SET media_type='embed',
media_url='https://www.youtube.com/embed/VIDEO_ID'
WHERE slug='notifications-and-qr-access';
```
Add a button link (works alongside any media):
```sql
UPDATE topic SET link_url='https://jqc.ltservicesinc.com',
link_label='Open the portal'
WHERE slug='shared-inspector-access';
```
`media_type` values: `none`, `image`, `video`, `embed`.
## Add a whole new section or topic
```sql
INSERT INTO section (num,title,subtitle,sort_order)
VALUES (5,'New Section','Optional subtitle',50);
INSERT INTO topic (section_id,slug,title,body_html,media_type,sort_order)
VALUES ((SELECT id FROM section WHERE num=5),
'my-new-topic','My New Topic','<p>Content.</p>','none',10);
```
Sections order by `sort_order`; topics order by `sort_order` within a section.
---
## Admin panel (browser editing)
Instead of SQL, edit content at **`/admin`**. Single admin account, session login,
CSRF-protected forms, and every create/update/delete is written to `audit_log`.
### One-time setup
1. **Already-deployed DB?** add the audit table:
```bash
sudo mysql < add_admin.sql
```
(New deploys skip this — `schema.sql` already includes `audit_log`.)
2. Set these in `/opt/jqc-features/.env`:
```bash
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=<paste hash from command below>
SESSION_COOKIE_SECURE=1 # keep 1 in production (HTTPS)
```
Generate the password hash (never store the plaintext):
```bash
./venv/bin/python -c "from werkzeug.security import generate_password_hash as g; print(g('YOUR_PASSWORD'))"
```
3. Install the new dependency and restart:
```bash
sudo ./venv/bin/pip install -r requirements.txt
sudo systemctl restart jqc-features
```
Then visit `https://your-domain/admin`, sign in, and manage content.
### What you can do
- **Dashboard** — every section with its topics; edit or delete inline.
- **Add / edit topic** — section, title, slug (auto if blank), body HTML, media
(image / video / embed), caption, an optional link button, and sort order.
- **Add / edit section** — number (`§NN`, unique), title, subtitle, sort order.
- Deleting a section cascades to its topics (with a confirm prompt).
### Notes
- `SESSION_COOKIE_SECURE=1` means the login cookie only sends over HTTPS. For a
quick plain-HTTP test on the box, set it to `0` — never in production.
- `SECRET_KEY` must be stable and secret; changing it logs everyone out.
- The admin routes live under `/admin`; the public page and `schema.sql` SQL
workflow above still work unchanged.
---
## Brute-force protection (fail2ban)
Every login attempt is written to `logs/auth.log` (rotating, 1 MB × 5) with the
real client IP:
```
2026-07-22 12:00:00,123 jqc.auth WARNING FAILED LOGIN user=admin from 203.0.113.5
2026-07-22 12:00:05,456 jqc.auth INFO LOGIN OK user=admin from 203.0.113.5
```
The real IP comes from `ProxyFix` reading nginx's `X-Forwarded-For` (gunicorn
binds 127.0.0.1, so the header can't be spoofed from outside). Config ships in
`deploy/fail2ban/`.
### Install
```bash
sudo apt-get install -y fail2ban
sudo cp deploy/fail2ban/filter.d/jqc-admin.conf /etc/fail2ban/filter.d/
sudo cp deploy/fail2ban/jail.d/jqc-admin.local /etc/fail2ban/jail.d/
# edit logpath in the jail file if your app dir differs from /home/jqc/jqc_features
sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
```
Default policy: **5 failures in 10 min → 1 h ban** (`maxretry`/`findtime`/`bantime`
in the jail file).
### Verify
```bash
# regex matches the log lines:
sudo fail2ban-regex logs/auth.log deploy/fail2ban/filter.d/jqc-admin.conf
# jail is live:
sudo fail2ban-client status jqc-admin
```
`fail2ban-regex` should report matches equal to the number of `FAILED LOGIN`
lines. `status` shows currently banned IPs.
### Scope note
This jail bans credential-guessing that reaches the password check (a real
browser session with a valid CSRF token). Dumb bots that POST without a CSRF
token get an HTTP 400 and never reach the check — they can't guess a password
anyway. To also throttle those, add an nginx `limit_req` on `/admin/login`.