feat(supersync): add privacy policy address via env vars

For German legal compliance (Impressum), the privacy policy needs
to display the operator's address. This change:

- Rename privacy.html to privacy.template.html with placeholders
- Add PRIVACY_* env vars to .env.example
- Generate privacy.html at server startup from template
- Add PrivacyConfig to ServerConfig

If env vars are not set, placeholder text is shown.
This commit is contained in:
Johannes Millan 2025-12-28 15:38:09 +01:00
parent d18b11619b
commit 4d5c79f129
5 changed files with 97 additions and 3 deletions

View file

@ -40,6 +40,14 @@ SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM="SuperSync <your-email@gmail.com>"
# Privacy Policy - Address for German legal requirements (Impressum)
# These values are injected into privacy.html on server startup
PRIVACY_CONTACT_NAME=Your Name
PRIVACY_ADDRESS_STREET=Street 123
PRIVACY_ADDRESS_CITY=12345 City
PRIVACY_ADDRESS_COUNTRY=Germany
PRIVACY_CONTACT_EMAIL=contact@example.com
# GITHUB DEPLOY
GHCR_USER=uuuser
GHCR_TOKEN=TOOOOKEN

View file

@ -3,3 +3,6 @@ dist
data
./.env
.env
# Generated from privacy.template.html at server startup
public/privacy.html

View file

@ -50,6 +50,13 @@
.back-link:hover {
color: var(--primary);
}
address {
font-style: normal;
margin: 1rem 0;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.5rem;
}
</style>
</head>
<body>
@ -121,8 +128,24 @@
changes by posting the new Privacy Policy on this page.
</p>
<h2>8. Contact Us</h2>
<p>If you have any questions about this Privacy Policy, please contact us.</p>
<h2>8. Data Controller</h2>
<p>The data controller responsible for your personal data is:</p>
<address>
{{ PRIVACY_CONTACT_NAME }}<br />
{{ PRIVACY_ADDRESS_STREET }}<br />
{{ PRIVACY_ADDRESS_CITY }}<br />
{{ PRIVACY_ADDRESS_COUNTRY }}<br />
<br />
Email:
<a href="mailto:{{ PRIVACY_CONTACT_EMAIL }}">{{ PRIVACY_CONTACT_EMAIL }}</a>
</address>
<h2>9. Contact Us</h2>
<p>
If you have any questions about this Privacy Policy, please contact us at
<a href="mailto:{{ PRIVACY_CONTACT_EMAIL }}">{{ PRIVACY_CONTACT_EMAIL }}</a
>.
</p>
</div>
</body>
</html>

View file

@ -4,6 +4,14 @@ import { Logger } from './logger';
/** CORS origin can be a string or RegExp for pattern matching (e.g., localhost with any port) */
export type CorsOrigin = string | RegExp;
export interface PrivacyConfig {
contactName: string;
addressStreet: string;
addressCity: string;
addressCountry: string;
contactEmail: string;
}
export interface ServerConfig {
port: number;
dataDir: string;
@ -24,6 +32,11 @@ export interface ServerConfig {
pass?: string;
from: string;
};
/**
* Privacy policy contact information.
* Required for German legal compliance (Impressum).
*/
privacy?: PrivacyConfig;
/**
* Test mode configuration. When enabled, provides endpoints for E2E testing.
* NEVER enable in production!
@ -142,6 +155,17 @@ export const loadConfigFromEnv = (
};
}
// Privacy policy configuration (for German legal requirements)
if (process.env.PRIVACY_CONTACT_NAME) {
config.privacy = {
contactName: process.env.PRIVACY_CONTACT_NAME,
addressStreet: process.env.PRIVACY_ADDRESS_STREET || '',
addressCity: process.env.PRIVACY_ADDRESS_CITY || '',
addressCountry: process.env.PRIVACY_ADDRESS_COUNTRY || '',
contactEmail: process.env.PRIVACY_CONTACT_EMAIL || '',
};
}
// Test mode configuration
// Requires both TEST_MODE=true AND TEST_MODE_CONFIRM=yes-i-understand-the-risks
// This double-check prevents accidental test mode enablement

View file

@ -5,7 +5,7 @@ import rateLimit from '@fastify/rate-limit';
import helmet from '@fastify/helmet';
import fastifyStatic from '@fastify/static';
import * as path from 'path';
import { loadConfigFromEnv, ServerConfig } from './config';
import { loadConfigFromEnv, ServerConfig, PrivacyConfig } from './config';
import { Logger } from './logger';
import { prisma, disconnectDb } from './db';
import { apiRoutes } from './api';
@ -13,6 +13,39 @@ import { pageRoutes } from './pages';
import { syncRoutes, startCleanupJobs, stopCleanupJobs } from './sync';
import { testRoutes } from './test-routes';
const generatePrivacyHtml = (privacy?: PrivacyConfig): void => {
const publicDir = path.join(__dirname, '../../public');
const templatePath = path.join(publicDir, 'privacy.template.html');
const outputPath = path.join(publicDir, 'privacy.html');
if (!fs.existsSync(templatePath)) {
Logger.warn('privacy.template.html not found, skipping generation');
return;
}
let template = fs.readFileSync(templatePath, 'utf-8');
// Replace placeholders with values from config (allow optional whitespace)
template = template
.replace(
/\{\{\s*PRIVACY_CONTACT_NAME\s*\}\}/g,
privacy?.contactName || '[Contact Name]',
)
.replace(
/\{\{\s*PRIVACY_ADDRESS_STREET\s*\}\}/g,
privacy?.addressStreet || '[Street Address]',
)
.replace(/\{\{\s*PRIVACY_ADDRESS_CITY\s*\}\}/g, privacy?.addressCity || '[City]')
.replace(
/\{\{\s*PRIVACY_ADDRESS_COUNTRY\s*\}\}/g,
privacy?.addressCountry || '[Country]',
)
.replace(/\{\{\s*PRIVACY_CONTACT_EMAIL\s*\}\}/g, privacy?.contactEmail || '[Email]');
fs.writeFileSync(outputPath, template);
Logger.info('Generated privacy.html from template');
};
export { ServerConfig, loadConfigFromEnv };
export const createServer = (
@ -30,6 +63,9 @@ export const createServer = (
Logger.info(`Created data directory: ${fullConfig.dataDir}`);
}
// Generate privacy.html from template with env vars
generatePrivacyHtml(fullConfig.privacy);
let fastifyServer: FastifyInstance | undefined;
return {