Rechtliches: Skript zum Hochladen der Datenschutzerklärung als neue Version
Der Ablauf des Vorbilds (hochladen) ist jetzt eine exportierte, parametrisierte Funktion; datenschutz-hochladen.mjs ruft sie mit Karte datenschutz, PDF und HTML auf und bricht ab, wenn der Stand in der HTML nicht der Freigabe entspricht. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit 06676c459d0ac4f534aed7d616e2c0d40ac97b97)
This commit is contained in:
1 parent
dd062b1f15
commit
0fbeadd0c1
3 files changed
+253
-18
No files matched your search
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
// Lädt die Datenschutzerklärung (PDF und HTML) nach der Freigabe als neue Version in WOMS „Rechtliches“ hoch (Karte datenschutz, Bucket
|
||||
// legal-files, Collection legalVersions): neue Fassung "current", die bisherige gültige wird "superseded" mit validTo. Nur mit
|
||||
// --freigabe=JJJJ-MM-TT; dieser Tag muss der „Stand TT.MM.JJJJ“ in public/datenschutz.html entsprechen. Schon hochgeladene Stände (gleiche source,
|
||||
// also gleicher Commit) werden übersprungen. Den Ablauf teilt sich das Skript mit rechtliches-hochladen.mjs (Funktion hochladen).
|
||||
//
|
||||
// node scripts/datenschutz-hochladen.mjs --probe nur lesen: Schlüssel, Bucket, Karte datenschutz und ihre Versionen
|
||||
// node scripts/datenschutz-hochladen.mjs --freigabe=JJJJ-MM-TT lädt die Datenschutzerklärung hoch (validFrom = Freigabedatum); ohne Karte,
|
||||
// ohne beide Dateien, mit nicht committetem Stand oder mit abweichendem
|
||||
// „Stand“ Abbruch vor dem Schreiben
|
||||
//
|
||||
// Schlüssel: kurzlebiger Appwrite-Schlüssel mit files.read/files.write/documents.read/documents.write in /root/.appwrite-rechtliches.key (chmod 600).
|
||||
// Der Import dieser Datei liest weder den Schlüssel noch nutzt er das Netz; beides geschieht nur in main (deps.schluessel, deps.fetch).
|
||||
// Dieses Skript wird nur geschrieben und mit Nachbau-deps getestet, nie von einem Agenten ausgeführt (auch nicht mit --probe).
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { BUCKET, KEY_DATEI, alleDokumente, client, ddmmjjjj, hochladen, istKalendertag, versionen } from './rechtliches-hochladen.mjs'
|
||||
|
||||
const DOKUMENT_KEY = 'datenschutz'
|
||||
/** [PDF, HTML]: in dieser Reihenfolge hochgeladen. */
|
||||
const DATEIEN = Object.freeze(['docs/WEBklar-Datenschutzerklaerung.pdf', 'public/datenschutz.html'])
|
||||
const QUELLE = 'public/datenschutz.html'
|
||||
/** Für Kunden sichtbar (WOMS „Rechtliches“): darum weder „Workers“ noch „Hustler“. */
|
||||
const AENDERUNG = 'Abschnitt 11 Interessenten (Ansprache per E-Mail)'
|
||||
|
||||
const USAGE = 'Aufruf: node scripts/datenschutz-hochladen.mjs --probe | --freigabe=JJJJ-MM-TT'
|
||||
|
||||
/**
|
||||
* Kommandozeile prüfen: --probe allein oder genau eine --freigabe=JJJJ-MM-TT (ein echter Kalendertag).
|
||||
* @returns {{ modus: 'probe'|'hochladen', freigabe: string|null }}
|
||||
*/
|
||||
export function parseArgs(argv) {
|
||||
const fremde = argv.filter((a) => a !== '--probe' && !a.startsWith('--freigabe='))
|
||||
if (fremde.length > 0) throw new Error(`${USAGE}\nUnbekanntes Argument: ${fremde[0].slice(0, 40)}`)
|
||||
const probe = argv.filter((a) => a === '--probe')
|
||||
const freigaben = argv.filter((a) => a.startsWith('--freigabe='))
|
||||
if (probe.length > 0) {
|
||||
if (probe.length > 1 || freigaben.length > 0) throw new Error(`${USAGE}\n--probe steht allein.`)
|
||||
return { modus: 'probe', freigabe: null }
|
||||
}
|
||||
if (freigaben.length !== 1) throw new Error(`${USAGE}\nFreigabe fehlt: genau eine --freigabe=JJJJ-MM-TT ist beim Hochladen Pflicht.`)
|
||||
const freigabe = freigaben[0].slice('--freigabe='.length)
|
||||
if (!istKalendertag(freigabe)) throw new Error(`Freigabe ungültig (erwartet JJJJ-MM-TT, ein echter Kalendertag): ${freigabe.slice(0, 20)}`)
|
||||
return { modus: 'hochladen', freigabe }
|
||||
}
|
||||
|
||||
/** Der Tag hinter „Stand“ in der Kopfzeile der HTML (TT.MM.JJJJ), sonst null. */
|
||||
const standIn = (html) => /<p class="meta">\s*Stand (\d{2}\.\d{2}\.\d{4})\s*<\/p>/.exec(html)?.[1] ?? null
|
||||
|
||||
/** Einstieg: deps = { fetch, schluessel, log, git, dateien } wie bei rechtliches-hochladen.mjs. @returns {Promise<number>} Exit-Code (0 oder 1) */
|
||||
export async function main(argv, deps = {}) {
|
||||
const log = deps.log ?? console.log
|
||||
let args
|
||||
try {
|
||||
args = parseArgs(argv)
|
||||
} catch (err) {
|
||||
log(err.message)
|
||||
return 1
|
||||
}
|
||||
try {
|
||||
const fetchFn = deps.fetch ?? globalThis.fetch
|
||||
const api = client(fetchFn, (deps.schluessel ?? (() => readFileSync(KEY_DATEI, 'utf8').trim()))())
|
||||
if (args.modus === 'probe') return await probe(api, log)
|
||||
const { freigabe } = args
|
||||
return await hochladen(api, log, {
|
||||
documentKey: DOKUMENT_KEY,
|
||||
dateien: DATEIEN,
|
||||
quelle: QUELLE,
|
||||
changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: ${AENDERUNG}`,
|
||||
freigabe,
|
||||
kartenHinweis: '(die Karte gehört zu WOMS „Rechtliches“; dieses Skript legt keine Karten an)',
|
||||
pruefeInhalt: (lies) => {
|
||||
const stand = standIn(lies(QUELLE).toString('utf8'))
|
||||
if (!stand) return `Abbruch: „Stand TT.MM.JJJJ“ fehlt in ${QUELLE}`
|
||||
if (stand !== ddmmjjjj(freigabe)) return `Abbruch: Stand ${stand} in ${QUELLE} passt nicht zur Freigabe ${ddmmjjjj(freigabe)}`
|
||||
return null
|
||||
},
|
||||
}, deps)
|
||||
} catch (err) {
|
||||
log(`Fehler: ${err.message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/** --probe: nur lesen. Eine fehlende Karte ist ein Stopp. */
|
||||
async function probe(api, log) {
|
||||
await api('GET', `/storage/buckets/${BUCKET}/files`)
|
||||
const karten = await alleDokumente(api, 'legalDocuments')
|
||||
log(`${DOKUMENT_KEY}: ${(await versionen(api, DOKUMENT_KEY)).map((v) => `${v.version}:${v.status}`).join(' ') || '—'}`)
|
||||
if (!karten.some((k) => k.key === DOKUMENT_KEY)) {
|
||||
log(`STOPP: Karte fehlt: ${DOKUMENT_KEY}`)
|
||||
return 1
|
||||
}
|
||||
log(`Schlüssel ok (files.read + documents.read), Karte ${DOKUMENT_KEY} vorhanden.`)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
||||
main(process.argv.slice(2)).then((code) => {
|
||||
process.exitCode = code
|
||||
})
|
||||
}
|
||||
@@ -23,8 +23,8 @@ import { FUNKTIONEN_UNTERLAGEN_V6 } from '../server/services/products.js'
|
||||
const ENDPOINT = 'https://ticket.webklar.com/v1'
|
||||
const PROJECT = '6a1058610003c5a13a05'
|
||||
const DB = 'woms-database'
|
||||
const BUCKET = 'legal-files'
|
||||
const KEY_DATEI = '/root/.appwrite-rechtliches.key'
|
||||
export const BUCKET = 'legal-files'
|
||||
export const KEY_DATEI = '/root/.appwrite-rechtliches.key'
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||||
const LIVE = 'https://project.webklar.com/'
|
||||
|
||||
@@ -81,7 +81,7 @@ export function dokumentFuer(documentKey) {
|
||||
|
||||
const MODI = { '--probe': 'probe', '--karten-anlegen': 'karten', '--pruefen': 'pruefen' }
|
||||
|
||||
const istKalendertag = (iso) => {
|
||||
export const istKalendertag = (iso) => {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return false
|
||||
const d = new Date(`${iso}T12:00:00Z`)
|
||||
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === iso
|
||||
@@ -165,7 +165,7 @@ const q = (o) => encodeURIComponent(JSON.stringify(o))
|
||||
const neueId = () => Array.from(crypto.getRandomValues(new Uint8Array(10)), (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 20)
|
||||
|
||||
/** Kleiner Appwrite-Client über deps.fetch; Fehler werden zu Error mit Methode, Pfad und Status. */
|
||||
function client(fetchFn, schluessel) {
|
||||
export function client(fetchFn, schluessel) {
|
||||
const kopf = { 'X-Appwrite-Project': PROJECT, 'X-Appwrite-Key': schluessel }
|
||||
return async function api(method, pfad, body) {
|
||||
const formular = typeof FormData !== 'undefined' && body instanceof FormData
|
||||
@@ -180,7 +180,7 @@ function client(fetchFn, schluessel) {
|
||||
}
|
||||
}
|
||||
|
||||
async function alleDokumente(api, sammlung) {
|
||||
export async function alleDokumente(api, sammlung) {
|
||||
const aus = []
|
||||
for (let offset = 0; ; offset += 100) {
|
||||
const r = await api('GET', `/databases/${DB}/collections/${sammlung}/documents?queries[]=${q({ method: 'limit', values: [100] })}&queries[]=${q({ method: 'offset', values: [offset] })}`)
|
||||
@@ -189,12 +189,12 @@ async function alleDokumente(api, sammlung) {
|
||||
}
|
||||
}
|
||||
|
||||
async function versionen(api, documentKey) {
|
||||
export async function versionen(api, documentKey) {
|
||||
const r = await api('GET', `/databases/${DB}/collections/legalVersions/documents?queries[]=${q({ method: 'equal', attribute: 'documentKey', values: [documentKey] })}&queries[]=${q({ method: 'limit', values: [100] })}`)
|
||||
return r.documents
|
||||
}
|
||||
|
||||
const ddmmjjjj = (iso) => iso.split('-').reverse().join('.')
|
||||
export const ddmmjjjj = (iso) => iso.split('-').reverse().join('.')
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- Aufruf
|
||||
|
||||
@@ -218,7 +218,14 @@ export async function main(argv, deps = {}) {
|
||||
if (args.modus === 'probe') return await probe(api, log)
|
||||
if (args.modus === 'karten') return await kartenAnlegen(api, log)
|
||||
if (args.modus === 'pruefen') return await pruefen(api, log)
|
||||
return await hochladen(api, log, args, deps)
|
||||
const d = dokumentFuer(args.documentKey)
|
||||
return await hochladen(api, log, {
|
||||
documentKey: args.documentKey,
|
||||
dateien: d.dateien,
|
||||
quelle: d.quelle,
|
||||
changeNote: `Fassung vom ${ddmmjjjj(args.freigabe)}: Kundenansicht neu (Fassung 6)`,
|
||||
freigabe: args.freigabe,
|
||||
}, deps)
|
||||
} catch (err) {
|
||||
log(`Fehler: ${err.message}`)
|
||||
return 1
|
||||
@@ -263,28 +270,42 @@ async function pruefen(api, log) {
|
||||
return fehlt.length > 0 ? 1 : 0
|
||||
}
|
||||
|
||||
/** <documentKey> --freigabe=…: genau ein Dokument hochladen. Als Erstes die Kartenprüfung, dann Git- und Dateiprüfung, erst dann schreiben. */
|
||||
async function hochladen(api, log, { documentKey, freigabe }, deps) {
|
||||
const d = dokumentFuer(documentKey)
|
||||
/**
|
||||
* Genau ein Dokument als neue Version hochladen (auch vom Datenschutz-Skript genutzt). Als Erstes die Kartenprüfung, dann Datei- und Git-Prüfung
|
||||
* und die Inhaltsprüfung des Aufrufers, erst dann schreiben: erst alle Dateien in den Bucket, dann das Versionsdokument, zuletzt die bisherige
|
||||
* gültige Version ablösen. deps = { git, dateien } wie bei main.
|
||||
* @param {object} api Client aus client()
|
||||
* @param {Function} log
|
||||
* @param {object} dok documentKey; dateien = [PDF, HTML] (Pfade im Projekt, in dieser Reihenfolge hochgeladen); quelle = Pfad in source
|
||||
* (kundenbereich@<Commit>:<quelle>); changeNote und freigabe (JJJJ-MM-TT = validFrom) fertig vom Aufrufer; kartenHinweis hängt an der Meldung
|
||||
* „Karte fehlt“; pruefeInhalt(lies) darf nach der Git-Prüfung mit lies(pfad) → Buffer den Inhalt prüfen und gibt eine Abbruchmeldung oder null zurück
|
||||
* @returns {Promise<number>} Exit-Code (0 oder 1)
|
||||
*/
|
||||
export async function hochladen(api, log, { documentKey, dateien, quelle, changeNote, freigabe, kartenHinweis = '(zuerst --karten-anlegen)', pruefeInhalt }, deps) {
|
||||
const karten = await alleDokumente(api, 'legalDocuments')
|
||||
if (!karten.some((k) => k.key === documentKey)) {
|
||||
log(`Karte fehlt: ${documentKey} (zuerst --karten-anlegen)`)
|
||||
log(`Karte fehlt: ${documentKey} ${kartenHinweis}`)
|
||||
return 1
|
||||
}
|
||||
const git = deps.git ?? ((args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }))
|
||||
const dateisystem = deps.dateien ?? { existsSync: (rel) => existsSync(path.join(ROOT, rel)), readFileSync: (rel) => readFileSync(path.join(ROOT, rel)) }
|
||||
for (const rel of d.dateien) {
|
||||
for (const rel of dateien) {
|
||||
if (!dateisystem.existsSync(rel)) {
|
||||
log(`Datei fehlt: ${rel}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if (git(['status', '--porcelain', '--', ...d.dateien]).trim() !== '') {
|
||||
log(`Abbruch: nicht committeter Stand in ${d.dateien.join(' und ')} (git status --porcelain ist nicht leer)`)
|
||||
if (git(['status', '--porcelain', '--', ...dateien]).trim() !== '') {
|
||||
log(`Abbruch: nicht committeter Stand in ${dateien.join(' und ')} (git status --porcelain ist nicht leer)`)
|
||||
return 1
|
||||
}
|
||||
const inhaltsfehler = pruefeInhalt?.((rel) => dateisystem.readFileSync(rel))
|
||||
if (inhaltsfehler) {
|
||||
log(inhaltsfehler)
|
||||
return 1
|
||||
}
|
||||
const commit = git(['rev-parse', '--short', 'HEAD']).trim()
|
||||
const source = `kundenbereich@${commit}:${d.quelle}`
|
||||
const source = `kundenbereich@${commit}:${quelle}`
|
||||
const alle = await versionen(api, documentKey)
|
||||
const plan = planVersion({ alle, source, validFrom: freigabe })
|
||||
if (plan.schon) {
|
||||
@@ -292,7 +313,7 @@ async function hochladen(api, log, { documentKey, freigabe }, deps) {
|
||||
return 0
|
||||
}
|
||||
const fileIds = []
|
||||
for (const rel of d.dateien) {
|
||||
for (const rel of dateien) {
|
||||
const form = new FormData()
|
||||
form.append('fileId', neueId())
|
||||
form.append('file', new Blob([dateisystem.readFileSync(rel)], { type: rel.endsWith('.pdf') ? 'application/pdf' : 'text/html' }), path.basename(rel))
|
||||
@@ -306,7 +327,7 @@ async function hochladen(api, log, { documentKey, freigabe }, deps) {
|
||||
validFrom: freigabe,
|
||||
validTo: null,
|
||||
status: 'current',
|
||||
changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: Kundenansicht neu (Fassung 6)`,
|
||||
changeNote,
|
||||
fileIds,
|
||||
source,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user