Compare commits
10 commits
434313c6e8
...
be0b194737
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be0b194737 | ||
|
|
47b5fd7f87 | ||
|
|
c626b84324 | ||
|
|
562452c6c7 | ||
|
|
0d332dfe85 | ||
|
|
6a5003ffda | ||
|
|
dbb2781c23 | ||
|
|
c3ac96ec02 | ||
|
|
728cb73faf | ||
|
|
f3437f2f0e |
22 changed files with 503 additions and 21 deletions
37
.env.example
37
.env.example
|
|
@ -5,5 +5,38 @@ NEXT_PUBLIC_API_BASE=https://api.lomavuokraus.fi
|
|||
# Runtime env flag used in UI
|
||||
APP_ENV=local
|
||||
|
||||
# Secrets (override in Kubernetes Secret)
|
||||
APP_SECRET=change-me
|
||||
# Core app secrets (override in Kubernetes Secret)
|
||||
APP_URL=http://localhost:3000
|
||||
AUTH_SECRET=change-me
|
||||
DATABASE_URL=postgresql://user:password@host:5432/lomavuokraus?sslmode=disable
|
||||
|
||||
# Mail (fill in SMTP_USER/SMTP_PASS)
|
||||
SMTP_HOST=smtp.lomavuokraus.fi
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM=noreply@lomavuokraus.fi
|
||||
SMTP_TLS=true
|
||||
SMTP_SSL=false
|
||||
SMTP_REJECT_UNAUTHORIZED=true
|
||||
DKIM_SELECTOR=mail2025
|
||||
DKIM_DOMAIN=lomavuokraus.fi
|
||||
DKIM_PRIVATE_KEY_PATH=creds/dkim/lomavuokraus.fi/mail2025.private
|
||||
|
||||
# Feature flags / behaviour
|
||||
AUTO_APPROVE_LISTINGS=false
|
||||
|
||||
# External APIs / infra
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_TRANSLATIONS_KEY=
|
||||
HETZNER_API_TOKEN=
|
||||
HCLOUD_TOKEN=
|
||||
HETZNER_TOKEN=
|
||||
JOKER_DYNDNS_USERNAME=
|
||||
JOKER_DYNDNS_PASSWORD=
|
||||
REGISTRY_USERNAME=
|
||||
REGISTRY_PASSWORD=
|
||||
|
||||
# Admin bootstrap (used by seed/reset scripts)
|
||||
ADMIN_EMAIL=
|
||||
ADMIN_INITIAL_PASSWORD=
|
||||
|
|
|
|||
18
.forgejo/workflows/ci.yml
Normal file
18
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run type-check
|
||||
- run: npm run format:check
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,6 +18,7 @@ coverage
|
|||
deploy/.last-image
|
||||
|
||||
creds/
|
||||
!creds/secrets.enc.env
|
||||
k3s.yaml
|
||||
|
||||
# Local-only documentation (now tracked in docs/)
|
||||
|
|
|
|||
7
.sops.yaml
Normal file
7
.sops.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
creation_rules:
|
||||
- paths:
|
||||
- creds/secrets.enc.env
|
||||
key_groups:
|
||||
- age:
|
||||
- age1hkehkc2rryjl975c2mg5cghmjr54n4wjshncl292h2eg5l394fhs4uydrh
|
||||
encrypted_regex: '^(AUTH_SECRET|DATABASE_URL|DB_.*|APP_URL|SMTP_.*|DKIM_.*|AUTO_APPROVE_LISTINGS|OPENAI_.*|H(ETZNER|CLOUD)_TOKEN|JOKER_DYNDNS_.*|REGISTRY_.*|NETDATA_.*|ADMIN_.*)$'
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
- Pushes (docker, ctr, skopeo from k3s node) fail: connection closed while uploading blobs (`http://registry.halla-aho.net:443/... use of closed network connection`). Suspect registry reverse-proxy dropping uploads/HTTPS handling.
|
||||
- Need to inspect registry host logs/config and retry push once fixed.
|
||||
- Secrets:
|
||||
- `APP_SECRET` removed from `deploy/env.sh`; export it in shell before deploy.
|
||||
- `AUTH_SECRET` removed from `deploy/env.sh`; export it in shell (or via `scripts/load-secrets.sh`) before deploy.
|
||||
- `creds/` and `k3s.yaml` are git-ignored; contains joker DYNDNS creds and registry auth.
|
||||
|
||||
# Lomavuokraus app progress (Nov 24)
|
||||
|
|
@ -81,3 +81,5 @@
|
|||
- Added site favicon generated from the updated logo (`public/favicon.ico`).
|
||||
- New admin monitoring dashboard at `/admin/monitor` surfaces Hetzner node status, Kubernetes nodes/pods health, and PostgreSQL connection/size checks with auto-refresh.
|
||||
- Netdata installed on k3s node (`node1.lomavuokraus.fi:8443`) and DB host (`db1.lomavuokraus.fi:8443`) behind self-signed TLS + basic auth; DB Netdata includes Postgres metrics via dedicated `netdata` role.
|
||||
- Footer now includes a minimal cookie usage statement (essential cookies only; site requires acceptance).
|
||||
- Forgejo deployment scaffolding added: Docker Compose + runner config guidance and Apache vhost for git.halla-aho.net, plus CI workflow placeholder under `.forgejo/workflows/`.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default function SiteFooter() {
|
|||
Version <code>{version}</code>
|
||||
</span>
|
||||
</div>
|
||||
<p className="footer-cookie">{t('footerCookieNotice')}</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,6 +466,13 @@ p {
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-cookie {
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.privacy-block ul {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
|
|
|
|||
44
creds/secrets.enc.env
Normal file
44
creds/secrets.enc.env
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Encrypted with sops (age). To edit: sops creds/secrets.enc.env
|
||||
AUTH_SECRET=ENC[AES256_GCM,data:DQl4MEDrsUe76/NYdI03BZb31/NTTA0nBTUALhsULvT1EvMRRiR1,iv:rXjuUm3Z5OFMa0Jc/h8BSF1DWxddrtH2DyvPwX8KZ3c=,tag:eL7NZj0WE44y9AxVoCXrFA==,type:str]
|
||||
DATABASE_URL=ENC[AES256_GCM,data:87ysYMPY0lI8LrL46dHPjcZOYo4IlP/aQ4h+psSXS4EKzUiBJuExtF8f+kAZNJBc4Xj+B82Q34CfbbsBC8NdQoeebqvXF6/0IluvmpCBhpN2MKWqwlx/mQZTENZA3J0QiAP+BapeZXuXLc//,iv:n4W+hyWf1oH8IahJJs1XQ3d3i5hJjLXaNE+b4Z66Zbw=,tag:JYsBd5d0APkikYYg8Ea/Fg==,type:str]
|
||||
DB_HOST=ENC[AES256_GCM,data:qlmMVJEkDwfiRYGFUdP/,iv:syUEM2Cx64jE7IW8zRSqfe9uAL+JEMl9Tu5nw12kdWI=,tag:VoAJgXgGy8OXksUvn/SpUQ==,type:str]
|
||||
DB_PORT=ENC[AES256_GCM,data:vLjEIHpK,iv:ym9R0E/S5WKLKF+0thz9tmwI4pYa7skrY4sDpY42y0E=,tag:SK8gDGrnx8/YAPi8qB7KeQ==,type:str]
|
||||
DB_USER=ENC[AES256_GCM,data:KIlxtjxjx0EIDxXxHKY=,iv:dlhX4pIe6AE8fzT+TVB3hg9gJ46sq85Qp8f4pkxO9n8=,tag:WCMaOPpnI8SA8WMRDA392w==,type:str]
|
||||
DB_PASS=ENC[AES256_GCM,data:CJEh2+yQrPptlTnthe2UWsc/baNXBIAC0lpxtm8DI47gTg==,iv:UPrDDlMbWaiAiz5QcjRqXRIiVohMRF7wtmi5ioONcLE=,tag:jLSfXcefy8+cll8bVclkSQ==,type:str]
|
||||
APP_URL=ENC[AES256_GCM,data:1vZsUQ/d4MTbBM+juPJeVwseAVz4O2M=,iv:KrsOr3w1i3j3R19KAowFxFbdSm1B9IikqRoqU9tmQus=,tag:BwR3v/2R7bAFxHnB5waFIA==,type:str]
|
||||
SMTP_HOST=ENC[AES256_GCM,data:H+47VDofg1EEBo6q1UEleQ==,iv:KlAxFj/bKOMikubGbqPYkKPZt18oIdXxrLjEVnbxhN8=,tag:LpC930G7lsjt9YuSEFxW8A==,type:str]
|
||||
SMTP_PORT=ENC[AES256_GCM,data:F6fiIz4=,iv:Pelfn6ciIGvqF4nMPX5WrsWqkLtAxPBmBOSBvn4RUmo=,tag:UWBGXnDAYf5zCQJagiSE9Q==,type:str]
|
||||
SMTP_USER=ENC[AES256_GCM,data:CnbiHSRC0w/TOwwUwto=,iv:s8tJxs3X3BaFf5LrR+hHVvRIbDEfNI6ZszdTi+lg3PQ=,tag:DAbkk3szBzmhUrNTPZdA6A==,type:str]
|
||||
SMTP_PASS=ENC[AES256_GCM,data:GmmDQ2mROFz9zpGpXgtwlUvU2dmWg9KHsw==,iv:pl7O1QCjkaaqZ0sGS6kIBUKJ5glE8FTyA1vCYz0BfoU=,tag:/eE1ZWrLlKsa043VY3p3DQ==,type:str]
|
||||
SMTP_FROM=ENC[AES256_GCM,data:XY9BFPRmpO5KblsSMB2680DNx/bvUndfXA==,iv:yCUsaOMu1MrHtfR9AwfuIscyiEqsl0OYvO8UiKO6IYk=,tag:MpEIu2UJXEjE+9n1MVaHRQ==,type:str]
|
||||
SMTP_TLS=ENC[AES256_GCM,data:sBPcDhWs,iv:lZRjMBtrtAinC1blRFE0RH5jzyEcdLf+UOgEHuKitEk=,tag:R3Q4+ri3m5LB+xzohsYJHA==,type:str]
|
||||
SMTP_SSL=ENC[AES256_GCM,data:dYAo31bHTw==,iv:OG0c0HYzewLwlsgPlyn0nz+vwdeuSCjJ5ifdE9bfQvI=,tag:EJ5CX6+weXcrcjdrr1uicg==,type:str]
|
||||
SMTP_REJECT_UNAUTHORIZED=ENC[AES256_GCM,data:HhBgopyX,iv:7W9TYVN1aEH5QWK1NXGE9mw9gCd7yagg/LFVNHSdMV4=,tag:rcWGdoXH2wXtqWaImLEfHA==,type:str]
|
||||
DKIM_SELECTOR=ENC[AES256_GCM,data:taqEDa6Ah1G6yQ==,iv:n4KfE3lFBIyndBFkgvoBvzAlqP2HV4RIyL8c7lLoQSU=,tag:nos80rdqlj7YQshANBoTvw==,type:str]
|
||||
DKIM_DOMAIN=ENC[AES256_GCM,data:ZQkoRT8bQ7/Kdt0O/M4kRB8=,iv:wSbk2ALpbcxRT8neFrcQBEj8ja54ZtWeKTfllm8Pr5s=,tag:o5ZvS1enT31pV4+EM8i5LA==,type:str]
|
||||
DKIM_PRIVATE_KEY_PATH=ENC[AES256_GCM,data:8hbyhw/+I1VWGzoDNIKMY3avRvSmt17EmHiwv7WpeJZzalEXqTmDB+bpkVEQ,iv:/7bUU6boXo1HPccvNBCDnLyql0hGaMXmGIeQEqpFnwM=,tag:nTE+dR0JB/Y1ri5h/FaC3A==,type:str]
|
||||
AUTO_APPROVE_LISTINGS=ENC[AES256_GCM,data:bAHiVYhjXw==,iv:/jKFQ5L+meSNF6tn54Dc8dpK1I0+zG/o/F0QqMInmwU=,tag:9f7mb2QN17AoualTjkLeSQ==,type:str]
|
||||
OPENAI_API_KEY=ENC[AES256_GCM,data:+Avly5xD/jgkeiiHg7WNVH/OyGRf9L+iKDh/SvD7LrFuJlltORwzY253l29vD6cucaYfFanTWwDaV6TKuatNiRXxzy7aSoOfs0y4qonXofIRb3BH2/DarGWxQtRi5LRrXFIpCRQfN7x+XGbeF8ZZBW/DHFYywH0E2fU9li/iafXVOKBRcV4WYTIA3sJJuObHFjxgRYMB2SWtw8noZvkK+pzQi6lCKA==,iv:WTbvAzkjoDLAuDIdbFjt9BU5+2pGDv5Ksc4xru0Wx20=,tag:b8ddMgRoq+kMlS994yPx9A==,type:str]
|
||||
OPENAI_TRANSLATIONS_KEY=ENC[AES256_GCM,data:QGIhIhzn775yXZq9OYKTwGwcLnQAIJn8TM95TRFb5jWY/gTevspeYLDVrrGDKv/WHX8aTg4r4z9t6cbgbSX677fSlrNy2c2YEb5uCLGh0QUK2LoPt9R9yF/iyi5CU2obRlqamtF6knUGG5mdOCewKx/k+updDLmOe+CPD91H6nWbE9+1Pmbdf22bM+rOt9t4+MS6UP0UbIYU0sBEtfPTvN68260fUA==,iv:eYzIudN/i0A38X1up+Jnxv7qE5aiUEu0WQHEtQ4DvlA=,tag:qM7noB1GvXt2F69aVd0KVQ==,type:str]
|
||||
HETZNER_API_TOKEN="hoRULGviS8G3OGaJ68josx00M53efhuntVM5Rfft1AOvUR0ZQTXlO6yivhGqBM5o"
|
||||
HCLOUD_TOKEN=ENC[AES256_GCM,data:0qm+oR2daCd9e0qc+2wApobRod0RsUeA2QL1kQarQW77FvPAgQ7jBkML39kBP3xsYsrdmiZuIdg63Q3xeBQ9mVq2,iv:JUjR7vGyX00TOlUCOD8M9jiB6iio4IiX73sLzW/CADA=,tag:9p0kgDD7ul8s5T0cL7vsfA==,type:str]
|
||||
HETZNER_TOKEN=ENC[AES256_GCM,data:e95QAx49/qkoBnAmyup7NBs/Xm/fkWutMIWzlAv4P1ybSJNoCyC4ZibpNTCtYtK/3Hux5ItdOBm/jhssjlX363cI,iv:4UzDqJd0mYP3IJgeMn9xz9UjsVyODZ44CDAz5PPaXbk=,tag:ZEAqueoXY4kBfe+89nA+Kg==,type:str]
|
||||
JOKER_DYNDNS_USERNAME=ENC[AES256_GCM,data:FBAh4B+vxosvxfHbrZukSNea,iv:4FexfnCieAaZ7iUSvaiATr5THpqoHaebX/QigA9Wl58=,tag:dY6lQnHTJWOZa7WhHwPELA==,type:str]
|
||||
JOKER_DYNDNS_PASSWORD=ENC[AES256_GCM,data:1neBKFvQhFGfOgdPI6EHZKKI,iv:fC1J7QjAoQMUKxx6lMO32n6zjQ5tQa/icBDn19Lih2k=,tag:W110zqgwbsrVbkZzav/F0Q==,type:str]
|
||||
REGISTRY_USERNAME=ENC[AES256_GCM,data:Mt1mk15DPH8=,iv:Hxg4fRLRZ2hKRACTIIUMSODRBOgRv5Y6KfZ8477cwc0=,tag:Ufq2jYQjYzjiB88PF0gpBA==,type:str]
|
||||
REGISTRY_PASSWORD=ENC[AES256_GCM,data:4l2oMR65+X+1,iv:cfh6Esayb6zJEqKrtfY0LAncIrPi7lj/ckcbMgTMFys=,tag:vhsiagOyGVjN7+GNPwZ8LA==,type:str]
|
||||
NETDATA_USER=ENC[AES256_GCM,data:6laH3H7JKgpU,iv:PCI8HL6S68AaBRg11YlNbK9R12G1ROMqRIpXKIlsBDQ=,tag:qL1uWrtWz5+c6f6Xbv4Low==,type:str]
|
||||
NETDATA_PASS=ENC[AES256_GCM,data:CZxY7WeoHuUNmECOiL6XLVeTt/k5PAHFyV4dVBrF,iv:Zp00OVvA5hI8c9AD4pvDB1s99yw9iOlgBV4K4nbFFVg=,tag:Y077Bp8qk/SoEOajOdvMLg==,type:str]
|
||||
NETDATA_HOST_NODE1=ENC[AES256_GCM,data:QEHB348q+BQubmvIJkH/8mie2fjb0mE=,iv:ZUNZA64cuflqQq+JS/e0wByTzny0eE/4rvaxzNN1JFw=,tag:YfmJWJCd1MQW2Q2bWferLg==,type:str]
|
||||
NETDATA_HOST_DB1=ENC[AES256_GCM,data:B8mSqI+rusKxazFcziGabYMM1i1q,iv:1hDrheKjdo7dMF1m7qOWQ9llye2glEQm79LONauLiYk=,tag:H1BVhIKfymLBzGS6CZEp3Q==,type:str]
|
||||
NETDATA_PG_USER=ENC[AES256_GCM,data:n0G+MZWEWQK9,iv:11id08TbMpc5NUqtKsoXiIFfM5GJCFStzJ5aY1AGplc=,tag:gXWcgEXko4P7qMwVLGnF1Q==,type:str]
|
||||
NETDATA_PG_PASS=ENC[AES256_GCM,data:ylpbhrcFFKC5qiyt+RRPZhCWt098tPhGWxabgsO31tR7+g==,iv:O4b5DsNfCpqlhZ3GmXMRzp2/fUpMtLnhSqmoaD4+Q04=,tag:TG7ge49hjzkWcBO93hoXuA==,type:str]
|
||||
NETDATA_PG_ROLE=ENC[AES256_GCM,data:6/ESugRIiDgBM2e8,iv:kXJOKx7fbO4k9zV2APTXCxOrGJ/93dCkmVA33v9fzrY=,tag:HwEWzVagm6cDnPychaekaw==,type:str]
|
||||
ADMIN_EMAIL=ENC[AES256_GCM,data:AuhvA1iUOnMcE7el4TSE7cbLAg==,iv:eWNxWDA978OWQO10/ywc8CpKuUhC0uTqObmD5Tefgtc=,tag:pI4SXW1ZrZPthD2qRAPrIw==,type:str]
|
||||
ADMIN_INITIAL_PASSWORD=ENC[AES256_GCM,data:WiJm5VO57/qLu8fK0rZqFDk=,iv:2SwLMvMUjwkEDZo8VRtPHfs+cabz2L+9ZQAaWyn1o9c=,tag:+TunF3tuLxYvMeJsYNQuCQ==,type:str]
|
||||
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNlZ6dWs5VnJXa0hrekdp\nYlpzYll6NUFnTnV0Sk0zbnB2WEw5alVwT2swCm82NmlUbjdabEJ2d1pHTGhYRG1m\nZmIyUHFQcVhvZHNzRTJuZVNqYThaWGMKLS0tIGtHTEhzQnh2SFVxdlBvRlpMblVW\nOWdVY0ErK2pVSzdtckc4Y0lPRTdrdmsKv5M0ojCoW5SQhnjXY116SmjvyCtSnehg\nQqtL6jElOv4MeLASHwYLYzznU6dxkZK3OKvcLh6mu+41Pnbl8u26yw==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_0__map_recipient=age1hkehkc2rryjl975c2mg5cghmjr54n4wjshncl292h2eg5l394fhs4uydrh
|
||||
sops_encrypted_regex=^(AUTH_SECRET|DATABASE_URL|DB_.*|APP_URL|SMTP_.*|DKIM_.*|AUTO_APPROVE_LISTINGS|OPENAI_.*|H(ETZNER|CLOUD)_TOKEN|JOKER_DYNDNS_.*|REGISTRY_.*|NETDATA_.*|ADMIN_.*)$
|
||||
sops_lastmodified=2025-12-11T11:36:56Z
|
||||
sops_mac=ENC[AES256_GCM,data:bnDwxj/t2X+vkq1nd2Bej23GBn3hALXW6PAp4FyoAlvwajztp9U2eyF7voLQDeX1kurVBuACPExzzMnerEXOebF9l5SGcIYfvtVj9kk4I0WRCbVBt/QKgEtqYJ3l1TXrDe8ZPTj6O2rK6WW36RDExFDu3tzzvVEaHErZMjAhD1U=,iv:hBwrHOvabZEqeWHSFGvk5sHYbJiF6/3wY1JXgaevB9w=,tag:2fZQ9AQoHQJ+zte2yLpi5g==,type:str]
|
||||
sops_version=3.11.0
|
||||
|
|
@ -12,5 +12,5 @@ export APP_ENV="production"
|
|||
export CLUSTER_ISSUER="$PROD_CLUSTER_ISSUER"
|
||||
export INGRESS_CLASS
|
||||
|
||||
# optionally set APP_SECRET in the environment before running
|
||||
# optionally set AUTH_SECRET (and other secrets) in the environment before running
|
||||
bash deploy/deploy.sh
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@ export APP_ENV="staging"
|
|||
export CLUSTER_ISSUER="$STAGING_CLUSTER_ISSUER"
|
||||
export INGRESS_CLASS
|
||||
|
||||
# optionally set APP_SECRET in the environment before running
|
||||
# optionally set AUTH_SECRET (and other secrets) in the environment before running
|
||||
bash deploy/deploy.sh
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ export CLUSTER_ISSUER="${TEST_CLUSTER_ISSUER}"
|
|||
export INGRESS_CLASS
|
||||
export APP_REPLICAS="${APP_REPLICAS:-1}"
|
||||
|
||||
# optionally set APP_SECRET and DATABASE_URL (pointing to lomavuokraus_testing) in the environment before running
|
||||
# optionally set AUTH_SECRET and DATABASE_URL (pointing to lomavuokraus_testing) in the environment before running
|
||||
bash deploy/deploy.sh
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
if [[ -f scripts/load-secrets.sh ]]; then
|
||||
# Export secrets from creds/secrets.env (dotenv) when available.
|
||||
source scripts/load-secrets.sh
|
||||
fi
|
||||
source deploy/env.sh
|
||||
|
||||
if [[ ! -f deploy/.last-image ]]; then
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ flowchart LR
|
|||
<ul>
|
||||
<li>Source: Next.js app with TypeScript and Prisma.</li>
|
||||
<li>Env: <code>.env</code> (local), K8s Secret <code>lomavuokraus-web-secrets</code> in cluster.</li>
|
||||
<li>Local secrets: <code>creds/secrets.env</code> (dotenv) loadable via <code>scripts/load-secrets.sh</code>.</li>
|
||||
<li>Prisma schema: <code>prisma/schema.prisma</code>, migrations in <code>prisma/migrations/</code>.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -84,7 +85,7 @@ flowchart LR
|
|||
<h2>Config & Env Vars</h2>
|
||||
<ul>
|
||||
<li>From ConfigMap (public): <code>NEXT_PUBLIC_SITE_URL</code>, <code>NEXT_PUBLIC_API_BASE</code>, <code>APP_ENV</code>.</li>
|
||||
<li>From Secret: DB URL, AUTH_SECRET, SMTP, DKIM, etc.</li>
|
||||
<li>From Secret: DB URL, AUTH_SECRET, SMTP, DKIM, etc. (materialize from <code>creds/secrets.env</code>).</li>
|
||||
<li>App env resolution: <code>process.env.*</code> in Next server code.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
|||
41
docs/secrets.md
Normal file
41
docs/secrets.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Secrets workflow (sops + age)
|
||||
|
||||
## Files
|
||||
- `creds/age-key.txt`: age private key (keep out of git; store in a password manager). Public key is in the header.
|
||||
- `creds/secrets.enc.env`: encrypted dotenv managed by sops/age (committable).
|
||||
- `creds/secrets.env`: decrypted dotenv (git-ignored) produced when loading secrets; not committed.
|
||||
- Legacy plaintext secrets moved to `creds/deprecated/` for reference.
|
||||
|
||||
## Editing secrets
|
||||
```bash
|
||||
# Ensure sops+age binaries are available
|
||||
sops creds/secrets.enc.env
|
||||
```
|
||||
Sops will decrypt, open in $EDITOR, and re-encrypt on save. The age recipient is configured in `.sops.yaml`.
|
||||
|
||||
## Loading secrets locally
|
||||
```bash
|
||||
source scripts/load-secrets.sh
|
||||
```
|
||||
This decrypts `creds/secrets.enc.env` to `creds/secrets.env` if needed (requires sops) and exports all variables.
|
||||
|
||||
## Adding developers
|
||||
- Share `creds/age-key.txt` securely (password manager). They need the age secret key to decrypt.
|
||||
- No change to `.sops.yaml` is needed unless you rotate keys.
|
||||
|
||||
## Deploys/CI
|
||||
- `deploy/deploy.sh` sources `scripts/load-secrets.sh`, so providing `creds/secrets.enc.env` + age key is enough for secret env injection.
|
||||
|
||||
## Rotating keys
|
||||
- Generate a new age key: `age-keygen -o creds/age-key.txt` (keep old backup if you need to reencrypt).
|
||||
- Update `.sops.yaml` recipient to the new public key.
|
||||
- Re-encrypt: `SOPS_AGE_KEY_FILE=creds/age-key.txt sops --encrypt --in-place creds/secrets.enc.env`.
|
||||
|
||||
## Per-user age keys
|
||||
- Keys live under `creds/age/<user>.key` (git-ignored) and carry a public key in the header.
|
||||
- Helper: `./scripts/manage-age-key.sh add alice` generates a key and appends the recipient to `.sops.yaml`.
|
||||
- Remove: `./scripts/manage-age-key.sh remove alice` deletes the key file and strips the recipient (re-encrypt afterwards).
|
||||
- List: `./scripts/manage-age-key.sh list`.
|
||||
- After adding/removing recipients, re-encrypt secrets: `sops --encrypt --in-place creds/secrets.enc.env`.
|
||||
|
||||
Share each user’s private key securely (password manager). Multiple recipients in `.sops.yaml` allow any listed user to decrypt.
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<ul>
|
||||
<li>Script: <code>scripts/run-test-suite.sh</code></li>
|
||||
<li>Runs: <code>npm audit</code> (high), Trivy fs scan, ZAP baseline.</li>
|
||||
<li>Outputs: <code>reports/runs/<timestamp>/summary.html</code> with links to all tool reports.</li>
|
||||
<li>Outputs: <code>reports/runs/<timestamp>/summary.html</code> with links to all tool reports and a textual summary printed to the console. Index of all runs: <code>reports/index.html</code>.</li>
|
||||
<li>Config:
|
||||
<ul>
|
||||
<li><code>TARGET</code>: ZAP target URL (default test env).</li>
|
||||
|
|
|
|||
55
forgejo/README.md
Normal file
55
forgejo/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
Forgejo on halla-aho.net
|
||||
========================
|
||||
|
||||
Lightweight Git hosting + CI with Forgejo (Gitea fork) behind Apache on halla-aho.net.
|
||||
|
||||
What’s included
|
||||
- Docker Compose for Forgejo + SSH and an Actions runner (`forgejo/docker-compose.yml`).
|
||||
- Apache vhost snippet (added to `default-ssl.conf`) to reverse-proxy `git.halla-aho.net` to the Forgejo container on port 3200.
|
||||
- SSH for git is exposed on host port 2223 (mapped to container 22); change in compose if that port is taken.
|
||||
|
||||
Prereqs
|
||||
- Docker installed on halla-aho.net.
|
||||
- SSLMate certs for `git.halla-aho.net` placed on the host (paths referenced in `default-ssl.conf`).
|
||||
- A DNS record for `git.halla-aho.net` pointing to the server.
|
||||
|
||||
Deploy Forgejo
|
||||
1) Create host dirs for data:
|
||||
```
|
||||
sudo mkdir -p /srv/forgejo/data /srv/forgejo/runner
|
||||
sudo chown -R $USER:$USER /srv/forgejo
|
||||
```
|
||||
2) Start the Forgejo service:
|
||||
```
|
||||
docker compose -f forgejo/docker-compose.yml up -d forgejo
|
||||
```
|
||||
- If port 2223 is already in use, edit `forgejo/docker-compose.yml` (`ports:` and `FORGEJO__SERVER__SSH_PORT`) to another free port, then rerun the command.
|
||||
3) Configure Apache (already added to `default-ssl.conf`):
|
||||
- VirtualHost `git.halla-aho.net:9443` proxies to `http://127.0.0.1:3200/`.
|
||||
- TLS files: `/etc/apache2/ssl/git.halla-aho.net.{crt,key,chain.crt}` (update if different).
|
||||
- Enable the site and reload Apache.
|
||||
4) Finish setup in the UI at `https://git.halla-aho.net/`:
|
||||
- Create the admin user.
|
||||
- Configure SMTP in the admin UI (Mail settings).
|
||||
- Set `ROOT_URL`/`SSH_DOMAIN` if you change ports/domains.
|
||||
|
||||
Register the Actions runner
|
||||
1) In Forgejo, create a runner registration token (Site Admin → Runners).
|
||||
2) Register the runner (writes `/srv/forgejo/runner/config.yaml`):
|
||||
```
|
||||
docker compose -f forgejo/docker-compose.yml run --rm runner \
|
||||
forgejo-runner register \
|
||||
--instance https://git.halla-aho.net \
|
||||
--token <REGISTRATION_TOKEN> \
|
||||
--name halla-runner \
|
||||
--labels docker \
|
||||
--config /data/config.yaml
|
||||
```
|
||||
3) Start the runner:
|
||||
```
|
||||
docker compose -f forgejo/docker-compose.yml up -d runner
|
||||
```
|
||||
|
||||
CI workflow for this repo
|
||||
- Add workflows under `.forgejo/workflows/`.
|
||||
- Example included: `ci.yml` runs npm install + lint + type-check + format check on push/PR using the `docker` runner label.
|
||||
36
forgejo/docker-compose.yml
Normal file
36
forgejo/docker-compose.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:10
|
||||
container_name: forgejo
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- TZ=Europe/Helsinki
|
||||
- FORGEJO__SERVER__DOMAIN=git.halla-aho.net
|
||||
- FORGEJO__SERVER__ROOT_URL=https://git.halla-aho.net/
|
||||
- FORGEJO__SERVER__HTTP_PORT=3000
|
||||
- FORGEJO__SERVER__PROTOCOL=http
|
||||
- FORGEJO__SERVER__SSH_DOMAIN=git.halla-aho.net
|
||||
- FORGEJO__SERVER__SSH_PORT=2223
|
||||
- FORGEJO__DATABASE__DB_TYPE=sqlite3
|
||||
- FORGEJO__DATABASE__PATH=/data/forgejo.db
|
||||
- FORGEJO__MAILER__ENABLED=false
|
||||
volumes:
|
||||
- /srv/forgejo/data:/data
|
||||
ports:
|
||||
- "3200:3000" # HTTP (Apache will reverse proxy)
|
||||
- "2223:22" # SSH for git
|
||||
|
||||
runner:
|
||||
image: codeberg.org/forgejo/runner:4
|
||||
container_name: forgejo-runner
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- forgejo
|
||||
volumes:
|
||||
- /srv/forgejo/runner:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command: ["forgejo-runner", "daemon", "--config", "/data/config.yaml"]
|
||||
|
|
@ -48,6 +48,8 @@ const baseMessages = {
|
|||
footerAbout: 'About',
|
||||
footerPricing: 'Pricing',
|
||||
footerPrivacy: 'Privacy & cookies',
|
||||
footerCookieNotice:
|
||||
'We use only essential cookies for login and security. By using this site you consent; if you do not accept, please do not use the site.',
|
||||
loginTitle: 'Login',
|
||||
emailLabel: 'Email',
|
||||
passwordLabel: 'Password',
|
||||
|
|
@ -389,6 +391,8 @@ const baseMessages = {
|
|||
footerAbout: 'Tietoa',
|
||||
footerPricing: 'Hinnasto',
|
||||
footerPrivacy: 'Tietosuoja ja evästeet',
|
||||
footerCookieNotice:
|
||||
'Käytämme vain välttämättömiä evästeitä kirjautumiseen ja turvallisuuteen. Käyttämällä sivustoa hyväksyt evästeet; jos et hyväksy, älä käytä sivustoa.',
|
||||
loginTitle: 'Kirjaudu sisään',
|
||||
emailLabel: 'Sähköposti',
|
||||
passwordLabel: 'Salasana',
|
||||
|
|
@ -723,6 +727,8 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
|
|||
evChargingNo: 'Ingen laddning i närheten',
|
||||
evChargingAny: 'Alla',
|
||||
evChargingExplain: 'Finns det EV-laddning på plats eller i närheten?',
|
||||
footerCookieNotice:
|
||||
'Vi använder endast nödvändiga cookies för inloggning och säkerhet. Genom att använda sajten godkänner du cookies; om du inte gör det, använd inte webbplatsen.',
|
||||
};
|
||||
|
||||
export const messages = { ...baseMessages, sv: svMessages } as const;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit",
|
||||
"format:check": "prettier --check ."
|
||||
"format:check": "prettier --check .",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.0.0",
|
||||
|
|
|
|||
31
scripts/load-secrets.sh
Normal file
31
scripts/load-secrets.sh
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
# Shell helper to export secrets from a single dotenv file.
|
||||
# Usage: source scripts/load-secrets.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SECRETS_FILE="${SECRETS_FILE:-$ROOT_DIR/creds/secrets.env}"
|
||||
ENCRYPTED_FILE="${ENCRYPTED_FILE:-$ROOT_DIR/creds/secrets.enc.env}"
|
||||
|
||||
ensure_decrypted() {
|
||||
if [[ -f "$SECRETS_FILE" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -f "$ENCRYPTED_FILE" ]]; then
|
||||
if command -v sops >/dev/null 2>&1; then
|
||||
echo "Decrypting $ENCRYPTED_FILE -> $SECRETS_FILE"
|
||||
sops -d "$ENCRYPTED_FILE" >"$SECRETS_FILE"
|
||||
else
|
||||
echo "sops not found and $SECRETS_FILE is missing. Install sops or set SECRETS_FILE." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_decrypted || exit 0
|
||||
|
||||
echo "Loading secrets from $SECRETS_FILE"
|
||||
set -a
|
||||
source "$SECRETS_FILE"
|
||||
set +a
|
||||
114
scripts/manage-age-key.sh
Executable file
114
scripts/manage-age-key.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env bash
|
||||
# Manage per-user age keys and sops recipients.
|
||||
# - Creates a new age keypair under creds/age/<user>.key
|
||||
# - Updates .sops.yaml recipients list for creds/secrets.enc.env
|
||||
# - Can delete a user (removes key file and recipient entry)
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/manage-age-key.sh add <username>
|
||||
# ./scripts/manage-age-key.sh remove <username>
|
||||
# ./scripts/manage-age-key.sh list
|
||||
#
|
||||
# Notes:
|
||||
# - Keep private keys out of git; creds/age/ is git-ignored.
|
||||
# - After add/remove, re-encrypt secrets: sops --encrypt --in-place creds/secrets.enc.env
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
AGE_DIR="$ROOT_DIR/creds/age"
|
||||
SOPS_CONFIG="$ROOT_DIR/.sops.yaml"
|
||||
|
||||
usage() {
|
||||
sed -n '1,12p' "${BASH_SOURCE[0]}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
list_keys() {
|
||||
if [[ -d "$AGE_DIR" ]]; then
|
||||
find "$AGE_DIR" -maxdepth 1 -type f -name '*.key' -printf '%f\n'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_age_dir() {
|
||||
mkdir -p "$AGE_DIR"
|
||||
}
|
||||
|
||||
add_user() {
|
||||
local user="$1"
|
||||
ensure_age_dir
|
||||
local key_file="$AGE_DIR/${user}.key"
|
||||
if [[ -f "$key_file" ]]; then
|
||||
echo "Key already exists: $key_file"
|
||||
exit 1
|
||||
fi
|
||||
echo "Generating age key for ${user} -> ${key_file}"
|
||||
age-keygen -o "$key_file" >/dev/null
|
||||
local pub
|
||||
pub=$(grep '^# public key:' "$key_file" | awk '{print $4}')
|
||||
echo "Public key: $pub"
|
||||
|
||||
if [[ ! -f "$SOPS_CONFIG" ]]; then
|
||||
echo "sops config not found at $SOPS_CONFIG; skipping recipient update."
|
||||
echo "Add this recipient manually: $pub"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -q "$pub" "$SOPS_CONFIG"; then
|
||||
echo "Recipient already present in $SOPS_CONFIG"
|
||||
else
|
||||
echo "Adding recipient to $SOPS_CONFIG"
|
||||
# Append under the first age key list.
|
||||
tmp=$(mktemp)
|
||||
awk -v pub="$pub" '
|
||||
/^ *- age:/ { in_age=1; print; next }
|
||||
in_age && /^ *- age1/ { print " - " pub; in_age=0 }
|
||||
{ print }
|
||||
' "$SOPS_CONFIG" >"$tmp"
|
||||
mv "$tmp" "$SOPS_CONFIG"
|
||||
fi
|
||||
|
||||
echo "Done. Re-encrypt secrets after updating recipients:"
|
||||
echo " sops --encrypt --in-place creds/secrets.enc.env"
|
||||
}
|
||||
|
||||
remove_user() {
|
||||
local user="$1"
|
||||
local key_file="$AGE_DIR/${user}.key"
|
||||
if [[ -f "$key_file" ]]; then
|
||||
rm -f "$key_file"
|
||||
echo "Removed key $key_file"
|
||||
else
|
||||
echo "No key file for $user at $key_file"
|
||||
fi
|
||||
if [[ -f "$SOPS_CONFIG" ]]; then
|
||||
local pub
|
||||
pub=$(grep '^# public key:' "$key_file" 2>/dev/null | awk '{print $4}')
|
||||
if [[ -n "${pub:-}" ]]; then
|
||||
tmp=$(mktemp)
|
||||
awk -v pub="$pub" '!($0 ~ pub)' "$SOPS_CONFIG" >"$tmp"
|
||||
mv "$tmp" "$SOPS_CONFIG"
|
||||
echo "Removed recipient from $SOPS_CONFIG (if present)."
|
||||
fi
|
||||
fi
|
||||
echo "Re-encrypt secrets to drop removed recipients if needed:"
|
||||
echo " sops --encrypt --in-place creds/secrets.enc.env"
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
add)
|
||||
[[ $# -eq 2 ]] || usage
|
||||
add_user "$2"
|
||||
;;
|
||||
remove|rm|delete)
|
||||
[[ $# -eq 2 ]] || usage
|
||||
remove_user "$2"
|
||||
;;
|
||||
list|ls)
|
||||
list_keys
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
|
|
@ -21,6 +21,10 @@ RUN_DIR="reports/runs/${RUN_TS}"
|
|||
mkdir -p "$RUN_DIR"
|
||||
|
||||
SUMMARY_ROWS=()
|
||||
SUMMARY_TEXT_ROWS=()
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
SKIP_COUNT=0
|
||||
|
||||
log() {
|
||||
echo "[$(date +"%H:%M:%S")] $*"
|
||||
|
|
@ -30,7 +34,67 @@ record_result() {
|
|||
local name="$1"; shift
|
||||
local status="$1"; shift
|
||||
local detail="$1"; shift
|
||||
local detail_text="$1"; shift
|
||||
SUMMARY_ROWS+=("<tr><td>${name}</td><td>${status}</td><td>${detail}</td></tr>")
|
||||
SUMMARY_TEXT_ROWS+=("${name}: ${status}${detail_text:+ - ${detail_text}}")
|
||||
case "$status" in
|
||||
PASS) PASS_COUNT=$((PASS_COUNT + 1)) ;;
|
||||
FAIL) FAIL_COUNT=$((FAIL_COUNT + 1)) ;;
|
||||
SKIP) SKIP_COUNT=$((SKIP_COUNT + 1)) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_index() {
|
||||
local runs_dir="reports/runs"
|
||||
local index_file="reports/index.html"
|
||||
local items=""
|
||||
|
||||
if [ -d "$runs_dir" ]; then
|
||||
while IFS= read -r run_dir; do
|
||||
local meta_file="${runs_dir}/${run_dir}/meta.txt"
|
||||
[ -f "$meta_file" ] || continue
|
||||
|
||||
local run_ts target pass fail skip
|
||||
run_ts=$(grep -E '^RUN_TS=' "$meta_file" | head -n1 | cut -d= -f2-)
|
||||
target=$(grep -E '^TARGET=' "$meta_file" | head -n1 | cut -d= -f2-)
|
||||
pass=$(grep -E '^PASS_COUNT=' "$meta_file" | head -n1 | cut -d= -f2-)
|
||||
fail=$(grep -E '^FAIL_COUNT=' "$meta_file" | head -n1 | cut -d= -f2-)
|
||||
skip=$(grep -E '^SKIP_COUNT=' "$meta_file" | head -n1 | cut -d= -f2-)
|
||||
|
||||
run_ts="${run_ts:-$run_dir}"
|
||||
target="${target:-unknown}"
|
||||
pass="${pass:-0}"
|
||||
fail="${fail:-0}"
|
||||
skip="${skip:-0}"
|
||||
|
||||
items+=$'\n'" <li><div class=\"run\"><a href=\"runs/${run_ts}/summary.html\">${run_ts}</a></div><div class=\"meta\">Target: ${target} | Pass: ${pass} | Fail: ${fail} | Skip: ${skip}</div></li>"
|
||||
done < <(ls "$runs_dir" 2>/dev/null | sort -r)
|
||||
fi
|
||||
|
||||
cat >"$index_file" <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Lomavuokraus Test Suite Runs</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 16px; background: #0b0d11; color: #e9ecf1; }
|
||||
a { color: #7cc7ff; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { margin-bottom: 10px; padding: 10px; border: 1px solid #1f2937; border-radius: 6px; background: #111827; }
|
||||
.run { font-weight: bold; }
|
||||
.meta { font-size: 0.95em; color: #cfd6e0; margin-top: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Suite Runs</h1>
|
||||
<ul>
|
||||
${items:-" <li>No runs found.</li>"}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
}
|
||||
|
||||
# 1) npm audit
|
||||
|
|
@ -39,13 +103,13 @@ if command -v npm >/dev/null 2>&1; then
|
|||
AUDIT_JSON="$RUN_DIR/npm-audit.json"
|
||||
AUDIT_TXT="$RUN_DIR/npm-audit.txt"
|
||||
if npm audit --audit-level=high --json >"$AUDIT_JSON" 2>"$AUDIT_TXT"; then
|
||||
record_result "npm audit" "PASS" "<a href=\"npm-audit.txt\">text</a> | <a href=\"npm-audit.json\">json</a>"
|
||||
record_result "npm audit" "PASS" "<a href=\"npm-audit.txt\">text</a> | <a href=\"npm-audit.json\">json</a>" "reports: ${AUDIT_TXT}, ${AUDIT_JSON}"
|
||||
else
|
||||
record_result "npm audit" "FAIL" "<a href=\"npm-audit.txt\">text</a> | <a href=\"npm-audit.json\">json</a>"
|
||||
record_result "npm audit" "FAIL" "<a href=\"npm-audit.txt\">text</a> | <a href=\"npm-audit.json\">json</a>" "reports: ${AUDIT_TXT}, ${AUDIT_JSON}"
|
||||
fi
|
||||
else
|
||||
log "npm not found; skipping npm audit"
|
||||
record_result "npm audit" "SKIP" "npm not available"
|
||||
record_result "npm audit" "SKIP" "npm not available" "npm not available"
|
||||
fi
|
||||
|
||||
# 2) Lint / type-check / format / tests
|
||||
|
|
@ -55,20 +119,20 @@ run_npm_check() {
|
|||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
log "npm not found; skipping ${name}"
|
||||
record_result "${name}" "SKIP" "npm not available"
|
||||
record_result "${name}" "SKIP" "npm not available" "npm not available"
|
||||
return
|
||||
fi
|
||||
|
||||
if npm run 2>/dev/null | grep -qE "^ ${name}$"; then
|
||||
log "Running ${name}..."
|
||||
if npm run "${name}" >"$outfile" 2>&1; then
|
||||
record_result "${name}" "PASS" "<a href=\"${name}.txt\">log</a>"
|
||||
record_result "${name}" "PASS" "<a href=\"${name}.txt\">log</a>" "log: ${outfile}"
|
||||
else
|
||||
record_result "${name}" "FAIL" "<a href=\"${name}.txt\">log</a>"
|
||||
record_result "${name}" "FAIL" "<a href=\"${name}.txt\">log</a>" "log: ${outfile}"
|
||||
fi
|
||||
else
|
||||
log "npm script '${name}' not defined; skipping"
|
||||
record_result "${name}" "SKIP" "script not defined"
|
||||
record_result "${name}" "SKIP" "script not defined" "script not defined"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -84,13 +148,13 @@ if command -v trivy >/dev/null 2>&1; then
|
|||
log "Running Trivy (${TRIVY_MODE}) on ${TRIVY_TARGET}..."
|
||||
TRIVY_TXT="$RUN_DIR/trivy.txt"
|
||||
if trivy "${TRIVY_MODE}" --severity HIGH,CRITICAL --timeout 5m "$TRIVY_TARGET" >"$TRIVY_TXT"; then
|
||||
record_result "Trivy (${TRIVY_MODE})" "PASS" "<a href=\"trivy.txt\">report</a>"
|
||||
record_result "Trivy (${TRIVY_MODE})" "PASS" "<a href=\"trivy.txt\">report</a>" "report: ${TRIVY_TXT}"
|
||||
else
|
||||
record_result "Trivy (${TRIVY_MODE})" "FAIL" "<a href=\"trivy.txt\">report</a>"
|
||||
record_result "Trivy (${TRIVY_MODE})" "FAIL" "<a href=\"trivy.txt\">report</a>" "report: ${TRIVY_TXT}"
|
||||
fi
|
||||
else
|
||||
log "Trivy not found; skipping"
|
||||
record_result "Trivy" "SKIP" "trivy not available"
|
||||
record_result "Trivy" "SKIP" "trivy not available" "trivy not available"
|
||||
fi
|
||||
|
||||
# 4) OWASP ZAP baseline
|
||||
|
|
@ -99,9 +163,9 @@ ZAP_DIR="$RUN_DIR/zap"
|
|||
mkdir -p "$ZAP_DIR"
|
||||
log "Running ZAP baseline against ${TARGET}..."
|
||||
if TARGET="$TARGET" REPORT_DIR="$ZAP_DIR" "${BASH_SOURCE%/*}/zap-baseline.sh"; then
|
||||
record_result "OWASP ZAP baseline" "PASS" "<a href=\"zap/zap-report.html\">HTML</a> | <a href=\"zap/zap-report.json\">JSON</a>"
|
||||
record_result "OWASP ZAP baseline" "PASS" "<a href=\"zap/zap-report.html\">HTML</a> | <a href=\"zap/zap-report.json\">JSON</a>" "reports: ${ZAP_DIR}/zap-report.html, ${ZAP_DIR}/zap-report.json"
|
||||
else
|
||||
record_result "OWASP ZAP baseline" "FAIL" "<a href=\"zap/zap-report.html\">HTML</a> | <a href=\"zap/zap-report.json\">JSON</a>"
|
||||
record_result "OWASP ZAP baseline" "FAIL" "<a href=\"zap/zap-report.html\">HTML</a> | <a href=\"zap/zap-report.json\">JSON</a>" "reports: ${ZAP_DIR}/zap-report.html, ${ZAP_DIR}/zap-report.json"
|
||||
fi
|
||||
|
||||
# Summary HTML
|
||||
|
|
@ -134,5 +198,21 @@ cat >"$SUMMARY_FILE" <<EOF
|
|||
</html>
|
||||
EOF
|
||||
|
||||
META_FILE="$RUN_DIR/meta.txt"
|
||||
cat >"$META_FILE" <<EOF
|
||||
RUN_TS=${RUN_TS}
|
||||
TARGET=${TARGET}
|
||||
PASS_COUNT=${PASS_COUNT}
|
||||
FAIL_COUNT=${FAIL_COUNT}
|
||||
SKIP_COUNT=${SKIP_COUNT}
|
||||
EOF
|
||||
|
||||
update_index
|
||||
|
||||
log "Summary:"
|
||||
for row in "${SUMMARY_TEXT_ROWS[@]}"; do
|
||||
echo " - ${row}"
|
||||
done
|
||||
|
||||
log "Done. Reports in ${RUN_DIR}"
|
||||
echo "Open ${SUMMARY_FILE} in a browser for the summary."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue