Prepare for going public: legal pages, corrections contact, portrait proxy
/impressum and /datenschutz read the operator's details from the environment so they stay out of this repository, and show a visible notice until set. Portraits go through an allowlisted /api/img so visitors' IPs stay with us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd44d8fbba
commit
256906e9fe
12 files changed
+256
-12
No files matched your search
@@ -12,6 +12,13 @@ POLITRACK_READER_PASSWORD=change-me-too
|
||||
POLITRACK_USER_AGENT=PolitrackBot/0.1 (+https://example.org/about; contact: you@example.org)
|
||||
POLITRACK_MIN_REQUEST_INTERVAL=2.0
|
||||
|
||||
# Shown on /impressum and /datenschutz. Required before the site goes public (§ 5 ECG, §§ 24, 25 MedienG).
|
||||
# POLITRACK_LEGAL_NAME=
|
||||
# POLITRACK_LEGAL_ADDRESS=
|
||||
# POLITRACK_LEGAL_EMAIL=
|
||||
# Defaults to POLITRACK_LEGAL_EMAIL.
|
||||
# POLITRACK_CORRECTIONS_EMAIL=
|
||||
|
||||
# Legislative periods of the Austrian parliament to ingest in detail (JSON list).
|
||||
POLITRACK_PARLAMENT_PERIODS=["XXVII","XXVIII"]
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
API_INTERNAL_URL: http://api:8000
|
||||
POLITRACK_USER_AGENT: ${POLITRACK_USER_AGENT:-}
|
||||
POLITRACK_LEGAL_NAME: ${POLITRACK_LEGAL_NAME:-}
|
||||
POLITRACK_LEGAL_ADDRESS: ${POLITRACK_LEGAL_ADDRESS:-}
|
||||
POLITRACK_LEGAL_EMAIL: ${POLITRACK_LEGAL_EMAIL:-}
|
||||
POLITRACK_CORRECTIONS_EMAIL: ${POLITRACK_CORRECTIONS_EMAIL:-}
|
||||
ports:
|
||||
- "${WEB_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
|
||||
+3
-2
@@ -33,6 +33,7 @@ Next.js frontend (de/en), Docker Compose for Unraid.
|
||||
party/institution channels; curated `social_accounts.yaml`
|
||||
- [x] X deletion sync (`social.x_deletions`): posts gone from X keep a marked record without their text
|
||||
- [x] EP final roll-call votes of Austrian MEPs (`europarl.votes`)
|
||||
- [x] Draft legal pages (`/impressum`, `/datenschutz`), corrections contact, portrait proxy (`/api/img`)
|
||||
- [x] Pages: organisations, money explorer, social feed, Wikipedia edits, person network ("follow the money"),
|
||||
say-vs-vote on roll calls
|
||||
|
||||
@@ -59,8 +60,8 @@ Next.js frontend (de/en), Docker Compose for Unraid.
|
||||
- [ ] Wayback Machine archiving of evidence URLs (`document.archive_url`)
|
||||
|
||||
**Product**
|
||||
- [ ] Before going public: Impressum/Offenlegung (§ 5 ECG, § 25 MedienG), privacy policy, a corrections contact
|
||||
- [ ] Image proxy for portraits and logos (no visitor IPs to Wikimedia/parliament)
|
||||
- [ ] Before going public: have `/impressum` and `/datenschutz` legally reviewed, set `POLITRACK_LEGAL_*` in `.env`,
|
||||
and document the balancing test (see legal.md)
|
||||
- [ ] Compare two people or parties; topic pages (`/themen/{slug}`); Open Graph share images; CSV export
|
||||
- [ ] Review queue for editors in the web UI (today `politrack review`)
|
||||
- [ ] Community link submissions and a corrections/right-of-reply form
|
||||
|
||||
+40
-2
@@ -16,7 +16,44 @@
|
||||
"organizations": "Organisationen",
|
||||
"money": "Geld",
|
||||
"social": "Social Media",
|
||||
"wikipedia": "Wikipedia-Bearbeitungen"
|
||||
"wikipedia": "Wikipedia-Bearbeitungen",
|
||||
"imprint": "Impressum",
|
||||
"privacy": "Datenschutz"
|
||||
},
|
||||
"Imprint": {
|
||||
"title": "Impressum",
|
||||
"intro": "Offenlegung gemäß § 5 ECG und §§ 24, 25 MedienG.",
|
||||
"notConfigured": "Die Angaben zum Medieninhaber sind noch nicht eingetragen. Vor einer Veröffentlichung müssen sie vollständig sein.",
|
||||
"missing": "[noch nicht eingetragen]",
|
||||
"ownerHeading": "Medieninhaber und Herausgeber",
|
||||
"email": "E-Mail",
|
||||
"purposeHeading": "Unternehmensgegenstand",
|
||||
"purpose": "Keiner. Politrack ist ein privates, nicht gewinnorientiertes Projekt ohne Werbung.",
|
||||
"directionHeading": "Grundlegende Richtung",
|
||||
"direction": "Information über die öffentliche Tätigkeit österreichischer Politikerinnen, Politiker und Parteien auf Grundlage öffentlich zugänglicher Quellen, unabhängig von Parteien und Interessengruppen.",
|
||||
"correctionsHeading": "Korrekturen und Gegendarstellung",
|
||||
"corrections": "Eine Angabe ist falsch oder unvollständig? Schreib uns bitte die Nummer der Angabe und eine Quelle. Widersprochene Angaben markieren wir als umstritten, falsche korrigieren wir.",
|
||||
"liabilityHeading": "Haftung",
|
||||
"liability": "Alle Angaben stammen aus den jeweils verlinkten Quellen; maßgeblich sind diese. Für die Inhalte verlinkter Seiten sind deren Betreiber verantwortlich."
|
||||
},
|
||||
"Privacy": {
|
||||
"title": "Datenschutz",
|
||||
"intro": "Welche Daten Politrack verarbeitet und welche Rechte du hast.",
|
||||
"draft": "Entwurf: Dieser Text wurde noch nicht rechtlich geprüft.",
|
||||
"missing": "[noch nicht eingetragen]",
|
||||
"controllerHeading": "Verantwortlicher",
|
||||
"visitorsHeading": "Wenn du diese Website besuchst",
|
||||
"visitors": "Politrack verwendet keine Werbung, kein Tracking und keine Skripte, Schriften oder Einbettungen von Dritten. Beim Abruf fallen technisch notwendige Zugriffsdaten an (IP-Adresse, Zeitpunkt, aufgerufene Seite), die nur so lange gespeichert werden, wie es für den sicheren Betrieb nötig ist.",
|
||||
"cookiesHeading": "Cookies",
|
||||
"cookies": "Wir speichern nur deine eigenen Einstellungen in Cookies: Quellen- und Themenfilter sowie das Farbschema. Sie dienen keiner Nachverfolgung.",
|
||||
"imagesHeading": "Porträts",
|
||||
"images": "Porträts laden wir über unseren eigenen Server, sodass deine IP-Adresse nicht an das Parlament oder andere Quellen übermittelt wird. Links zu externen Quellen öffnest du selbst; dort gelten die Bestimmungen der jeweiligen Betreiber.",
|
||||
"subjectsHeading": "Daten über Politikerinnen und Politiker",
|
||||
"subjects": "Politrack verarbeitet personenbezogene Daten ausschließlich zur öffentlichen Rolle von Personen: Funktionen, Abstimmungen, öffentliche Aussagen und Auftritte, gemeldete Nebentätigkeiten, Parteispenden und öffentlich bekannte Verfahren. Private Spender werden nur so genannt, wie es die amtliche Veröffentlichung vorsieht.",
|
||||
"basisHeading": "Rechtsgrundlage",
|
||||
"basis": "Berechtigtes Interesse und öffentliches Interesse an politischer Transparenz (Art. 6 Abs. 1 lit. f DSGVO) sowie die Freiheit der Meinungsäußerung und Information (Art. 85 DSGVO).",
|
||||
"rightsHeading": "Deine Rechte",
|
||||
"rights": "Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung und Widerspruch. Wende dich dazu an die oben genannte Adresse. Außerdem kannst du dich bei der Österreichischen Datenschutzbehörde beschweren."
|
||||
},
|
||||
"Home": {
|
||||
"headline": "Politik in Österreich – nachvollziehbar.",
|
||||
@@ -336,7 +373,8 @@
|
||||
"kind": "Art",
|
||||
"notCollected": "Wir umgehen keine Bezahlschranken, Logins oder technischen Sperren und halten uns an robots.txt und Nutzungsbedingungen. Von Medien speichern wir nur Überschrift, Teaser und Link.",
|
||||
"data": "Die Betreiber:innen dieser Instanz stellen die Daten auch maschinenlesbar bereit: als REST-API mit OpenAPI-Dokumentation, als MCP-Server für KI-Agenten und als Lesezugriff auf die Datenbank. Jede Angabe kommt dort mit denselben Belegen wie hier.",
|
||||
"corrections": "Fehler gefunden? Wir korrigieren falsche Angaben, markieren widersprochene als umstritten und löschen nichts stillschweigend. Bitte nenne die Nummer der Angabe (siehe Permalink) und eine Quelle für die Korrektur."
|
||||
"corrections": "Fehler gefunden? Wir korrigieren falsche Angaben, markieren widersprochene als umstritten und löschen nichts stillschweigend. Bitte nenne die Nummer der Angabe (siehe Permalink) und eine Quelle für die Korrektur.",
|
||||
"correctionsContact": "Kontakt für Korrekturen"
|
||||
},
|
||||
"Common": {
|
||||
"loadError": "Die Daten konnten nicht geladen werden. Bitte versuche es gleich noch einmal.",
|
||||
|
||||
+40
-2
@@ -16,7 +16,9 @@
|
||||
"organizations": "Organisations",
|
||||
"money": "Money",
|
||||
"social": "Social media",
|
||||
"wikipedia": "Wikipedia edits"
|
||||
"wikipedia": "Wikipedia edits",
|
||||
"imprint": "Legal notice",
|
||||
"privacy": "Privacy"
|
||||
},
|
||||
"Home": {
|
||||
"headline": "Austrian politics – traceable.",
|
||||
@@ -336,7 +338,8 @@
|
||||
"kind": "Kind",
|
||||
"notCollected": "We don't get around paywalls, logins or technical barriers, and we respect robots.txt and terms of use. From media we store only the headline, teaser and link.",
|
||||
"data": "The operators of this instance also provide the data in machine-readable form: a REST API with OpenAPI documentation, an MCP server for AI agents and read access to the database. Every fact comes with the same sources as here.",
|
||||
"corrections": "Found a mistake? We correct wrong facts, mark contradicted ones as disputed and never delete anything silently. Please quote the fact's number (see its permalink) and a source for the correction."
|
||||
"corrections": "Found a mistake? We correct wrong facts, mark contradicted ones as disputed and never delete anything silently. Please quote the fact's number (see its permalink) and a source for the correction.",
|
||||
"correctionsContact": "Contact for corrections"
|
||||
},
|
||||
"Common": {
|
||||
"loadError": "The data could not be loaded. Please try again in a moment.",
|
||||
@@ -586,6 +589,41 @@
|
||||
"about": "About Politrack"
|
||||
}
|
||||
},
|
||||
"Imprint": {
|
||||
"title": "Legal notice",
|
||||
"intro": "Disclosure under § 5 ECG and §§ 24, 25 MedienG (Austrian law).",
|
||||
"notConfigured": "The publisher's details have not been entered yet. They must be complete before publication.",
|
||||
"missing": "[not entered yet]",
|
||||
"ownerHeading": "Media owner and publisher",
|
||||
"email": "Email",
|
||||
"purposeHeading": "Business purpose",
|
||||
"purpose": "None. Politrack is a private, non-profit project without advertising.",
|
||||
"directionHeading": "Editorial line",
|
||||
"direction": "Information on the public activity of Austrian politicians and parties, based on publicly available sources and independent of parties and interest groups.",
|
||||
"correctionsHeading": "Corrections and right of reply",
|
||||
"corrections": "Is a fact wrong or incomplete? Please send us its number and a source. We mark contradicted facts as disputed and correct wrong ones.",
|
||||
"liabilityHeading": "Liability",
|
||||
"liability": "All facts come from the sources linked with them, which are authoritative. Linked sites are the responsibility of their operators."
|
||||
},
|
||||
"Privacy": {
|
||||
"title": "Privacy",
|
||||
"intro": "What data Politrack processes and what rights you have.",
|
||||
"draft": "Draft: this text has not been legally reviewed yet.",
|
||||
"missing": "[not entered yet]",
|
||||
"controllerHeading": "Controller",
|
||||
"visitorsHeading": "When you visit this website",
|
||||
"visitors": "Politrack uses no advertising, no tracking and no third-party scripts, fonts or embeds. Serving a page produces technically necessary access data (IP address, time, page requested), kept only as long as needed to run the site securely.",
|
||||
"cookiesHeading": "Cookies",
|
||||
"cookies": "Cookies only store your own settings: source and topic filters and the colour scheme. They are not used for tracking.",
|
||||
"imagesHeading": "Portraits",
|
||||
"images": "Portraits are loaded through our own server, so your IP address is not passed to the parliament or other sources. External source links are yours to open; the operators' own terms apply there.",
|
||||
"subjectsHeading": "Data about politicians",
|
||||
"subjects": "Politrack processes personal data only about people's public role: offices, votes, public statements and appearances, declared side jobs, party donations and publicly known proceedings. Private donors are named only as the official publication names them.",
|
||||
"basisHeading": "Legal basis",
|
||||
"basis": "Legitimate and public interest in political transparency (Art. 6(1)(f) GDPR) and freedom of expression and information (Art. 85 GDPR).",
|
||||
"rightsHeading": "Your rights",
|
||||
"rights": "You have the right to access, rectification, erasure, restriction and objection. Please contact the address above. You can also complain to the Austrian Data Protection Authority."
|
||||
},
|
||||
"Footer": {
|
||||
"tagline": "Austrian politics, sourced. An independent project that collects public information on politicians and parties – every fact with its source.",
|
||||
"principleEvidence": "Every fact with its source",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type {Metadata} from 'next';
|
||||
import {getTranslations, setRequestLocale} from 'next-intl/server';
|
||||
import {Notice} from '@/components/Notice';
|
||||
import {PageHeader} from '@/components/PageHeader';
|
||||
import {legalDetails} from '@/lib/legal';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // the operator's details are read from the environment at request time
|
||||
|
||||
const SECTIONS = ['visitors', 'cookies', 'images', 'subjects', 'basis', 'rights'] as const;
|
||||
|
||||
export async function generateMetadata({params}: {params: Promise<{locale: string}>}): Promise<Metadata> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Privacy'});
|
||||
return {title: t('title')};
|
||||
}
|
||||
|
||||
export default async function PrivacyPage({params}: {params: Promise<{locale: string}>}) {
|
||||
const {locale} = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('Privacy');
|
||||
const {name, address, email} = legalDetails();
|
||||
|
||||
return (
|
||||
<article>
|
||||
<PageHeader title={t('title')} lede={t('intro')} />
|
||||
<div className="prose">
|
||||
<Notice tone="caution">{t('draft')}</Notice>
|
||||
<section>
|
||||
<h2>{t('controllerHeading')}</h2>
|
||||
<p>{name ?? t('missing')}<br />{address ?? t('missing')}</p>
|
||||
<p>{email ? <a href={`mailto:${email}`}>{email}</a> : t('missing')}</p>
|
||||
</section>
|
||||
{SECTIONS.map((key) => (
|
||||
<section key={key}>
|
||||
<h2>{t(`${key}Heading`)}</h2>
|
||||
<p>{t(key)}</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type {Metadata} from 'next';
|
||||
import {getTranslations, setRequestLocale} from 'next-intl/server';
|
||||
import {Notice} from '@/components/Notice';
|
||||
import {PageHeader} from '@/components/PageHeader';
|
||||
import {legalDetails} from '@/lib/legal';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // the operator's details are read from the environment at request time
|
||||
|
||||
export async function generateMetadata({params}: {params: Promise<{locale: string}>}): Promise<Metadata> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Imprint'});
|
||||
return {title: t('title')};
|
||||
}
|
||||
|
||||
export default async function ImprintPage({params}: {params: Promise<{locale: string}>}) {
|
||||
const {locale} = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('Imprint');
|
||||
const {name, address, email, correctionsEmail} = legalDetails();
|
||||
const missing = t('missing');
|
||||
|
||||
return (
|
||||
<article>
|
||||
<PageHeader title={t('title')} lede={t('intro')} />
|
||||
<div className="prose">
|
||||
{(!name || !address || !email) && <Notice tone="caution">{t('notConfigured')}</Notice>}
|
||||
<section>
|
||||
<h2>{t('ownerHeading')}</h2>
|
||||
<p>{name ?? missing}<br />{address ?? missing}</p>
|
||||
<p>{t('email')}: {email ? <a href={`mailto:${email}`}>{email}</a> : missing}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>{t('purposeHeading')}</h2>
|
||||
<p>{t('purpose')}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>{t('directionHeading')}</h2>
|
||||
<p>{t('direction')}</p>
|
||||
</section>
|
||||
<section id="korrekturen">
|
||||
<h2>{t('correctionsHeading')}</h2>
|
||||
<p>{t('corrections')}</p>
|
||||
<p>{correctionsEmail ? <a href={`mailto:${correctionsEmail}`}>{correctionsEmail}</a> : missing}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>{t('liabilityHeading')}</h2>
|
||||
<p>{t('liability')}</p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {SourceMeter, StatusBadge} from '@/components/badges';
|
||||
import {ExternalLink} from '@/components/ExternalLink';
|
||||
import {Notice} from '@/components/Notice';
|
||||
import {PageHeader} from '@/components/PageHeader';
|
||||
import {Link} from '@/i18n/navigation';
|
||||
import {api, type LegalStage} from '@/lib/api';
|
||||
|
||||
const LEGAL_STAGES: LegalStage[] = ['allegation_reported', 'investigation', 'immunity_waiver_requested', 'immunity_waived',
|
||||
@@ -125,7 +126,7 @@ export default async function MethodPage({params}: {params: Promise<{locale: str
|
||||
</section>
|
||||
<section id={ANCHORS.corrections}>
|
||||
<h2>{t('section.corrections')}</h2>
|
||||
<p>{t('corrections')}</p>
|
||||
<p>{t('corrections')} <Link href="/impressum#korrekturen">{t('correctionsContact')}</Link></p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Image proxy for portraits, so that visitors' IP addresses don't reach the publishers.
|
||||
// Only raster images from the hosts below: anything else would turn this into an open proxy.
|
||||
import type {NextRequest} from 'next/server';
|
||||
|
||||
const ALLOWED_HOSTS = new Set(['www.parlament.gv.at', 'www.europarl.europa.eu']);
|
||||
const IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
||||
const MAX_BYTES = 5 * 1024 * 1024;
|
||||
const MAX_REDIRECTS = 3;
|
||||
const WEEK = 7 * 24 * 60 * 60;
|
||||
|
||||
function allowedUrl(raw: string | null): URL | null {
|
||||
try {
|
||||
const url = new URL(raw ?? '');
|
||||
return url.protocol === 'https:' && ALLOWED_HOSTS.has(url.hostname) ? url : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
let url = allowedUrl(request.nextUrl.searchParams.get('u'));
|
||||
if (!url) return new Response(null, {status: 400});
|
||||
const headers = {'User-Agent': process.env.POLITRACK_USER_AGENT ?? 'PolitrackBot/0.1'};
|
||||
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(url, {headers, redirect: 'manual', signal: AbortSignal.timeout(10_000), next: {revalidate: WEEK}});
|
||||
} catch {
|
||||
return new Response(null, {status: 502});
|
||||
}
|
||||
if (upstream.status >= 300 && upstream.status < 400) {
|
||||
url = allowedUrl(new URL(upstream.headers.get('location') ?? '', url).href);
|
||||
if (!url) return new Response(null, {status: 502});
|
||||
continue;
|
||||
}
|
||||
if (!upstream.ok) return new Response(null, {status: 404});
|
||||
const type = (upstream.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
|
||||
if (!IMAGE_TYPES.has(type)) return new Response(null, {status: 415});
|
||||
const body = await upstream.arrayBuffer();
|
||||
if (body.byteLength > MAX_BYTES) return new Response(null, {status: 413});
|
||||
return new Response(body, {
|
||||
headers: {'Content-Type': type, 'Cache-Control': `public, max-age=${WEEK}`, 'X-Content-Type-Options': 'nosniff'}
|
||||
});
|
||||
}
|
||||
return new Response(null, {status: 502});
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import {initials} from '@/lib/format';
|
||||
|
||||
/**
|
||||
* Portrait or initials. Portraits come from Wikimedia/parliament: loaded lazily and without a referrer.
|
||||
* Decorative (alt=""), because the name is always printed next to it.
|
||||
* Portrait or initials. Portraits go through our own image proxy (app/api/img), so that visitors' IP addresses
|
||||
* don't reach third parties. Decorative (alt=""), because the name is always printed next to it.
|
||||
*/
|
||||
export function Avatar({name, src, size = 'md'}: {name: string; src?: string | null; size?: 'md' | 'lg'}) {
|
||||
const className = size === 'lg' ? 'avatar avatar--lg' : 'avatar';
|
||||
if (src) {
|
||||
return <img src={src} alt="" className={className} loading="lazy" decoding="async" referrerPolicy="no-referrer" />;
|
||||
return <img src={`/api/img?u=${encodeURIComponent(src)}`} alt="" className={className} loading="lazy" decoding="async" />;
|
||||
}
|
||||
return <span className={className} aria-hidden="true">{initials(name)}</span>;
|
||||
}
|
||||
@@ -16,7 +16,9 @@ const ITEMS = {
|
||||
former: {href: '/ehemalige', key: 'former', icon: 'history'},
|
||||
method: {href: '/methodik', key: 'method', icon: 'book'},
|
||||
settings: {href: '/einstellungen', key: 'settings', icon: 'sliders'},
|
||||
search: {href: '/suche', key: 'search', icon: 'search'}
|
||||
search: {href: '/suche', key: 'search', icon: 'search'},
|
||||
imprint: {href: '/impressum', key: 'imprint', icon: 'document'},
|
||||
privacy: {href: '/datenschutz', key: 'privacy', icon: 'shield'}
|
||||
} satisfies Record<string, NavItem>;
|
||||
|
||||
export const PRIMARY_NAV: NavItem[] = [ITEMS.persons, ITEMS.parties, ITEMS.votes, ITEMS.money, ITEMS.organizations, ITEMS.calendar];
|
||||
@@ -26,5 +28,5 @@ export const NAV_GROUPS: {key: string; items: NavItem[]}[] = [
|
||||
{key: 'people', items: [ITEMS.persons, ITEMS.parties, ITEMS.former]},
|
||||
{key: 'decisions', items: [ITEMS.votes, ITEMS.calendar, ITEMS.social]},
|
||||
{key: 'influence', items: [ITEMS.money, ITEMS.organizations, ITEMS.wikipedia]},
|
||||
{key: 'about', items: [ITEMS.method, ITEMS.settings, ITEMS.search]}
|
||||
{key: 'about', items: [ITEMS.method, ITEMS.settings, ITEMS.search, ITEMS.imprint, ITEMS.privacy]}
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
// The operator's details for the legal pages. They come from the environment (.env on the server), so the
|
||||
// operator's name and address stay out of this public repository. Read at request time, not at build time.
|
||||
export function legalDetails() {
|
||||
const email = process.env.POLITRACK_LEGAL_EMAIL || null;
|
||||
return {
|
||||
name: process.env.POLITRACK_LEGAL_NAME || null,
|
||||
address: process.env.POLITRACK_LEGAL_ADDRESS || null,
|
||||
email,
|
||||
correctionsEmail: process.env.POLITRACK_CORRECTIONS_EMAIL || email
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user