Spillkveld – driftsoppsett under arbeid
+Dette er en midlertidig testside for publisering på VM.
+Den ferdige appen og GraphQL-backenden er ikke publisert ennå.
+diff --git a/.gitignore b/.gitignore index d6e42ef..22519f7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ coverage/ .vite/ *.local *.tsbuildinfo +playwright-report/ +test-results/ +.env +.env.* +!.env.example diff --git a/README.md b/README.md index 162a80c..84251a9 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,217 @@ # Spillkveld -Spillkveld er en nettside som skal gjøre det enklere for venner og familier å finne brettspill som passer til antall personer og tiden de har. Dette er den første, kjørbare prototypen i IT2810. Den lar medstudenter prøve hovedideen før vi kobler på en backend og et større datasett. +Spillkveld hjelper venner og familier å finne brettspill som passer til antall personer og tiden de har. Applikasjonen er utviklet av gruppe T32 i IT2810. -## Kom i gang +**Prøv appen:** [Spillkveld på VM](http://it2810-32.idi.ntnu.no/project2/). Bruk NTNU-nettet eller NTNU VPN utenfor campus. -Du trenger Node.js 22 eller nyere og npm. Kjør følgende fra prosjektmappen: +**Repo:** [IT2810-H26/T32-Project-2](https://git.ntnu.no/IT2810-H26/T32-Project-2). + +## Funksjoner + +- Søk i navn og beskrivelse, og kombiner filtre for antall spillere, spilletid og kategori. +- Sorter etter navn eller spilletid, og bla gjennom seks spill per side. +- Åpne spilldetaljer og gå tilbake med søk og filtre bevart. +- Legg til og fjern spill i «Kveldens utvalg». Listen lagres i nettleseren og beholdes etter ny innlasting. +- Mobiltilpasset grensesnitt med tastaturbetjening, synlig fokus og meldinger ved lasting, feil og ingen treff. + +Spillene hentes fra **PostgreSQL gjennom GraphQL**. Søk, filtrering, sortering og sidevisning skjer i backend. + +**De 12 spillene er eksempeldata**, med forenklede beskrivelser, kategorier og spilletider. De kommer ikke fra et valgt eller offisielt brettspilldatasett. Symbolene er dekorative ikoner. + +## Lokal oppstart + +Du trenger Git, npm, Node.js 22.22 eller 24, og en kjørende PostgreSQL-installasjon. Løsningen er testet med PostgreSQL 17.11 og 18.6. + +### 1. Installer avhengigheter + +```bash +git clone https://git.ntnu.no/IT2810-H26/T32-Project-2.git +cd T32-Project-2 +npm ci +npm --prefix server ci +``` + +### 2. Opprett databaser + +Åpne `psql` som lokal PostgreSQL-administrator. På en ny installasjon: + +```sql +CREATE ROLE spillkveld_app LOGIN; +\password spillkveld_app +CREATE DATABASE spillkveld OWNER spillkveld_app; +CREATE DATABASE spillkveld_test OWNER spillkveld_app; +``` + +`\password` ber om et passord til den lokale databasebrukeren. Testdatabasen brukes bare til automatiske tester. + +### 3. Konfigurer og fyll databasen + +Kopier `server/.env.example` til `server/.env`, og sett inn ditt lokale databasepassord: + +```dotenv +DATABASE_URL=postgresql://spillkveld_app:DITT_LOKALE_PASSORD@127.0.0.1:5432/spillkveld +TEST_DATABASE_URL=postgresql://spillkveld_app:DITT_LOKALE_PASSORD@127.0.0.1:5432/spillkveld_test +PORT=3001 +HOST=127.0.0.1 +``` + +Spesialtegn i passordet må URL-kodes. Faktiske passord og `.env`-filer skal ikke legges i Git. + +Kjør fra prosjektroten: + +```bash +npm --prefix server run db:setup +npm --prefix server run db:seed +``` + +Seed kan kjøres flere ganger uten duplikater; eksisterende spill blir ikke oppdatert. + +### 4. Start appen + +Start backend: + +```bash +npm --prefix server run dev +``` + +Start frontend i en annen terminal, også fra prosjektroten: ```bash -npm install npm run dev ``` -Åpne adressen som Vite viser, vanligvis `http://localhost:5173`. +Åpne [http://localhost:5173/project2/](http://localhost:5173/project2/). La `VITE_GRAPHQL_URL` være tom ved lokal utvikling; Vite videresender `/graphql` til backend på port 3001. + +## Arkitektur + +Frontend bruker React, TypeScript og Vite. Apollo Client sender GraphQL-spørringer til en Node.js-backend med GraphQL Yoga. Backend bruker `pg` til å hente spill fra PostgreSQL. Apache serverer frontend og videresender API-kall på VM-en. + +| Mappe | Innhold | +| ----------------------- | ------------------------------------------------------------ | +| `src/` | Grensesnitt, Apollo-oppsett og frontend-tester | +| `server/src/` | GraphQL-skjema, validering og parameteriserte SQL-spørringer | +| `server/db/` | Tabellskjema og de 12 eksempelspillene | +| `server/tests/`, `e2e/` | Backend-tester og Playwright-test | +| `deploy/` | Apache, systemd og publiseringsskript | + +API-et har fire spørringer: `games` for katalogen, `game` for detaljer, `gamesByIds` for utvalgte spill og `categories` for kategorier. Backend beregner totalt antall treff før sidevisning og sikrer stabil sortering. Databasen krever gyldige spillergrenser, positiv spilletid og at nødvendige felt er fylt ut. Feltene er definert i `server/db/schema.sql` og `server/src/schema.ts`. + +## Tester og kodekvalitet -For å kontrollere prosjektet: +Kjør fra prosjektroten: ```bash npm test +npm run test:backend npm run lint +npm run format:check npm run build +npm --prefix server run build ``` -`npm run preview` viser produksjonsbygget lokalt etter `npm run build`. +Frontend-testene bruker mockede GraphQL-svar og trenger ingen database. Backend-testene bruker `TEST_DATABASE_URL` med `localhost` eller `127.0.0.1` og databasen `spillkveld_test`. **Testene tømmer spilltabellen i testdatabasen** og setter inn egne testdata. -## Dette fungerer nå +Lint kontrollerer både frontend og backend. Formateringskommandoen dekker frontend og Vite-konfigurasjonen; øvrige filer kan kontrolleres med: -- Søk etter ord i spillnavn eller beskrivelse. -- Filtre for antall spillere, maksimal spilletid og kategori. Filtrene kan kombineres. -- Sortering etter navn eller kortest spilletid, og sidevisning av søkeresultater. -- Detaljvisning med beskrivelse og praktisk informasjon. Søk og filtre beholdes når du går tilbake. -- «Kveldens utvalg»: legg til og fjern spill. Listen lagres i nettleserens lokale lagring på samme enhet. -- Mobiltilpasset grensesnitt med synlig tastaturfokus, tydelige feltnavn og melding ved null treff. +```bash +npx prettier --check "server/**/*.{ts,mjs,json}" "e2e/**/*.ts" playwright.config.ts README.md +``` + +### Ende-til-ende-test + +Installer nettleseren én gang med `npx playwright install chromium`. Med lokal frontend og backend i gang kjører du `npm run test:e2e`. Mot VM fra PowerShell: + +```powershell +$env:PLAYWRIGHT_BASE_URL = 'http://it2810-32.idi.ntnu.no/project2/' +npm run test:e2e +Remove-Item Env:PLAYWRIGHT_BASE_URL +``` + +Testen søker etter Azul, kontrollerer ekte GraphQL-svar, åpner detaljer og går tilbake med søket bevart. + +### Verifisert 16. oktober 2026 + +- Alle 13 frontend-tester og 22 backend-tester passerer. Backend-testene er også kjørt på VM med det felles databaseskjemaet. +- Lint, frontend-formatering og begge produksjonsbygg passerer. +- Ende-til-ende-testen passerer mot den publiserte appen, både før og etter VM-omstart. Appens hovedfunksjoner er også prøvd manuelt. +- Apache, PostgreSQL og backend startet automatisk etter omstart. Prosjekt 1 fungerer fortsatt. +- Seed kan kjøres flere ganger uten duplikater. Databasen avviser ugyldige spillergrenser, spilletid og manglende navn. + +## Bygg og drift på VM + +Frontend bygges med Vite-base `/project2/` og `VITE_GRAPHQL_URL=/project2/graphql`. Fra PowerShell i prosjektroten: + +```powershell +$env:VITE_GRAPHQL_URL = '/project2/graphql' +npm run build +Remove-Item Env:VITE_GRAPHQL_URL +npm --prefix server run build +``` + +| Del | Oppsett på `it2810-32.idi.ntnu.no` | +| ---------- | -------------------------------------------------------------------------------- | +| Frontend | `dist/` publiseres til `/var/www/html/project2` | +| Backend | `/opt/spillkveld/server`, port `127.0.0.1:3001` | +| API-proxy | Apache videresender `/project2/graphql` til backend `/graphql` | +| PostgreSQL | Installert direkte uten Docker, database `spillkveld` på `127.0.0.1:5432` | +| Miljøfil | `/etc/spillkveld/backend.env`, kun lesbar for root | +| Tjeneste | `spillkveld-backend.service`, kjører `npm start` som systembrukeren `spillkveld` | + +VM-en bruker Node.js 22.22.1, npm 9.2.0, PostgreSQL 18.6 og Apache 2.4.66. Systemd leser `DATABASE_URL`, `HOST` og `PORT` fra miljøfilen. Databasebrukeren `spillkveld_app` eier databasen uten administratorrettigheter. + +Oppsettet forutsetter at Node.js, npm, PostgreSQL og Apache er installert på VM-en, og at prosjekt 1 svarer under `/project1/`. Kjør de to første kommandoene fra mappen som inneholder `deploy/`. + +Førstegangsoppsettet består av tre steg: -**Spillene er statiske eksempeldata.** Beskrivelser, kategorier og spilletider er forenklet for å demonstrere prototypen. De er ikke hentet fra et valgt eller offisielt brettspilldatasett. Illustrasjonene er dekorative symboler, ikke spillbilder. +1. Kjør `sudo bash deploy/setup-database.sh` for å opprette database, bruker og miljøfil. +2. Kjør `sudo bash deploy/install-runtime.sh` for å installere Apache- og tjenesteoppsett. +3. Klargjør en utgivelsesmappe med backend under `server/`, bygget frontend under `dist/` og en kopi av `deploy/publish-first-release.sh` direkte i mappen. Installer, bygg og kontroller backend der, og kjør `sudo bash publish-first-release.sh`. Skriptet er tilpasset gruppens VM og brukeren `bjoabe`. -## Videre arbeid +Førstegangspubliseringen er gjennomført. Skriptet skal ikke kjøres på nytt på den publiserte appen. Den midlertidige testtjenesten er deaktivert. Ved senere oppdateringer bygges og testes en ny kopi før driftsfilene erstattes og backend startes på nytt. Behold miljøfilen og eksisterende data. -Vi skal velge og vurdere et brettspilldatasett, og bygge en Node.js- og TypeScript-backend med GraphQL og egen database på gruppens virtuelle maskin. Da skal søk, filtre, sortering og sidevisning gjelde hele katalogen, og klienten hente én resultatside om gangen. Backend skal også lagre «Kveldens utvalg». Vi vil vurdere bilder og vanskelighetsgrad når datakilde og rettigheter er avklart. «Hjelp meg å velge» kan bli en senere utvidelse. Kart, vurderinger og kommentarer er ikke del av denne prototypen. +Etter SSH-innlogging på VM kan drift kontrolleres med: + +```bash +systemctl status apache2 postgresql spillkveld-backend +curl --fail http://127.0.0.1:3001/health +sudo journalctl -u spillkveld-backend -n 50 --no-pager +sudo systemctl restart spillkveld-backend +``` + +`/health` gir HTTP 200 med `{"status":"ok","database":"ok"}` når databasen er tilgjengelig, ellers HTTP 503. Ved Apache-endringer kontrolleres konfigurasjonen med `sudo apache2ctl configtest` før reload. + +## Begrensninger og videre arbeid + +Katalogen har bare de 12 eksempelspillene, og detaljvisningen viser foreløpig omtrent samme informasjon som kortet. «Kveldens utvalg» synkroniseres ikke mellom nettlesere eller enheter. Én ende-til-ende-test dekker søk og detaljer; den dekker ikke alle funksjoner og skjermstørrelser. VM-en bruker HTTP og krever NTNU-nett/VPN. + +Et større datasett, bilder og «Hjelp meg å velge» kan vurderes videre. Kart, vurderinger og kommentarer er ikke implementert. ## Arbeid i repoet -Tekstfiler skal bruke LF-linjeskift på alle operativsystemer. `.gitattributes` styrer dette i Git, og `.editorconfig` gir samme regel til editorer og Prettier. Bruk støtte for EditorConfig i editoren din. +Tekstfiler skal bruke LF-linjeskift, styrt av `.gitattributes` og `.editorconfig`. Arbeid knyttes til en issue og gjøres på egen branch, for eksempel `feature/7-vm-deployment`. Issue- og PR-tekster og commitmeldinger skrives på engelsk; commitmeldinger viser til relevant issue. + +`main` er beskyttet. Endringer leveres som pull request og må godkjennes av et annet medlem av `IT2810-H26-T32`; godkjenning fra ansatte teller ikke. Svar på og løs alle review-kommentarer. Nye commits fjerner tidligere godkjenning, så be om ny gjennomgang etter endringer. + +Prosjektet bygger på [prosjektbeskrivelsen](https://docs.google.com/document/d/18swVXIU3Pm7DdFzoIUmIShoD_kerodH1R3CQX6Zy5qw/edit). + +## KI-erklæringer + +### Kamilla – database + +ChatGPT ble brukt til å forklare PostgreSQL-oppsett, bistå med utkast til SQL-skjema og seed-fil og gi testinstruksjoner. SQL-filene ble kjørt, og resultatene ble kontrollert lokalt. + +### Marius – backend + +ChatGPT ble brukt til stegvis veiledning og kodeforslag for backend-konfigurasjon, databasetilgang, GraphQL-operasjoner, API-tester og dokumentasjon. + +Verifiseringen av implementasjonen er beskrevet under tester. Ved backendleveransen var de gjenstående integrasjonskontrollene ennå ikke gjennomført. + +### Andreas – frontend + +ChatGPT ble brukt som støtte under frontend-utviklingen, hovedsakelig til å diskutere løsningsforslag, feilsøke, gjennomgå kode og foreslå forbedringer. + +KI ble også brukt til hjelp med React, TypeScript, Apollo Client, GraphQL-integrasjon, automatiske tester og dokumentasjon. Forslagene ble gjennomgått, tilpasset ved behov og testet for å kontrollere at de fungerte som tiltenkt. Alle endringer ble kontrollert med automatiske tester, ESLint og produksjonsbygg. -`main` er beskyttet. Arbeid på en egen branch og åpne en pull request når arbeidet skal gjennomgås. Før den kan merges, må et medlem av `IT2810-H26-T32` godkjenne den; du kan ikke godkjenne din egen pull request, og godkjenning fra ansatte teller ikke. Svar på og løs alle review-kommentarer. Nye commits etter en godkjenning fjerner godkjenningen, så be om ny gjennomgang hvis du gjør endringer. +### Bjørn – prototype, drift og integrasjon -Denne prototypen følger [prosjektbeskrivelsen](https://docs.google.com/document/d/18swVXIU3Pm7DdFzoIUmIShoD_kerodH1R3CQX6Zy5qw/edit) og [issue #1](https://git.ntnu.no/IT2810-H26/T32-Project-2/issues/1). +Codex ble brukt til hjelp med prototypen, kodegjennomgang, VM-oppsett, publisering, Playwright-test og samlet dokumentasjon. Resultatet ble kontrollert med tester, lint, produksjonsbygg, HTTP-/GraphQL-kall og manuell utprøving. Automatisk oppstart og ende-til-ende-test ble også kontrollert etter VM-omstart. diff --git a/deploy/install-runtime.sh b/deploy/install-runtime.sh new file mode 100644 index 0000000..2bd15cc --- /dev/null +++ b/deploy/install-runtime.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $EUID -ne 0 ]]; then + echo 'Run with sudo: sudo bash deploy/install-runtime.sh' >&2 + exit 1 +fi + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +[[ -s /etc/spillkveld/backend.env ]] || { echo 'Run setup-database.sh first.' >&2; exit 1; } +[[ $(curl -s -o /dev/null -w '%{http_code}' http://localhost/project1/) == 200 ]] + +if systemctl is-active --quiet spillkveld-backend.service; then + echo 'The real backend is running; refusing to replace it with the test server.' >&2 + exit 1 +fi +if ss -H -ltn 'sport = :3001' | grep -q .; then + systemctl is-active --quiet spillkveld-setup-test.service || { + echo 'Port 3001 is occupied by another process.' >&2 + exit 1 + } +fi + +if id spillkveld >/dev/null 2>&1; then + [[ $(getent passwd spillkveld | cut -d: -f7) == /usr/sbin/nologin ]] || { + echo 'An unexpected spillkveld system account already exists.' >&2 + exit 1 + } +else + useradd --system --user-group --home-dir /opt/spillkveld --no-create-home --shell /usr/sbin/nologin spillkveld +fi + +install -d -m 755 /opt/spillkveld/setup-test /var/www/html/project2 +install -m 644 "$script_dir/setup-test/server.mjs" /opt/spillkveld/setup-test/server.mjs +if [[ ! -e /var/www/html/project2/index.html ]]; then + install -m 644 "$script_dir/setup-test/index.html" /var/www/html/project2/index.html +fi +install -m 644 "$script_dir/spillkveld-backend.service" /etc/systemd/system/spillkveld-backend.service +install -m 644 "$script_dir/spillkveld-setup-test.service" /etc/systemd/system/spillkveld-setup-test.service +systemd-analyze verify /etc/systemd/system/spillkveld-backend.service /etc/systemd/system/spillkveld-setup-test.service + +configuration=/etc/apache2/conf-available/spillkveld.conf +if [[ -e $configuration ]] && ! cmp -s "$configuration" "$script_dir/spillkveld.conf"; then + echo 'Existing Apache configuration differs; inspect it before replacing it.' >&2 + exit 1 +fi +was_enabled=false +[[ ! -e /etc/apache2/conf-enabled/spillkveld.conf ]] || was_enabled=true +install -m 644 "$script_dir/spillkveld.conf" "$configuration" +a2enmod proxy proxy_http +a2enconf spillkveld +if ! apache2ctl configtest; then + if [[ $was_enabled == false ]]; then a2disconf spillkveld; fi + echo 'Apache configuration failed validation; Apache has not been reloaded.' >&2 + exit 1 +fi + +systemctl daemon-reload +systemctl enable postgresql +systemctl enable --now spillkveld-setup-test.service +curl --fail --silent --retry 5 --retry-connrefused --retry-delay 1 http://127.0.0.1:3001/health +systemctl reload apache2 + +[[ $(curl -s -o /dev/null -w '%{http_code}' http://localhost/project1/) == 200 ]] +[[ $(curl -s -o /dev/null -w '%{http_code}' http://localhost/project2/) == 200 ]] +[[ $(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost/project2/graphql) == 503 ]] +curl --silent -X POST http://localhost/project2/graphql | + node -e 'let body=""; process.stdin.on("data", c => body += c); process.stdin.on("end", () => { if (JSON.parse(body).setupOnly !== true) process.exit(1); });' +echo +echo 'Runtime setup verified. The temporary server is running; the real backend is not deployed.' diff --git a/deploy/publish-first-release.sh b/deploy/publish-first-release.sh new file mode 100644 index 0000000..070b659 --- /dev/null +++ b/deploy/publish-first-release.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# First publication only. Build and test the staged release before running this. +set -euo pipefail +umask 077 + +[[ $EUID -eq 0 ]] || { echo 'Run this script with sudo.' >&2; exit 1; } +release=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +environment_file=/etc/spillkveld/backend.env +[[ -f $environment_file && ! -L $environment_file ]] +[[ $(stat -c %u "$environment_file") == 0 ]] +[[ ! -e /opt/spillkveld/server ]] +! systemctl is-active --quiet spillkveld-backend.service +systemctl is-active --quiet spillkveld-setup-test.service +[[ -s $release/server/dist/index.js && -s $release/dist/index.html ]] +[[ $(curl -s -o /dev/null -w '%{http_code}' http://localhost/project1/) == 200 ]] +apache2ctl configtest + +set -a +# This root-owned file was generated by setup-database.sh and is never printed. +source "$environment_file" +set +a +[[ $DATABASE_URL =~ ^postgresql://spillkveld_app:[a-f0-9]{64}@127\.0\.0\.1:5432/spillkveld$ ]] +export TEST_DATABASE_URL="${DATABASE_URL%/spillkveld}/spillkveld_test" + +test_owner=$(runuser -u postgres -- psql -X -At -d postgres -c "SELECT pg_get_userbyid(datdba) FROM pg_database WHERE datname = 'spillkveld_test'") +if [[ -z $test_owner ]]; then + runuser -u postgres -- createdb --owner=spillkveld_app spillkveld_test +elif [[ $test_owner != spillkveld_app ]]; then + echo 'Unexpected test database owner; stopping.' >&2 + exit 1 +fi + +export PGHOST=127.0.0.1 PGPORT=5432 PGUSER=spillkveld_app +export PGPASSWORD="${DATABASE_URL#postgresql://spillkveld_app:}" +PGPASSWORD="${PGPASSWORD%@127.0.0.1:5432/spillkveld}" + +# Validate the shared schema and seed in the separate test database first. +( + cd "$release/server" + DATABASE_URL="$TEST_DATABASE_URL" runuser -u bjoabe -- npm run db:setup + DATABASE_URL="$TEST_DATABASE_URL" runuser -u bjoabe -- npm run db:seed + runuser -u bjoabe -- npm test +) + +# Production SQL runs with the application role, never the PostgreSQL admin role. +( + cd "$release/server" + runuser -u bjoabe -- npm run db:setup + runuser -u bjoabe -- npm run db:seed +) +count=$(PGDATABASE=spillkveld runuser -u bjoabe -- psql -X -At -v ON_ERROR_STOP=1 -c 'SELECT COUNT(*) FROM games') +[[ $count == 12 ]] || { echo 'Expected exactly 12 example games; stopping.' >&2; exit 1; } + +backup="/var/www/html/project2.before-first-release-$(date +%Y%m%d%H%M%S)" +[[ ! -e $backup ]] +cp -a -- "$release/server" /opt/spillkveld/server +chown -R root:root /opt/spillkveld/server +chmod -R a+rX /opt/spillkveld/server + +frontend_swapped=false +rollback() { + echo 'Publication failed; restoring the temporary test service.' >&2 + systemctl disable --now spillkveld-backend.service || true + systemctl enable --now spillkveld-setup-test.service || true + if [[ $frontend_swapped == true ]]; then + mv -- /var/www/html/project2 "${backup}.failed" + mv -- "$backup" /var/www/html/project2 + fi +} +trap rollback ERR +systemctl disable --now spillkveld-setup-test.service +systemctl enable --now spillkveld-backend.service +curl --fail --silent --retry 10 --retry-connrefused --retry-delay 1 http://127.0.0.1:3001/health + +mv -- /var/www/html/project2 "$backup" +frontend_swapped=true +cp -a -- "$release/dist" /var/www/html/project2 +chown -R root:root /var/www/html/project2 +chmod -R a+rX /var/www/html/project2 +curl --fail --silent http://localhost/project2/ >/dev/null +curl --fail --silent -H 'Content-Type: application/json' \ + --data '{"query":"{ games(input: {query: \"Azul\"}) {totalCount items {id name}} }"}' \ + http://localhost/project2/graphql | + node -e 'let body=""; process.stdin.on("data", c => body += c); process.stdin.on("end", () => { const result=JSON.parse(body); if (result.errors || !result.data?.games?.items?.some(g => g.id === "azul")) process.exit(1); console.log("Verified Azul through Apache and GraphQL."); });' +[[ $(curl -s -o /dev/null -w '%{http_code}' http://localhost/project1/) == 200 ]] +trap - ERR +echo +echo 'First release published. Database tests passed and project1 still responds.' +echo 'Open http://it2810-32.idi.ntnu.no/project2/ to try the app.' diff --git a/deploy/setup-database.sh b/deploy/setup-database.sh new file mode 100644 index 0000000..b6d5f9a --- /dev/null +++ b/deploy/setup-database.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 + +if [[ $EUID -ne 0 ]]; then + echo 'Run with sudo: sudo bash setup-database.sh' >&2 + exit 1 +fi + +for command in psql createdb runuser node openssl; do + command -v "$command" >/dev/null +done + +admin_sql() { + runuser -u postgres -- psql -X -v ON_ERROR_STOP=1 -At -d postgres -c "$1" +} + +environment_file=/etc/spillkveld/backend.env +role_exists=$(admin_sql "SELECT EXISTS (SELECT FROM pg_roles WHERE rolname = 'spillkveld_app')") +database_exists=$(admin_sql "SELECT EXISTS (SELECT FROM pg_database WHERE datname = 'spillkveld')") + +if [[ -e $environment_file ]]; then + if [[ -L $environment_file || ! -f $environment_file || $(stat -c %u "$environment_file") != 0 ]]; then + echo 'Expected a regular root-owned backend.env; refusing to change it.' >&2 + exit 1 + fi + database_url=$(sed -n 's/^DATABASE_URL=//p' "$environment_file") + if [[ $database_url =~ ^postgresql://spillkveld_app:([a-f0-9]{64})@127\.0\.0\.1:5432/spillkveld$ ]]; then + password=${BASH_REMATCH[1]} + else + echo 'Existing DATABASE_URL does not match this setup; refusing to overwrite it.' >&2 + exit 1 + fi +else + if [[ $role_exists == t || $database_exists == t ]]; then + echo 'Database or role already exists without backend.env; inspect it before continuing.' >&2 + exit 1 + fi + password=$(openssl rand -hex 32) +fi + +if [[ $role_exists == t ]]; then + flags=$(admin_sql "SELECT rolcanlogin AND NOT rolsuper AND NOT rolcreatedb AND NOT rolcreaterole AND NOT rolreplication AND NOT rolbypassrls AND NOT EXISTS (SELECT FROM pg_auth_members WHERE member = pg_roles.oid) FROM pg_roles WHERE rolname = 'spillkveld_app'") + [[ $flags == t ]] || { echo 'Existing role has unexpected privileges.' >&2; exit 1; } +fi +if [[ $database_exists == t ]]; then + owner=$(admin_sql "SELECT pg_get_userbyid(datdba) FROM pg_database WHERE datname = 'spillkveld'") + [[ $owner == spillkveld_app ]] || { echo 'Existing database has an unexpected owner.' >&2; exit 1; } +fi + +# Save credentials before creating resources, so a failed run can be resumed. +install -d -m 750 -o root -g root /etc/spillkveld +if [[ ! -e $environment_file ]]; then + temporary_file=$(mktemp /etc/spillkveld/backend.env.XXXXXX) + trap 'rm -f -- "${temporary_file:-}"' EXIT + printf 'DATABASE_URL=postgresql://spillkveld_app:%s@127.0.0.1:5432/spillkveld\nPORT=3001\nHOST=127.0.0.1\n' "$password" > "$temporary_file" + mv -- "$temporary_file" "$environment_file" + trap - EXIT +fi +chmod 600 "$environment_file" + +if [[ $role_exists == f ]]; then + # Send a SCRAM verifier to PostgreSQL, not the plain-text password. + verifier=$(printf '%s' "$password" | node --input-type=module -e ' + import { readFileSync } from "node:fs"; + import { randomBytes, pbkdf2Sync, createHmac, createHash } from "node:crypto"; + const password = readFileSync(0, "utf8"); + const salt = randomBytes(16); + const salted = pbkdf2Sync(password, salt, 4096, 32, "sha256"); + const client = createHmac("sha256", salted).update("Client Key").digest(); + const stored = createHash("sha256").update(client).digest("base64"); + const server = createHmac("sha256", salted).update("Server Key").digest("base64"); + process.stdout.write(`SCRAM-SHA-256$4096:${salt.toString("base64")}$${stored}:${server}`); + ') + printf "CREATE ROLE spillkveld_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD '%s';\n" "$verifier" | + runuser -u postgres -- psql -X -v ON_ERROR_STOP=1 -d postgres +fi + +if [[ $database_exists == f ]]; then + runuser -u postgres -- createdb --owner=spillkveld_app spillkveld +fi + +PGPASSWORD="$password" psql -X -v ON_ERROR_STOP=1 --host=127.0.0.1 --username=spillkveld_app --dbname=spillkveld \ + -c 'SELECT current_user, current_database();' +echo 'Database ready. Credentials are in /etc/spillkveld/backend.env (root-only).' diff --git a/deploy/setup-test/index.html b/deploy/setup-test/index.html new file mode 100644 index 0000000..1672505 --- /dev/null +++ b/deploy/setup-test/index.html @@ -0,0 +1,15 @@ + + +
+ + +Dette er en midlertidig testside for publisering på VM.
+Den ferdige appen og GraphQL-backenden er ikke publisert ennå.
+