Compare commits
No commits in common. "ui-before-merged-beheer-v0.4.08" and "main" have entirely different histories.
ui-before-
...
main
|
|
@ -13,8 +13,3 @@ POSTGRES_PASSWORD=change-me-locally
|
|||
# Connectiestring die de app gebruikt.
|
||||
# Hostnaam 'db' verwijst naar de postgres-service binnen het compose-netwerk.
|
||||
DATABASE_URL=postgres://teach:change-me-locally@db:5432/teach
|
||||
|
||||
# Eerste overkoepelende beheerder (wordt eenmalig aangemaakt als er nog geen bestaat).
|
||||
# Laat SUPER_PASS leeg om een willekeurig wachtwoord in de serverlog te krijgen.
|
||||
SUPER_USER=beheerder
|
||||
SUPER_PASS=
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: dev # jouw dev-runner (test-server)
|
||||
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
|
||||
env:
|
||||
# 10.0.20.22:3000 - host van je Forgejo (= container registry host)
|
||||
REGISTRY: ${{ vars.REGISTRY }}
|
||||
|
|
@ -19,23 +19,13 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dependencies en securitytests
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "$PWD:/app" \
|
||||
-v /app/node_modules \
|
||||
-w /app node:22-alpine \
|
||||
sh -lc 'npm ci && npm test && npm audit --omit=dev --audit-level=moderate'
|
||||
|
||||
- name: Image-tags bepalen
|
||||
id: meta
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||
echo "sha_tag=${IMAGE}:dev-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||
echo "dev_tag=${IMAGE}:dev" >> "$GITHUB_OUTPUT"
|
||||
echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Platte docker build/push i.p.v. buildx: betrouwbaarder tegen een
|
||||
# HTTP-registry. De runner-host moet 10.0.20.22:3000 als
|
||||
|
|
@ -44,25 +34,24 @@ jobs:
|
|||
run: |
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
docker build \
|
||||
--build-arg APP_VERSION="${{ steps.meta.outputs.app_version }}" \
|
||||
--build-arg GIT_SHA="${GITHUB_SHA::7}" \
|
||||
--build-arg APP_VERSION="dev-${GITHUB_SHA::7}" \
|
||||
-t "${{ steps.meta.outputs.sha_tag }}" \
|
||||
-t "${{ steps.meta.outputs.dev_tag }}" .
|
||||
docker push "${{ steps.meta.outputs.sha_tag }}"
|
||||
docker push "${{ steps.meta.outputs.dev_tag }}"
|
||||
|
||||
# Deploy via gewone ssh (geen externe action nodig).
|
||||
- name: Deploy naar test-VM
|
||||
run: |
|
||||
echo "${{ secrets.TEST_SSH_KEY }}" > deploy_key
|
||||
chmod 600 deploy_key
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
|
||||
-i deploy_key deploy/compose.deploy.yaml \
|
||||
${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }}:${{ secrets.TEST_DEPLOY_PATH }}/compose.deploy.yaml
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
|
||||
-i deploy_key deploy/nginx.conf \
|
||||
${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }}:${{ secrets.TEST_DEPLOY_PATH }}/nginx.conf.new
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
|
||||
-i deploy_key ${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }} \
|
||||
"set -e && cd ${{ secrets.TEST_DEPLOY_PATH }} && mv -f nginx.conf.new nginx.conf && echo '${{ secrets.REGISTRY_TOKEN }}' | docker login ${{ vars.REGISTRY }} -u '${{ secrets.REGISTRY_USER }}' --password-stdin && sed -i 's|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|' .env && docker compose -f compose.deploy.yaml pull app && docker compose -f compose.deploy.yaml up -d --force-recreate app web && docker image prune -f"
|
||||
rm -f deploy_key
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.TEST_HOST }}
|
||||
username: ${{ secrets.TEST_USER }}
|
||||
key: ${{ secrets.TEST_SSH_KEY }}
|
||||
script: |
|
||||
set -e
|
||||
cd ${{ secrets.TEST_DEPLOY_PATH }}
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
# Zet de te draaien image-tag in de env van de VM
|
||||
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|" .env
|
||||
docker compose -f compose.deploy.yaml pull
|
||||
docker compose -f compose.deploy.yaml up -d
|
||||
docker image prune -f
|
||||
|
|
|
|||
|
|
@ -12,21 +12,13 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: prod # jouw prod-runner (productieserver)
|
||||
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
|
||||
env:
|
||||
REGISTRY: ${{ vars.REGISTRY }}
|
||||
IMAGE_NAME: ${{ github.repository }} # bes-r/teach
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dependencies en securitytests
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "$PWD:/app" \
|
||||
-v /app/node_modules \
|
||||
-w /app node:22-alpine \
|
||||
sh -lc 'npm ci && npm test && npm audit --omit=dev --audit-level=moderate'
|
||||
|
||||
- name: Image-tags bepalen
|
||||
id: meta
|
||||
run: |
|
||||
|
|
@ -43,24 +35,22 @@ jobs:
|
|||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
docker build \
|
||||
--build-arg APP_VERSION="${{ steps.meta.outputs.version }}" \
|
||||
--build-arg GIT_SHA="${GITHUB_SHA::7}" \
|
||||
-t "${{ steps.meta.outputs.version_tag }}" \
|
||||
-t "${{ steps.meta.outputs.latest_tag }}" .
|
||||
docker push "${{ steps.meta.outputs.version_tag }}"
|
||||
docker push "${{ steps.meta.outputs.latest_tag }}"
|
||||
|
||||
# Deploy via gewone ssh (geen externe action nodig).
|
||||
- name: Deploy naar productie-VM
|
||||
run: |
|
||||
echo "${{ secrets.PROD_SSH_KEY }}" > deploy_key
|
||||
chmod 600 deploy_key
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
|
||||
-i deploy_key deploy/compose.deploy.yaml \
|
||||
${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:${{ secrets.PROD_DEPLOY_PATH }}/compose.deploy.yaml
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
|
||||
-i deploy_key deploy/nginx.conf \
|
||||
${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:${{ secrets.PROD_DEPLOY_PATH }}/nginx.conf.new
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
|
||||
-i deploy_key ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} \
|
||||
"set -e && cd ${{ secrets.PROD_DEPLOY_PATH }} && mv -f nginx.conf.new nginx.conf && echo '${{ secrets.REGISTRY_TOKEN }}' | docker login ${{ vars.REGISTRY }} -u '${{ secrets.REGISTRY_USER }}' --password-stdin && sed -i 's|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|' .env && docker compose -f compose.deploy.yaml pull app && docker compose -f compose.deploy.yaml up -d --force-recreate app web && docker image prune -f"
|
||||
rm -f deploy_key
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.PROD_HOST }}
|
||||
username: ${{ secrets.PROD_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
script: |
|
||||
set -e
|
||||
cd ${{ secrets.PROD_DEPLOY_PATH }}
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|" .env
|
||||
docker compose -f compose.deploy.yaml pull
|
||||
docker compose -f compose.deploy.yaml up -d
|
||||
docker image prune -f
|
||||
|
|
|
|||
1
.gitignore
vendored
|
|
@ -8,7 +8,6 @@ npm-debug.log*
|
|||
|
||||
# Postgres data volume bij lokaal draaien
|
||||
pgdata/
|
||||
storage/
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
|
|
|
|||
11
AGENTS.md
|
|
@ -1,11 +0,0 @@
|
|||
# Projectafspraken
|
||||
- Werk nooit rechtstreeks op main
|
||||
- Push naar de dev branch
|
||||
- Werk met versienummering (v0.0.00)
|
||||
- In de dev-branch altijd beta weergeven bij de versienummering
|
||||
- Beschrijf gewijzigde functionaliteiten puntsgewijs in de pull request.
|
||||
- Gebruik geen sudo
|
||||
- Commit geen wachtwoorden, tokens, `.env`-bestanden of privésleutels.
|
||||
- Voer relevante tests uit voor iedere commit.
|
||||
- Push wijzigingen naar Forgejo, maar merge niet zelfstandig.
|
||||
- Bij opdracht "Release versie" maak een versie releaseversie
|
||||
11
CLAUDE.md
|
|
@ -1,11 +0,0 @@
|
|||
# Projectafspraken
|
||||
- Werk nooit rechtstreeks op main
|
||||
- Push naar de dev branch
|
||||
- Werk met versienummering (v0.0.00)
|
||||
- In de dev-branch altijd beta weergeven bij de versienummering
|
||||
- Beschrijf gewijzigde functionaliteiten puntsgewijs in de pull request.
|
||||
- Gebruik geen sudo
|
||||
- Commit geen wachtwoorden, tokens, `.env`-bestanden of privésleutels.
|
||||
- Voer relevante tests uit voor iedere commit.
|
||||
- Push wijzigingen naar Forgejo, maar merge niet zelfstandig.
|
||||
- Bij opdracht "Release versie" maak een versie releaseversie
|
||||
|
|
@ -13,19 +13,16 @@ ENV NODE_ENV=production
|
|||
WORKDIR /app
|
||||
|
||||
# Draai als non-root gebruiker
|
||||
RUN addgroup -S app && adduser -S app -G app && mkdir -p /app/storage/images && chown -R app:app /app/storage
|
||||
RUN addgroup -S app && adduser -S app -G app
|
||||
|
||||
COPY --chown=app:app --from=deps /app/node_modules ./node_modules
|
||||
COPY --chown=app:app package.json ./
|
||||
COPY --chown=app:app src ./src
|
||||
COPY --chown=app:app public ./public
|
||||
COPY --chown=app:app db ./db
|
||||
|
||||
# Versie + commit-hash worden tijdens de build meegegeven door CI
|
||||
# Versie wordt tijdens de build meegegeven door CI
|
||||
ARG APP_VERSION=dev
|
||||
ENV APP_VERSION=$APP_VERSION
|
||||
ARG GIT_SHA=
|
||||
ENV GIT_SHA=$GIT_SHA
|
||||
|
||||
USER app
|
||||
EXPOSE 3000
|
||||
|
|
|
|||
36
README.md
|
|
@ -35,9 +35,8 @@ App op http://localhost:3000, healthcheck op `/healthz`, DB-check op `/readyz`.
|
|||
serveert de HTML én biedt de `/api`, en praat met de DB.
|
||||
- **Forgejo container registry**: CI bouwt de image één keer en pusht die; beide
|
||||
VM's pullen exact dezelfde geteste image. Geen build op de productie-VM.
|
||||
- **Pangolin/Traefik verzorgt de publieke HTTPS-ingang**. De meegeleverde nginx
|
||||
vormt de interne proxylaag en luistert standaard op poort `8081`, zodat de
|
||||
Pangolin/Traefik-route deze via het VM-/containernetwerk kan bereiken. De Fastify-app vertrouwt precies twee proxy-hops.
|
||||
- **Nginx blijft ervoor** als reverse proxy / TLS op elke VM; de app luistert
|
||||
alleen op `127.0.0.1:<APP_PORT>`.
|
||||
|
||||
## Eenmalige setup
|
||||
|
||||
|
|
@ -108,29 +107,28 @@ POSTGRES_USER=teach
|
|||
POSTGRES_PASSWORD=<sterk-wachtwoord>
|
||||
DATABASE_URL=postgres://teach:<sterk-wachtwoord>@db:5432/teach
|
||||
APP_PORT=3000
|
||||
WEB_BIND_IP=0.0.0.0
|
||||
WEB_PORT=8081
|
||||
TRUST_PROXY_HOPS=2
|
||||
SUPER_USER=beheerder
|
||||
SUPER_PASS=<uniek-sterk-eerste-wachtwoord>
|
||||
EOF
|
||||
```
|
||||
|
||||
CI werkt bij elke deploy de `IMAGE=`-regel bij, pullt en herstart.
|
||||
|
||||
### 4. Pangolin/Traefik publiceren
|
||||
### 4. Nginx reverse proxy (per VM)
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name digibord.familiebesselink.nl; # test-VM eigen subdomein
|
||||
|
||||
Publiceer `http://<interne-vm-ip>:8081` via Pangolin/Traefik en laat daar TLS
|
||||
beëindigen. De interne nginx behoudt `X-Forwarded-Proto: https`, waarna de app
|
||||
Secure/HttpOnly/SameSite-cookies, HSTS en overige securityheaders gebruikt.
|
||||
# ssl_certificate ... (bestaande config)
|
||||
|
||||
- Beperk poort 8081 met de hostfirewall tot het Pangolin/Traefik- of tunnelnetwerk.
|
||||
- Pas `TRUST_PROXY_HOPS` alleen aan wanneer de proxyketen werkelijk verandert.
|
||||
- Bij een aparte Traefik-container kan een gedeeld intern Docker-netwerk nodig
|
||||
zijn; zet `WEB_BIND_IP` niet ruimer dan noodzakelijk.
|
||||
- `SUPER_PASS` is alleen nodig bij een lege database, wordt nooit gelogd en
|
||||
moet via de beveiligde deploy-omgeving worden aangeleverd.
|
||||
- De overgang naar v0.3.03-beta trekt bestaande browsersessies bewust in.
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow / branching
|
||||
|
||||
|
|
|
|||
1
VERSION
|
|
@ -1 +0,0 @@
|
|||
0.4.08-beta
|
||||
|
|
@ -16,14 +16,8 @@ services:
|
|||
PORT: 3000
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
APP_VERSION: dev
|
||||
SUPER_USER: ${SUPER_USER:-beheerder}
|
||||
SUPER_PASS: ${SUPER_PASS:-}
|
||||
TRUST_PROXY_HOPS: 0
|
||||
COOKIE_SECURE: "false"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- image_data:/app/storage/images
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
|
|
@ -45,4 +39,3 @@ services:
|
|||
|
||||
volumes:
|
||||
pgdata:
|
||||
image_data:
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
-- v0.2.00: scholen, klassen, rollen en sessies.
|
||||
-- Rollen: super (overkoepelend beheerder), admin (schoolbeheerder),
|
||||
-- teacher (groepsleiding), pupil (leerling).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schools (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS classes (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
|
||||
ADD COLUMN IF NOT EXISTS display_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS password_hash TEXT,
|
||||
-- alleen voor leerlingen: wachtwoord dat groepsleiding/beheer mag inzien
|
||||
ADD COLUMN IF NOT EXISTS password_plain TEXT,
|
||||
-- eenmalige koppelcode waarmee nieuwe gebruikers hun account activeren
|
||||
ADD COLUMN IF NOT EXISTS link_code TEXT UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS class_id BIGINT REFERENCES classes(id) ON DELETE SET NULL,
|
||||
-- alle app-gegevens van de gebruiker (borden, thema's, eigen woorden)
|
||||
ADD COLUMN IF NOT EXISTS data JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
-- gebruikersnaam uniek binnen een school; supers (school_id IS NULL) landelijk uniek
|
||||
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_username_key;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_school_username
|
||||
ON users (school_id, lower(username)) WHERE school_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_super_username
|
||||
ON users (lower(username)) WHERE school_id IS NULL;
|
||||
|
||||
-- welke groepsleiding hoort bij welke klas
|
||||
CREATE TABLE IF NOT EXISTS class_teachers (
|
||||
class_id BIGINT NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (class_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_school ON users(school_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_class ON users(class_id);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
-- v0.3.00: bijhouden of een account al gebruikt is, zodat leerling-
|
||||
-- wachtwoorden na de eerste keer inloggen niet langer inzichtelijk zijn.
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-- v0.2.21: extra functies naast de hoofdrol (users.role) - alleen voor stafaccounts.
|
||||
-- Leerlingen krijgen nooit een extra rol; dat wordt in de applicatielaag afgedwongen
|
||||
-- (net als de bestaande regel dat alleen super rollen mag wijzigen).
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, role)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user ON user_roles(user_id);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
-- Toewijzingen: een leerkracht koppelt één van haar eigen borden (via het
|
||||
-- stabiele board-id uit users.data) aan een klas of een individuele leerling.
|
||||
-- Puur additief, leeg bij aanmaak. Precies één van class_id/pupil_id gezet:
|
||||
-- een klas-toewijzing (standaard voor iedereen in de klas) of een leerling-
|
||||
-- toewijzing (uitzondering, voor een leerling op een ander niveau).
|
||||
CREATE TABLE IF NOT EXISTS assignments (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
class_id BIGINT REFERENCES classes(id) ON DELETE CASCADE,
|
||||
pupil_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
board_id TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (num_nonnulls(class_id, pupil_id) = 1)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id) WHERE class_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_pupil ON assignments(pupil_id) WHERE pupil_id IS NOT NULL;
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
-- v0.3.03: tenant-isolatie en security-invarianten ook in PostgreSQL afdwingen.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
||||
|
||||
-- Leerlingwachtwoorden worden voortaan alleen eenmalig in de API-respons getoond.
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS password_plain;
|
||||
|
||||
-- Ruim eventuele historische tenant-kruisingen veilig op vóór de triggers actief worden.
|
||||
UPDATE users u SET class_id = NULL
|
||||
WHERE class_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM classes c WHERE c.id = u.class_id AND c.school_id = u.school_id
|
||||
);
|
||||
|
||||
DELETE FROM class_teachers ct
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM classes c
|
||||
JOIN users u ON u.id = ct.user_id AND u.school_id = c.school_id
|
||||
WHERE c.id = ct.class_id
|
||||
AND (u.role = 'teacher' OR EXISTS (
|
||||
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
|
||||
))
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION teach_validate_user_class_school()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.class_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM classes c WHERE c.id = NEW.class_id AND c.school_id = NEW.school_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'leerling en klas moeten bij dezelfde school horen';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_users_class_school ON users;
|
||||
CREATE TRIGGER trg_users_class_school
|
||||
BEFORE INSERT OR UPDATE OF class_id, school_id ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION teach_validate_user_class_school();
|
||||
|
||||
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM classes c
|
||||
JOIN users u ON u.id = NEW.user_id AND u.school_id = c.school_id
|
||||
WHERE c.id = NEW.class_id
|
||||
AND (u.role = 'teacher' OR EXISTS (
|
||||
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
|
||||
))
|
||||
) THEN
|
||||
RAISE EXCEPTION 'groepsleiding en klas moeten bij dezelfde school horen';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
|
||||
CREATE TRIGGER trg_class_teachers_tenant
|
||||
BEFORE INSERT OR UPDATE ON class_teachers
|
||||
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
-- Voortgang: leerlingen loggen elk afgerond "moment" in een widget (ronde
|
||||
-- sommen, voltooid woord, geknapte ballon) zodat leerkrachten een tijdlijn
|
||||
-- per leerling kunnen bekijken. teacher_id/board_id komen server-side uit de
|
||||
-- bestaande assignments-tabel, nooit van de client, om te voorkomen dat een
|
||||
-- leerling voortgang onder een andere leerkracht kan wegschrijven.
|
||||
CREATE TABLE IF NOT EXISTS progress_events (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
board_id TEXT NOT NULL,
|
||||
widget_id TEXT NOT NULL,
|
||||
widget_type TEXT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
correct INT NOT NULL DEFAULT 0,
|
||||
stars INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_progress_events_pupil ON progress_events(pupil_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_progress_events_teacher ON progress_events(teacher_id, created_at DESC);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
-- Widget-delen: een toewijzing kan naast een heel bord (widget_id NULL,
|
||||
-- bestaand gedrag) ook één enkele widget van dat bord delen, met een modus:
|
||||
-- 'kijken' (leerling ziet de widget read-only, snellere poll) of 'werken'
|
||||
-- (leerling werkt erin; voortgang loopt via progress_events). widget_id
|
||||
-- verwijst naar het stabiele wid in de borddata van de leerkracht.
|
||||
ALTER TABLE assignments ADD COLUMN IF NOT EXISTS widget_id TEXT;
|
||||
ALTER TABLE assignments ADD COLUMN IF NOT EXISTS mode TEXT NOT NULL DEFAULT 'werken';
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
-- Opslaansysteem: optimistische concurrency + versiegeschiedenis.
|
||||
-- data_rev telt elke wijziging van users.data; een client stuurt de rev mee
|
||||
-- die hij geladen heeft en de update slaagt alleen als die nog actueel is
|
||||
-- (anders 409: een ander tabblad/digibord schreef eerder). user_data_versions
|
||||
-- bewaart periodieke snapshots van de vorige data zodat een leerkracht een
|
||||
-- eerdere versie kan terugzetten.
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS data_rev BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS data_updated_at TIMESTAMPTZ;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_data_versions (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
data JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_data_versions ON user_data_versions(user_id, created_at DESC);
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
-- Gedeelde bibliotheek: borden en ankerdiagrammen die de systeemmanager
|
||||
-- (school_id NULL = systeembreed, voor iedereen zichtbaar) of stafleden van
|
||||
-- een school (school_id gezet = alleen binnen die school zichtbaar) hebben
|
||||
-- klaargezet. Andere gebruikers kopiëren een item naar hun eigen collectie;
|
||||
-- er wordt niet live in gedeelde items gewerkt. folder is een pad met
|
||||
-- "/"-scheiding, dezelfde conventie als de mappenverkenner.
|
||||
CREATE TABLE IF NOT EXISTS shared_library (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
|
||||
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL,
|
||||
folder TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_shared_library ON shared_library(kind, school_id);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
-- Alleen-lezen-vlag voor gedeelde items: een vergrendeld school-item kan ook
|
||||
-- door de eigenaar niet meer gewijzigd worden, alleen nog door de
|
||||
-- schoolbeheerder of de systeemmanager (die de vlag ook zetten/weghalen).
|
||||
-- Systeembrede items zijn impliciet al alleen-lezen voor iedereen behalve de
|
||||
-- systeemmanager (beheerregel), de vlag is daar niet voor nodig.
|
||||
ALTER TABLE shared_library ADD COLUMN IF NOT EXISTS readonly BOOLEAN NOT NULL DEFAULT false;
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
-- v0.3.55-beta: doorzoekbare afbeeldingscatalogus met meervoudige thema's,
|
||||
-- map-paden, eigenaarschap en instelbare opslagquota.
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS image_quota_bytes BIGINT
|
||||
CHECK (image_quota_bytes IS NULL OR image_quota_bytes >= -1);
|
||||
ALTER TABLE schools
|
||||
ADD COLUMN IF NOT EXISTS image_quota_bytes BIGINT
|
||||
CHECK (image_quota_bytes IS NULL OR image_quota_bytes >= -1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_themes (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
key TEXT UNIQUE,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
|
||||
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
|
||||
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
folder TEXT NOT NULL DEFAULT '',
|
||||
name_nl TEXT NOT NULL,
|
||||
name_en TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (
|
||||
(scope = 'global' AND school_id IS NULL) OR
|
||||
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
|
||||
(scope = 'personal' AND owner_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_global_name
|
||||
ON image_themes (lower(name_nl)) WHERE scope = 'global';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_school_name
|
||||
ON image_themes (school_id, lower(name_nl)) WHERE scope = 'school';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_personal_name
|
||||
ON image_themes (owner_id, lower(name_nl)) WHERE scope = 'personal';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_folders (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
|
||||
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
|
||||
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (
|
||||
(scope = 'global' AND school_id IS NULL) OR
|
||||
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
|
||||
(scope = 'personal' AND owner_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_global_path
|
||||
ON image_folders (path) WHERE scope = 'global';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_school_path
|
||||
ON image_folders (school_id, path) WHERE scope = 'school';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_personal_path
|
||||
ON image_folders (owner_id, path) WHERE scope = 'personal';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_assets (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
|
||||
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
|
||||
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
folder TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
original_name TEXT,
|
||||
mime_type TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
|
||||
storage_key TEXT UNIQUE,
|
||||
public_path TEXT UNIQUE,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK ((storage_key IS NULL) <> (public_path IS NULL)),
|
||||
CHECK (
|
||||
(scope = 'global' AND school_id IS NULL) OR
|
||||
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
|
||||
(scope = 'personal' AND owner_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_owner ON image_assets(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_school ON image_assets(school_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_folder ON image_assets(scope, folder);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_asset_themes (
|
||||
asset_id BIGINT NOT NULL REFERENCES image_assets(id) ON DELETE CASCADE,
|
||||
theme_id BIGINT NOT NULL REFERENCES image_themes(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (asset_id, theme_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_image_asset_themes_theme ON image_asset_themes(theme_id);
|
||||
|
||||
INSERT INTO image_themes (key, scope, name_nl, name_en)
|
||||
VALUES
|
||||
('builtin-money', 'global', 'Geld', 'Money'),
|
||||
('builtin-coins', 'global', 'Munten', 'Coins'),
|
||||
('builtin-banknotes', 'global', 'Briefgeld', 'Banknotes')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
INSERT INTO image_folders (scope, path)
|
||||
VALUES ('global','Rekenen'), ('global','Rekenen/Geld'),
|
||||
('global','Rekenen/Geld/Munten'), ('global','Rekenen/Geld/Briefgeld')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO image_assets (key, scope, folder, name, mime_type, public_path, tags)
|
||||
VALUES
|
||||
('builtin-money-coin-1', 'global', 'Rekenen/Geld/Munten', '1 cent', 'image/png', '/img/money/coin-1.png', ARRAY['geld','money','munt','coin','cent']),
|
||||
('builtin-money-coin-2', 'global', 'Rekenen/Geld/Munten', '2 cent', 'image/png', '/img/money/coin-2.png', ARRAY['geld','money','munt','coin','cent']),
|
||||
('builtin-money-coin-5', 'global', 'Rekenen/Geld/Munten', '5 cent', 'image/png', '/img/money/coin-5.png', ARRAY['geld','money','munt','coin','cent']),
|
||||
('builtin-money-coin-10', 'global', 'Rekenen/Geld/Munten', '10 cent', 'image/png', '/img/money/coin-10.png', ARRAY['geld','money','munt','coin','cent']),
|
||||
('builtin-money-coin-20', 'global', 'Rekenen/Geld/Munten', '20 cent', 'image/png', '/img/money/coin-20.png', ARRAY['geld','money','munt','coin','cent']),
|
||||
('builtin-money-coin-50', 'global', 'Rekenen/Geld/Munten', '50 cent', 'image/png', '/img/money/coin-50.png', ARRAY['geld','money','munt','coin','cent']),
|
||||
('builtin-money-coin-100', 'global', 'Rekenen/Geld/Munten', '1 euro', 'image/png', '/img/money/coin-100.png', ARRAY['geld','money','munt','coin','euro']),
|
||||
('builtin-money-coin-200', 'global', 'Rekenen/Geld/Munten', '2 euro', 'image/png', '/img/money/coin-200.png', ARRAY['geld','money','munt','coin','euro']),
|
||||
('builtin-money-note-500', 'global', 'Rekenen/Geld/Briefgeld', '5 euro', 'image/jpeg', '/img/money/note-500.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
|
||||
('builtin-money-note-1000', 'global', 'Rekenen/Geld/Briefgeld', '10 euro', 'image/jpeg', '/img/money/note-1000.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
|
||||
('builtin-money-note-2000', 'global', 'Rekenen/Geld/Briefgeld', '20 euro', 'image/jpeg', '/img/money/note-2000.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
|
||||
('builtin-money-note-5000', 'global', 'Rekenen/Geld/Briefgeld', '50 euro', 'image/jpeg', '/img/money/note-5000.jpg', ARRAY['geld','money','briefgeld','banknote','euro'])
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
INSERT INTO image_asset_themes (asset_id, theme_id)
|
||||
SELECT a.id, t.id
|
||||
FROM image_assets a
|
||||
JOIN image_themes t ON t.key = 'builtin-money'
|
||||
WHERE a.key LIKE 'builtin-money-%'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO image_asset_themes (asset_id, theme_id)
|
||||
SELECT a.id, t.id
|
||||
FROM image_assets a
|
||||
JOIN image_themes t ON t.key = CASE
|
||||
WHEN a.key LIKE 'builtin-money-coin-%' THEN 'builtin-coins'
|
||||
ELSE 'builtin-banknotes'
|
||||
END
|
||||
WHERE a.key LIKE 'builtin-money-%'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
-- v0.3.57-beta: ouderportaal.
|
||||
-- Ouders (role 'parent', school_id NULL) koppelen aan kind-accounts (pupils):
|
||||
-- * parent_children: de koppeling zelf (een kind kan meerdere ouders hebben).
|
||||
-- * users.parent_code: eenmalige code waarmee staf een schoolleerling aan een
|
||||
-- ouder koppelt (code vervalt na gebruik).
|
||||
-- * school_link_requests: aanmeldverzoek van een thuiskind (pupil zonder
|
||||
-- school) bij een school; de school handelt het verzoek af (adopteren,
|
||||
-- koppelen aan een bestaande leerling of afwijzen).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS parent_children (
|
||||
parent_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (parent_id, pupil_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_parent_children_pupil ON parent_children(pupil_id);
|
||||
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS parent_code TEXT;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_parent_code
|
||||
ON users (parent_code) WHERE parent_code IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_link_requests (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
parent_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (pupil_id, school_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_school_link_requests_school ON school_link_requests(school_id);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
-- v0.3.83-beta: CMS voor de systeemmanager.
|
||||
-- Kleine sleutel/waarde-opslag voor site-inhoud die zonder release aanpasbaar
|
||||
-- moet zijn: welkomstwoord (inlogscherm), update-meldingen (banner) en
|
||||
-- aanvullende widget-uitleg (galerij). De waarde is JSONB zodat elke sleutel
|
||||
-- zijn eigen vorm heeft (bv. { nl, en } of { actief, tekst per taal, id }).
|
||||
CREATE TABLE IF NOT EXISTS site_content (
|
||||
key TEXT PRIMARY KEY,
|
||||
value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_by BIGINT REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
-- v0.3.95-beta: een gebruiker moet altijd te verwijderen zijn.
|
||||
--
|
||||
-- Alle foreign keys die naar users(id) verwijzen horen ON DELETE CASCADE te
|
||||
-- zijn (of bewust SET NULL, zoals site_content.updated_by). Databases die zijn
|
||||
-- ontstaan vóór een tabel z'n cascade kreeg houden echter de oude constraint:
|
||||
-- `CREATE TABLE IF NOT EXISTS` werkt een bestaande tabel nooit bij. Op zo'n
|
||||
-- database geeft het verwijderen van een gebruiker een foreign-key-fout (500
|
||||
-- "serverfout"). Deze migratie zoekt elke FK naar users die NIET cascade en
|
||||
-- NIET set-null is, en zet die alsnog op ON DELETE CASCADE - de bewuste
|
||||
-- SET NULL-constraints blijven ongemoeid.
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT con.conname AS name,
|
||||
con.conrelid::regclass::text AS tbl,
|
||||
att.attname AS col
|
||||
FROM pg_constraint con
|
||||
JOIN pg_attribute att
|
||||
ON att.attrelid = con.conrelid AND att.attnum = con.conkey[1]
|
||||
WHERE con.contype = 'f'
|
||||
AND con.confrelid = 'users'::regclass
|
||||
AND array_length(con.conkey, 1) = 1 -- alle user-FK's zijn 1 kolom
|
||||
AND con.confdeltype NOT IN ('c', 'n') -- niet cascade, niet set null
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE %s DROP CONSTRAINT %I', r.tbl, r.name);
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %s ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES users(id) ON DELETE CASCADE',
|
||||
r.tbl, r.name, r.col);
|
||||
RAISE NOTICE 'FK % op %(%): ON DELETE CASCADE hersteld', r.name, r.tbl, r.col;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
-- v0.4.1-beta: een klasnaam mag niet dubbel voorkomen binnen dezelfde school.
|
||||
-- Zonder deze constraint kon "Nieuwe klas" herhaaldelijk dezelfde naam
|
||||
-- aanmaken (bv. na een eerdere poging die door een andere bug leek te
|
||||
-- mislukken, maar stiekem toch al aanmaakte) - de app-laag controleert dit nu
|
||||
-- ook (POST /admin/classes), maar de database-constraint is het echte vangnet.
|
||||
--
|
||||
-- Bestaande dubbele klasnamen eerst uniek maken: anders faalt de CREATE
|
||||
-- UNIQUE INDEX hieronder op een database die deze bug al heeft laten
|
||||
-- ontstaan (en dan start de app helemaal niet meer op). Er wordt niets
|
||||
-- verwijderd, alleen de latere dubbele naam hernoemd - de klas, z'n
|
||||
-- leerlingen en koppelingen blijven ongemoeid.
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT id, name,
|
||||
ROW_NUMBER() OVER (PARTITION BY school_id, lower(name) ORDER BY id) AS rn
|
||||
FROM classes
|
||||
LOOP
|
||||
IF r.rn > 1 THEN
|
||||
UPDATE classes SET name = r.name || ' (' || r.rn || ')' WHERE id = r.id;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_classes_school_name
|
||||
ON classes (school_id, lower(name));
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
-- v0.4.07-beta: schoolbeheerder (en systeemmanager) konden niet aan een klas
|
||||
-- gekoppeld worden, met een verwarrende "groepsleiding en klas moeten bij
|
||||
-- dezelfde school horen" - ook al staat dit in de applicatielaag (POST
|
||||
-- /admin/classes/:id/teachers, src/api.js) al langer voor elke stafrol open.
|
||||
--
|
||||
-- Oorzaak: de trigger teach_validate_class_teacher() (uit 006) is destijds
|
||||
-- geschreven toen alleen groepsleiding aan een klas gekoppeld kon worden, en
|
||||
-- is nooit meegegroeid toen schoolbeheerder/systeemmanager daar later bij
|
||||
-- kwamen - de trigger stond dus in de weg van iets wat de app allang toestond.
|
||||
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM classes c
|
||||
JOIN users u ON u.id = NEW.user_id
|
||||
WHERE c.id = NEW.class_id
|
||||
AND (
|
||||
-- systeemmanager is school-onafhankelijk, mag overal aan gekoppeld worden
|
||||
u.role = 'super'
|
||||
OR (
|
||||
u.school_id = c.school_id
|
||||
AND (
|
||||
u.role IN ('teacher', 'admin')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM user_roles ur
|
||||
WHERE ur.user_id = u.id AND ur.role IN ('teacher', 'admin')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'alleen staf uit dezelfde school (of een systeemmanager) kan aan een klas gekoppeld worden';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- trigger zelf blijft ongewijzigd (verwijst naar de herdefinieerde functie),
|
||||
-- alleen opnieuw aangemaakt zodat deze migratie op zichzelf leesbaar is
|
||||
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
|
||||
CREATE TRIGGER trg_class_teachers_tenant
|
||||
BEFORE INSERT OR UPDATE ON class_teachers
|
||||
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();
|
||||
|
|
@ -19,29 +19,10 @@ services:
|
|||
environment:
|
||||
PORT: 3000
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
# APP_VERSION komt uit de image zelf (build-arg); hier niet overschrijven.
|
||||
SUPER_USER: ${SUPER_USER:-beheerder}
|
||||
SUPER_PASS: ${SUPER_PASS:-}
|
||||
# Pangolin/Traefik -> nginx -> app: twee vertrouwde proxy-hops.
|
||||
TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-2}
|
||||
COOKIE_SECURE: "true"
|
||||
APP_VERSION: ${IMAGE}
|
||||
ports:
|
||||
# Alleen op localhost van de VM; nginx zit ervoor als reverse proxy
|
||||
- "127.0.0.1:${APP_PORT:-3000}:3000"
|
||||
volumes:
|
||||
- image_data:/app/storage/images
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
- app
|
||||
ports:
|
||||
# Bereikbaar voor Pangolin/Traefik via het VM-/containernetwerk.
|
||||
# Beperk poort 8081 met de hostfirewall tot de proxy/tunnel.
|
||||
- "${WEB_BIND_IP:-0.0.0.0}:${WEB_PORT:-8081}:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
|
|
@ -62,4 +43,3 @@ services:
|
|||
|
||||
volumes:
|
||||
pgdata:
|
||||
image_data:
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
# Behoud het publieke schema van Pangolin/Traefik; val lokaal terug op nginx.
|
||||
map $http_x_forwarded_proto $teach_forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
"" $scheme;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
|
||||
# Wat groter zodat grote borden/afbeeldingen niet geweigerd worden
|
||||
client_max_body_size 55m;
|
||||
|
||||
# Gzip-vangnet: de app comprimeert zelf al (brotli/gzip via @fastify/compress)
|
||||
# en nginx comprimeert nooit dubbel (responses mét Content-Encoding blijven
|
||||
# ongemoeid) - dit vangt alleen wat de app mist. Let op: zonder
|
||||
# gzip_proxied comprimeert nginx geproxyde responses helemaal niet.
|
||||
gzip on;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml text/plain;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:3000;
|
||||
proxy_http_version 1.1;
|
||||
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 $teach_forwarded_proto;
|
||||
}
|
||||
}
|
||||
639
package-lock.json
generated
|
|
@ -8,11 +8,7 @@
|
|||
"name": "teach",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fastify/compress": "^8.3.1",
|
||||
"@fastify/cookie": "^11.1.1",
|
||||
"@fastify/helmet": "^13.1.0",
|
||||
"@fastify/rate-limit": "^11.1.0",
|
||||
"@fastify/static": "^10.1.0",
|
||||
"@fastify/static": "^8.0.1",
|
||||
"fastify": "^5.1.0",
|
||||
"pg": "^8.13.1"
|
||||
},
|
||||
|
|
@ -57,81 +53,6 @@
|
|||
"fast-uri": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/compress": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-8.3.1.tgz",
|
||||
"integrity": "sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/accept-negotiator": "^2.0.0",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"mime-db": "^1.52.0",
|
||||
"minipass": "^7.0.4",
|
||||
"peek-stream": "^1.1.3",
|
||||
"pump": "^3.0.0",
|
||||
"pumpify": "^2.0.1",
|
||||
"readable-stream": "^4.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/compress/node_modules/fastify-plugin": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
|
||||
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/cookie": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.1.tgz",
|
||||
"integrity": "sha512-sJ0NXzGVYjUB4OynPZRsIcQ1mKSP4rW45xLCN0aelRq5Vl37xVVbz5kJ6Y0a9m2T0mCUjYCuvlUA9QlTafrZWw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^2.0.0",
|
||||
"fastify-plugin": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/cookie/node_modules/cookie": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
|
||||
"integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/error": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
||||
|
|
@ -183,26 +104,6 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/helmet": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.1.0.tgz",
|
||||
"integrity": "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fastify-plugin": "^6.0.0",
|
||||
"helmet": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/merge-json-schemas": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
|
||||
|
|
@ -242,27 +143,6 @@
|
|||
"ipaddr.js": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/rate-limit": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.1.0.tgz",
|
||||
"integrity": "sha512-BeJ9tizLvmTXGD7deYU5G04OtHhwk5uHxbpEPVp09gKvUBIXmau/4Bshxhu9ci54MvVWfGjCEx4RzvsTntojwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lukeed/ms": "^2.0.2",
|
||||
"fastify-plugin": "^6.0.0",
|
||||
"toad-cache": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/send": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz",
|
||||
|
|
@ -287,9 +167,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@fastify/static": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.0.tgz",
|
||||
"integrity": "sha512-iK/8TvRM/EgNOyQL+EpWu+x3aR6o4GWt+UI+27zmE7w6t/6d80mXqOtWLdEKQ13vL/g1Jry0ae2icj6GP7tGzA==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz",
|
||||
"integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -303,12 +183,20 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/accept-negotiator": "^2.0.0",
|
||||
"@fastify/error": "^4.0.0",
|
||||
"@fastify/send": "^4.0.0",
|
||||
"content-disposition": "^2.0.1",
|
||||
"fastify-plugin": "^6.0.0",
|
||||
"content-disposition": "^0.5.4",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"fastq": "^1.17.1",
|
||||
"glob": "^13.0.0"
|
||||
"glob": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
||||
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/ms": {
|
||||
|
|
@ -326,18 +214,6 @@
|
|||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/abstract-logging": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||
|
|
@ -415,26 +291,6 @@
|
|||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
|
|
@ -447,47 +303,16 @@
|
|||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
|
||||
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
|
|
@ -503,11 +328,19 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -527,81 +360,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^2.0.0",
|
||||
"stream-shift": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/duplexify/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-decode-uri-component": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
|
||||
|
|
@ -697,9 +461,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/fastify-plugin": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz",
|
||||
"integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
|
||||
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -735,33 +499,44 @@
|
|||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
"path-scurry": "^2.0.2"
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
|
||||
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
|
||||
"license": "MIT",
|
||||
"node_modules/glob": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/EvanHahn"
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
|
|
@ -784,26 +559,6 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
|
|
@ -819,11 +574,26 @@
|
|||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-ref-resolver": {
|
||||
"version": "3.0.0",
|
||||
|
|
@ -888,9 +658,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"version": "11.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
|
||||
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
|
|
@ -908,15 +678,6 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
|
|
@ -950,13 +711,19 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
|
|
@ -975,17 +742,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/peek-stream": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
|
||||
"integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"duplexify": "^3.5.0",
|
||||
"through2": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
|
||||
|
|
@ -1151,21 +907,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
|
|
@ -1182,75 +923,12 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pumpify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
|
||||
"integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexify": "^4.1.1",
|
||||
"inherits": "^2.0.3",
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pumpify/node_modules/duplexify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.4.1",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stream-shift": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/pumpify/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
|
|
@ -1385,6 +1063,39 @@
|
|||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||
|
|
@ -1412,21 +1123,6 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-shift": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
|
||||
|
|
@ -1445,46 +1141,6 @@
|
|||
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/through2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "~2.3.6",
|
||||
"xtend": "~4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/through2/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/through2/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/through2/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toad-cache": {
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
|
||||
|
|
@ -1503,17 +1159,20 @@
|
|||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
|
|
|
|||
|
|
@ -9,15 +9,10 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "node --watch src/server.js",
|
||||
"test": "node --test"
|
||||
"dev": "node --watch src/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/compress": "^8.3.1",
|
||||
"@fastify/cookie": "^11.1.1",
|
||||
"@fastify/helmet": "^13.1.0",
|
||||
"@fastify/rate-limit": "^11.1.0",
|
||||
"@fastify/static": "^10.1.0",
|
||||
"@fastify/static": "^8.0.1",
|
||||
"fastify": "^5.1.0",
|
||||
"pg": "^8.13.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
.image-catalog-modal{width:min(1080px,96vw);max-height:94vh}
|
||||
/* titelbalk met ✕ blijft altijd in beeld, ook na scrollen in een lange grid */
|
||||
.image-catalog-modal .gallery-head{position:sticky;top:-24px;z-index:4;background:var(--surface);padding:4px 0 8px;margin-top:-4px}
|
||||
.ic-tools{display:grid;grid-template-columns:minmax(180px,1.5fr) repeat(3,minmax(130px,.8fr)) auto;gap:8px;position:sticky;top:12px;z-index:3;background:var(--surface);padding:8px 0 10px}
|
||||
.ic-tools input,.ic-tools select,.ic-add select,.ic-add input,.ic-editor input,.ic-editor select{min-width:0;width:100%;border:1px solid var(--line);border-radius:10px;padding:9px 10px;background:var(--surface);color:var(--ink);font:inherit}
|
||||
.ic-word-themes{display:flex;align-items:center;gap:7px;overflow-x:auto;padding:9px 10px;margin:0 0 10px;border:1px solid var(--line);border-radius:13px;background:linear-gradient(135deg,var(--surface-2),var(--accent-soft));scrollbar-width:thin}
|
||||
.ic-word-themes strong{flex:none;margin-right:3px;font-size:12px;color:var(--muted)}
|
||||
.ic-word-themes button{flex:none;border:1px solid var(--line);border-radius:999px;padding:7px 11px;background:var(--surface);color:var(--ink);font:inherit;font-size:12px;font-weight:850;cursor:pointer;transition:transform .12s,border-color .12s,background .12s}
|
||||
.ic-word-themes button:hover{transform:translateY(-1px);border-color:var(--accent)}
|
||||
.ic-word-themes button.active{border-color:var(--accent);background:var(--accent);color:#fff;box-shadow:var(--shadow-1)}
|
||||
.ic-quota{padding:8px 12px;margin-bottom:10px;border-radius:10px;background:var(--accent-soft);color:var(--accent-ink);font-size:13px;font-weight:800}
|
||||
.ic-add{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-bottom:10px;background:var(--surface-2)}
|
||||
.ic-add summary{cursor:pointer;font-weight:900}
|
||||
.ic-add-grid,.ic-editor-grid{display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:10px;margin-top:10px;align-items:end}
|
||||
.ic-field{display:flex;flex-direction:column;gap:4px;font-size:12px;font-weight:800;color:var(--muted)}
|
||||
.ic-field select[multiple]{min-height:88px}
|
||||
.ic-upload{display:flex;align-items:center;justify-content:center;min-height:44px;margin:0;cursor:pointer;text-align:center;color:var(--ink)}
|
||||
.ic-theme-new{display:flex;gap:8px;margin-top:10px}.ic-theme-new input{flex:1}
|
||||
.ic-status{min-height:20px;font-size:13px;font-weight:800;color:var(--red);padding:2px 0}.ic-status.ok{color:var(--green)}
|
||||
.ic-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:10px}
|
||||
.ic-card{position:relative;min-width:0;border:1px solid var(--line);border-radius:14px;background:var(--surface);overflow:hidden;transition:border-color .15s,box-shadow .15s}
|
||||
.ic-card:hover{border-color:var(--accent);box-shadow:var(--shadow-1)}
|
||||
.ic-choose{display:grid;grid-template-rows:120px auto auto;width:100%;height:100%;gap:5px;padding:10px;border:0;background:transparent;color:var(--ink);font:inherit;text-align:left;cursor:pointer}
|
||||
.ic-choose img{width:100%;height:120px;object-fit:contain;border-radius:9px;background:#fff}
|
||||
.ic-choose strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ic-choose span{font-size:11px;color:var(--muted);min-height:28px;overflow:hidden}
|
||||
.ic-card-edit{position:absolute;right:7px;top:7px;width:32px;height:32px;border:1px solid var(--line);border-radius:9px;background:var(--surface);cursor:pointer;box-shadow:var(--shadow-1)}
|
||||
.ic-editor{position:sticky;bottom:-24px;z-index:4;margin-top:12px;padding:14px;border:2px solid var(--accent);border-radius:14px;background:var(--surface);box-shadow:var(--shadow-2)}
|
||||
.ic-editor h3{margin:0}.ic-editor-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
|
||||
.ic-explorer-thumb{width:64px;height:56px;object-fit:contain;border-radius:8px;background:#fff}
|
||||
.img-quota-admin{display:flex;flex-direction:column;gap:7px}.img-quota-row{display:grid;grid-template-columns:minmax(180px,1fr) 130px 130px auto;align-items:center}
|
||||
.img-quota-users{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
||||
body.hc .ic-card,body.hc .ic-add,body.hc .ic-tools input,body.hc .ic-tools select,body.hc .ic-editor{border-color:#000}
|
||||
@media(max-width:760px){
|
||||
/* dvh i.p.v. vh: met een dynamische browserbalk (telefoon) is 100vh hoger
|
||||
dan het zichtbare scherm, waardoor titel en sluitknop wegvielen */
|
||||
.image-catalog-modal{width:100vw;max-height:100dvh;height:100dvh;border-radius:0;padding:16px}
|
||||
.image-catalog-modal .gallery-head{top:-16px}
|
||||
.ic-tools{top:20px;grid-template-columns:1fr 1fr;padding-top:4px}.ic-search,.ic-folders{grid-column:1/-1}
|
||||
.ic-word-themes{margin-inline:-2px;padding:8px}.ic-word-themes strong{position:sticky;left:0;z-index:1;padding:7px 6px;background:var(--surface-2)}
|
||||
.ic-add-grid,.ic-editor-grid{grid-template-columns:1fr}.ic-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.ic-choose{grid-template-rows:96px auto auto}.ic-choose img{height:96px}
|
||||
.img-quota-row{grid-template-columns:1fr 1fr}.img-quota-row .am-name{grid-column:1/-1}
|
||||
}
|
||||
@media(max-width:420px){.ic-grid{grid-template-columns:1fr}.ic-editor-actions{flex-wrap:wrap}.ic-editor-actions .tbtn{flex:1}}
|
||||
2104
public/css/teach.css
|
|
@ -1,4 +0,0 @@
|
|||
Quicksand - ontworpen door Andrew Paglinawan e.a.
|
||||
Gelicenseerd onder de SIL Open Font License 1.1 (https://openfontlicense.org).
|
||||
Bron: Google Fonts (https://fonts.google.com/specimen/Quicksand).
|
||||
Dit bestand (quicksand.woff2) is het latin-subset van de variabele font (gewichten 300-700).
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 104" role="img" aria-label="teachus.today uil">
|
||||
<!-- laag-poly uil: vlakke facetten in het merk-palet, transparante achtergrond -->
|
||||
<!-- oren -->
|
||||
<polygon points="33,28 40,10 48,22" fill="#1d3461"/>
|
||||
<polygon points="62,22 70,10 77,28" fill="#1d3461"/>
|
||||
<!-- voorhoofd -->
|
||||
<polygon points="40,10 70,10 62,22 48,22" fill="#2456b8"/>
|
||||
<!-- gezicht -->
|
||||
<polygon points="33,28 48,22 44,38" fill="#2b4d94"/>
|
||||
<polygon points="77,28 62,22 66,38" fill="#2b4d94"/>
|
||||
<polygon points="48,22 62,22 66,38 44,38" fill="#24407a"/>
|
||||
<!-- wangen -->
|
||||
<polygon points="33,28 44,38 36,46" fill="#2f6fe4"/>
|
||||
<polygon points="77,28 66,38 74,46" fill="#2f6fe4"/>
|
||||
<!-- ogen -->
|
||||
<circle cx="48.5" cy="30.5" r="4.6" fill="#cfe0f5"/>
|
||||
<circle cx="61.5" cy="30.5" r="4.6" fill="#cfe0f5"/>
|
||||
<circle cx="48.5" cy="30.5" r="2.3" fill="#101d3a"/>
|
||||
<circle cx="61.5" cy="30.5" r="2.3" fill="#101d3a"/>
|
||||
<!-- snavel -->
|
||||
<polygon points="55,36 58.5,41 55,46 51.5,41" fill="#f28c38"/>
|
||||
<!-- linkervleugel: cyaan/teal -->
|
||||
<polygon points="36,46 44,38 44,56" fill="#3ac2e8"/>
|
||||
<polygon points="36,46 44,56 32,62" fill="#18c5c0"/>
|
||||
<polygon points="32,62 44,56 40,74" fill="#66d4ef"/>
|
||||
<polygon points="32,62 40,74 30,78" fill="#12a8a4"/>
|
||||
<polygon points="30,78 40,74 38,88" fill="#18c5c0"/>
|
||||
<!-- borst: roze/oranje -->
|
||||
<polygon points="44,38 55,46 44,56" fill="#e83e8c"/>
|
||||
<polygon points="55,46 66,38 66,56" fill="#f28c38"/>
|
||||
<polygon points="44,56 55,46 58,64" fill="#c2257f"/>
|
||||
<polygon points="55,46 66,56 58,64" fill="#f06ba8"/>
|
||||
<polygon points="44,56 58,64 48,78" fill="#f5a55c"/>
|
||||
<polygon points="58,64 66,56 68,74" fill="#e83e8c"/>
|
||||
<polygon points="48,78 58,64 68,74" fill="#8b5cf6"/>
|
||||
<polygon points="48,78 68,74 58,86" fill="#f28c38"/>
|
||||
<!-- rechterflank: blauw/navy -->
|
||||
<polygon points="66,38 74,46 66,56" fill="#2f6fe4"/>
|
||||
<polygon points="66,56 74,46 76,66" fill="#1d3461"/>
|
||||
<polygon points="66,56 76,66 68,74" fill="#2456b8"/>
|
||||
<polygon points="68,74 76,66 70,84" fill="#1d3461"/>
|
||||
<!-- tak -->
|
||||
<polygon points="26,92 106,85 106,88.4 26,95.4" fill="#1d3461"/>
|
||||
<!-- blaadjes -->
|
||||
<polygon points="92,70 99,78 92,84 85,78" fill="#18c5c0"/>
|
||||
<polygon points="104,77 110,83 104,88 98,83" fill="#3ac2e8"/>
|
||||
<polygon points="96,90 102,95 96,100 90,95" fill="#12a8a4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
|
@ -1,8 +0,0 @@
|
|||
# Bronnen euro-afbeeldingen
|
||||
|
||||
Deze afbeeldingen worden lokaal meegeleverd, zodat de geldwidget ook zonder internetverbinding werkt.
|
||||
|
||||
- **Munten (1, 2, 5, 10, 20 en 50 cent, €1 en €2):** actuele gemeenschappelijke zijden van de Europese Commissie, gedownload van [Common sides of euro coins](https://economy-finance.ec.europa.eu/euro/euro-coins-and-notes/euro-coins/common-sides-euro-coins_en) op 15 juli 2026. De tweedimensionale afbeeldingen zijn eigendom van de Europese Unie en worden hier educatief gebruikt volgens de reproductievoorwaarden van de Europese Commissie.
|
||||
- **Bankbiljetten (€5, €10, €20 en €50):** voorzijde van de Europa-serie met Lagarde-handtekening uit het officiële [ECB-pakket met 72-dpi SPECIMEN-afbeeldingen](https://www.ecb.europa.eu/euro/shared/img/euro_banknotes_specimen_72dpi.zip), gedownload op 15 juli 2026. De zichtbare SPECIMEN-markering en originele 72-dpi-bestanden worden behouden volgens de [ECB-reproductieregels](https://www.ecb.europa.eu/euro/banknotes/images/html/index.en.html).
|
||||
|
||||
De bestandsnamen gebruiken centen als waarde, bijvoorbeeld coin-200.png voor €2 en note-500.jpg voor €5.
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 556 KiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 543 KiB |
|
|
@ -1,115 +0,0 @@
|
|||
# Wereldoriëntatie – beeldbronnen
|
||||
|
||||
De lokale WebP-bestanden zijn uitsluitend verkleinde en, waar nodig, gekaderde
|
||||
afgeleiden van de hieronder genoemde foto's. De soortnamen zijn gecontroleerd
|
||||
aan de hand van de bestandsbeschrijvingen op Wikimedia Commons.
|
||||
|
||||
## Bomen
|
||||
|
||||
| Bestand | Bronwerk | Maker | Licentie |
|
||||
| --- | --- | --- | --- |
|
||||
| `trees/oak.webp` | [1. English Oak (Quercus robur)](https://commons.wikimedia.org/wiki/File:1._English_Oak_(Quercus_robur)_(3607652442).jpg) | AnemoneProjectors | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
|
||||
| `trees/birch.webp` | [Betula pendula winter](https://commons.wikimedia.org/wiki/File:Betula_pendula_winter.jpg) | Maplan | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `trees/horse-chestnut.webp` | [Paardenkastanje2](https://commons.wikimedia.org/wiki/File:Paardenkastanje2.jpg) | Toubib | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `trees/pine.webp` | [Pinus sylvestris Glenmuick](https://commons.wikimedia.org/wiki/File:Pinus_sylvestris_Glenmuick.jpg) | Hello, I am Bruce | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
|
||||
| `trees/beech.webp` | [Fagales – Fagus sylvatica](https://commons.wikimedia.org/wiki/File:Fagales_-_Fagus_sylvatica.jpg) | Emőke Dénes | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `trees/maple.webp` | [Acer-pseudoplatanus](https://commons.wikimedia.org/wiki/File:Acer-pseudoplatanus.jpg) | MPF | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `trees/willow.webp` | [Salix alba an der Isar](https://commons.wikimedia.org/wiki/File:Salix_alba_an_der_Isar.jpg) | Burkhard Mücke | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `trees/alder.webp` | [Alnus glutinosa 011](https://commons.wikimedia.org/wiki/File:Alnus_glutinosa_011.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
|
||||
| `trees/lime.webp` | [Winterlinde (Wiki Loves Earth 2015)](https://commons.wikimedia.org/wiki/File:012_2015_05_06_Winterlinde_(Wiki_Loves_Earth_2015).jpg) | Friedrich Haag | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `trees/ash.webp` | [Fraxinus excelsior 001](https://commons.wikimedia.org/wiki/File:Fraxinus_excelsior_001.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
|
||||
| `trees/sweet-chestnut.webp` | [Anatolian sweet chestnut](https://commons.wikimedia.org/wiki/File:Castanea_sativa_-_Anatolian_sweet_chestnut_04.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `trees/rowan.webp` | [Rowan tree 20081002b](https://commons.wikimedia.org/wiki/File:Rowan_tree_20081002b.jpg) | Eeno11 | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `trees/hawthorn.webp` | [Crataegus monogyna in Joncret](https://commons.wikimedia.org/wiki/File:Crataegus_monogyna_in_Joncret_AR1aJPG.jpg) | Jean-Pol GRANDMONT | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
|
||||
| `trees/aspen.webp` | [Populus tremula 004](https://commons.wikimedia.org/wiki/File:Populus_tremula_004.jpg) | Willow | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
|
||||
| `trees/spruce.webp` | [Kuusk Keila-Paldiski rdt ääres](https://commons.wikimedia.org/wiki/File:Kuusk_Keila-Paldiski_rdt_%C3%A4%C3%A4res.jpg) | Ivar Leidus | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `trees/larch.webp` | [European larch – Larix decidua](https://commons.wikimedia.org/wiki/File:European_larch_-_Larix_decidua.jpg) | MrPanyGoff | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `trees/yew.webp` | [Witch’s Yew Tree](https://commons.wikimedia.org/wiki/File:2020_Witch%E2%80%99s_Yew_Tree_IRL.jpg) | Blarney Castle Estate | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `trees/elm.webp` | [Ulmus minor tree](https://commons.wikimedia.org/wiki/File:Ulmus_minor_tree.jpg) | Ptelea | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
|
||||
|
||||
## Bladeren en naalden
|
||||
|
||||
| Bestand | Bronwerk | Maker | Licentie |
|
||||
| --- | --- | --- | --- |
|
||||
| `leaves/oak.webp` | [Zomereik blad Quercus robur](https://commons.wikimedia.org/wiki/File:Zomereik_blad_Quercus_robur.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `leaves/birch.webp` | [Atlas roslin pl Brzoza brodawkowata 1393 6470](https://commons.wikimedia.org/wiki/File:Atlas_roslin_pl_Brzoza_brodawkowata_1393_6470.jpg) | Joanna Boisse | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/horse-chestnut.webp` | [Aesculus hippocastanum in botanical garden of UKW Bydgoszcz (2)](https://commons.wikimedia.org/wiki/File:Aesculus_hippocastanum_in_botanical_garden_of_UKW_Bydgoszcz_(2).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
|
||||
| `leaves/pine.webp` | [20151215Pinus sylvestris2](https://commons.wikimedia.org/wiki/File:20151215Pinus_sylvestris2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
|
||||
| `leaves/beech.webp` | [20130530Hain Rotbuche](https://commons.wikimedia.org/wiki/File:20130530Hain_Rotbuche.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
|
||||
| `leaves/maple.webp` | [Acer pseudoplatanus 003](https://commons.wikimedia.org/wiki/File:Acer_pseudoplatanus_003.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
|
||||
| `leaves/willow.webp` | [20160721Salix alba1](https://commons.wikimedia.org/wiki/File:20160721Salix_alba1.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
|
||||
| `leaves/alder.webp` | [20200531Alnus glutinosa2](https://commons.wikimedia.org/wiki/File:20200531Alnus_glutinosa2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
|
||||
| `leaves/lime.webp` | [Herbarium Tilia cordata img-007](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Tilia_cordata._img-007.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/ash.webp` | [Herbarium Fraxinus img-015](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Fraxinus._img-015.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/sweet-chestnut.webp` | [Castanea sativa leaves and immature fruit](https://commons.wikimedia.org/wiki/File:Castanea_sativa_leaves_and_immature_fruit.jpg) | Ekir | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `leaves/rowan.webp` | [Herbarium Sorbus aucuparia img-005](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Sorbus_aucuparia._img-005.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/hawthorn.webp` | [Crataegus monogyna – leaves and stipules](https://commons.wikimedia.org/wiki/File:(MHNT)_Crataegus_monogyna_-_Leaves_and_stipules.jpg) | Didier Descouens | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/aspen.webp` | [Herbarium Populus tremula img-017](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Populus_tremula._img-017.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/spruce.webp` | [Picea abies needles in Väsmestorp](https://commons.wikimedia.org/wiki/File:Picea_abies_needles_in_V%C3%A4smestorp_2024-10-29_(0261).jpg) | Achird | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `leaves/larch.webp` | [Larix decidua needles and male cones](https://commons.wikimedia.org/wiki/File:Larix_decidua_needles_and_male_cones.JPG) | Rosser1954 / Roger Griffith | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
|
||||
| `leaves/yew.webp` | [Yew berries](https://commons.wikimedia.org/wiki/File:Yew_berries_(50578142398).jpg) | hedera.baltica | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
|
||||
| `leaves/elm.webp` | [Elm leaves (Ulmus minor)](https://commons.wikimedia.org/wiki/File:Elm_leaves_(Ulmus_minor).jpg) | Daniel Capilla | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
|
||||
## Vogels
|
||||
|
||||
| Bestand | Bronwerk | Maker | Licentie |
|
||||
| --- | --- | --- | --- |
|
||||
| `birds/robin.webp` | [Erithacus rubecula, Roodborstje](https://commons.wikimedia.org/wiki/File:Erithacus_rubecula,_Roodborstje.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `birds/great-tit.webp` | [Parus major m](https://commons.wikimedia.org/wiki/File:Parus_major_m.jpg) | Sławek Staszczuk | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `birds/blackbird.webp` | [Solsort](https://commons.wikimedia.org/wiki/File:Solsort.jpg) | Malene Thyssen | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `birds/mallard.webp` | [Duisburg 10401IMG 0401](https://commons.wikimedia.org/wiki/File:Duisburg_10401IMG_0401.JPG) | Mbdortmund | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) |
|
||||
| `birds/blue-tit.webp` | [Cyanistes caeruleus 3 Luc Viatour](https://commons.wikimedia.org/wiki/File:Cyanistes_caeruleus_3_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `birds/house-sparrow.webp` | [Huismus, man](https://commons.wikimedia.org/wiki/File:Huismus,_man.jpg) | JrPol | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `birds/magpie.webp` | [Сорока (Pica pica), Калининград](https://commons.wikimedia.org/wiki/File:%D0%A1%D0%BE%D1%80%D0%BE%D0%BA%D0%B0_(Pica_pica),_%D0%9A%D0%B0%D0%BB%D0%B8%D0%BD%D0%B8%D0%BD%D0%B3%D1%80%D0%B0%D0%B4.jpg) | Dion Art | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `birds/woodpecker.webp` | [Grote bonte specht](https://commons.wikimedia.org/wiki/File:Grote_bonte_specht.JPG) | Usitea | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `birds/chaffinch.webp` | [Fringilla coelebs chaffinch male](https://commons.wikimedia.org/wiki/File:Fringilla_coelebs_chaffinch_male_edit2.jpg) | MichaelMaggs / Arad | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
|
||||
| `birds/starling.webp` | [Sturnus vulgaris – England – standing](https://commons.wikimedia.org/wiki/File:Sturnus_vulgaris_-England_-standing-8.jpg) | Mark Robinson | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
|
||||
| `birds/grey-heron.webp` | [Grey heron 2022 04 03 03](https://commons.wikimedia.org/wiki/File:Grey_heron_2022_04_03_03.jpg) | Alexis Lours | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
|
||||
| `birds/mute-swan.webp` | [Wild Mute swan at Lake Geneva](https://commons.wikimedia.org/wiki/File:093_Wild_Mute_swan_at_Lake_Geneva_during_sunset_Photo_by_Giles_Laurent.jpg) | Giles Laurent | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `birds/coot.webp` | [Fulica atra – Common coot](https://commons.wikimedia.org/wiki/File:Fulica_atra_-_Common_coot,_Adana_2016-11-12_01-1.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `birds/greylag-goose.webp` | [Graugans Anser Anser](https://commons.wikimedia.org/wiki/File:Graugans_Anser_Anser.jpg) | H005 / Pro2 | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
|
||||
| `birds/collared-dove.webp` | [Eurasian Collared Dove](https://commons.wikimedia.org/wiki/File:Streptopelia_decaocto_-_Eurasian_Collared_Dove_05.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `birds/wren.webp` | [Eurasian wren](https://commons.wikimedia.org/wiki/File:Eurasian_wren_(Troglodytes_troglodytes).jpg) | Alexis Lours | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
|
||||
| `birds/buzzard.webp` | [Buteo buteo juv Log](https://commons.wikimedia.org/wiki/File:Buteo_buteo_juv_Log_(2).jpg) | Yerpo | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `birds/barn-owl.webp` | [Tyto alba 1 Luc Viatour](https://commons.wikimedia.org/wiki/File:Tyto_alba_1_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
|
||||
## Wilde planten
|
||||
|
||||
| Bestand | Bronwerk | Maker | Licentie |
|
||||
| --- | --- | --- | --- |
|
||||
| `plants/dandelion.webp` | [Paardenbloem (Taraxacum officinale)](https://commons.wikimedia.org/wiki/File:Paardenbloem_(Taraxacum_officinale)_06.JPG) | Dominicus Johannes Bergsma | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `plants/nettle.webp` | [Brandnetel, Urtica dioica](https://commons.wikimedia.org/wiki/File:Brandnetel,_Urtica_dioica,_Locatie,_Famberhorst.jpg) | Dominicus Johannes Bergsma | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `plants/daisy.webp` | [Bellis perennis full plant](https://commons.wikimedia.org/wiki/File:Bellis_perennis_full_plant.jpg) | Grand-Duc | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `plants/red-clover.webp` | [Trifolium pratense](https://commons.wikimedia.org/wiki/File:Trifolium_pratense_RF.jpg) | Robert Flogaus-Faust | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
|
||||
| `plants/white-deadnettle.webp` | [Lamium album](https://commons.wikimedia.org/wiki/File:Lamium_album_RF.jpg) | Robert Flogaus-Faust | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
|
||||
| `plants/cow-parsley.webp` | [Fluitenkruid (Anthriscus sylvestris)](https://commons.wikimedia.org/wiki/File:Fluitenkruid_(Anthriscus_sylvestris)_26-04-2020._(actm.)_02.jpg) | Agnes Monkelbaan | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `plants/yarrow.webp` | [Achillea millefolium](https://commons.wikimedia.org/wiki/File:Achillea_millefolium_(52354).jpg) | Rhododendrites | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `plants/ribwort-plantain.webp` | [Plantago lanceolata kz09](https://commons.wikimedia.org/wiki/File:Plantago_lanceolata_kz09.jpg) | Krzysztof Ziarnek, Kenraiz | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `plants/common-poppy.webp` | [Papaver rhoeas LC0050](https://commons.wikimedia.org/wiki/File:Papaver_rhoeas_LC0050.jpg) | Jörg Hempel | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
|
||||
| `plants/chicory.webp` | [Cichorium intybus](https://commons.wikimedia.org/wiki/File:Cichorium_intybus_TK_2021-07-12_1.jpg) | T. Kebert | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
|
||||
| `plants/foxglove.webp` | [Digitalis purpurea LC0101](https://commons.wikimedia.org/wiki/File:Digitalis_purpurea_LC0101.jpg) | Jörg Hempel | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
|
||||
| `plants/common-sorrel.webp` | [Rumex acetosa in Aveyron](https://commons.wikimedia.org/wiki/File:Rumex_acetosa_in_Aveyron_(7).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
|
||||
|
||||
## Topografie
|
||||
|
||||
`netherlands-provinces.svg` is een vereenvoudigde afgeleide zonder labels van
|
||||
[Netherlands provinces map blank.svg](https://commons.wikimedia.org/wiki/File:Netherlands_provinces_map_blank.svg)
|
||||
door Gust Justice, gelicentieerd onder
|
||||
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). De afgeleide
|
||||
provinciecontouren worden onder dezelfde licentie verspreid.
|
||||
|
||||
De plaatsing van stadspunten is daarnaast gecontroleerd met
|
||||
[Map provinces Netherlands-en.svg](https://commons.wikimedia.org/wiki/File:Map_provinces_Netherlands-en.svg)
|
||||
door Alphathon ([CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/)).
|
||||
|
||||
`world-continents.svg` is gegenereerd uit de fysieke
|
||||
[Natural Earth 1:110m Geography Regions Polygons](https://www.naturalearthdata.com/downloads/110m-physical-vectors/110m-physical-labels/)
|
||||
en, voor de eilanden van Oceanië,
|
||||
[Admin 0 Countries](https://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/).
|
||||
Natural Earth stelt deze geografische brondata beschikbaar als publiek domein.
|
||||
De lokale afgeleide bevat zeven afzonderlijk bedienbare werelddeelcontouren.
|
||||
|
||||
De vier Nederlandse landsdelen volgen de
|
||||
[CBS-indeling voor Gebieden in Nederland 2026](https://www.cbs.nl/nl-nl/cijfers/detail/86247NED).
|
||||
De selectie en loop van de grote rivieren zijn gecontroleerd aan de hand van het
|
||||
[Rijkswaterstaat-overzicht Rivieren](https://www.rijkswaterstaat.nl/water/waterbeheer/beheer-en-ontwikkeling-rijkswateren/rivieren).
|
||||
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 153 KiB |