feat(legal): host the privacy policy and EULA at /privacy/ and /eula/
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 40s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 40s
Author ui/legal/{privacy,eula}_ru.md, reworked from the source documents: the seller INN is unified (290210610742), contacts unified to the Telegram bot + email + postal address, the EULA governing-law clause leads with Russian law + jurisdiction as residence tiers, the single-binding-language clause is dropped, data collection is scoped to voluntary/support provision, and "Компания" is no longer shout-cased.
Serve both as static pages through the render sidecar, reusing a shared renderLegalHtml generalised from renderOfferHtml: new GET /privacy/ and GET /eula/ (301 from the slashless form), the markdown baked into the image, no backend fetch. Route them at the edge via the @legal caddy matcher with a CI probe asserting each page's canonical URL + the seller INN, so a missing route cannot silently fall through to the landing. Add the two links to the landing footer.
Docs baked in: ARCHITECTURE (renderer legal pages + edge routing), FUNCTIONAL (+_ru) footer, renderer/README routes. Tests: renderer legal.test.mjs, ui renderLegalHtml unit, landing footer e2e.
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
// browser build unit-tests — so the sidecar runs them on the server. Bundled by `pnpm run bundle`
|
||||
// into dist/gameimage.mjs (type erasure only; the ui project type-checks the source):
|
||||
// - drawGameImage + setAlphabet: the finished-game PNG export (POST /render).
|
||||
// - renderOfferHtml: the public-offer page (GET /offer/) — the same renderer the landing build
|
||||
// used to invoke, now server-side so the live catalog price list can be spliced in.
|
||||
// - renderOfferHtml / renderLegalHtml: the public legal pages — GET /offer/ (with the live
|
||||
// catalog price list spliced in), GET /privacy/ and GET /eula/ (static) — server-side so one
|
||||
// renderer serves them all.
|
||||
export { drawGameImage, type RenderOptions } from '../../ui/src/lib/gameimage';
|
||||
export { setAlphabet } from '../../ui/src/lib/alphabet';
|
||||
export { renderOfferHtml } from '../../ui/src/lib/offer';
|
||||
export { renderOfferHtml, renderLegalHtml } from '../../ui/src/lib/offer';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// The public legal pages (GET /privacy/, GET /eula/): the owner-edited markdown under ui/legal/,
|
||||
// rendered by the SAME shared renderLegalHtml the offer uses (bundled from ui/src/lib/offer). Unlike
|
||||
// the offer, these carry no dynamic data — no backend fetch, no marker splice. Kept out of server.mjs
|
||||
// so the per-page title/canonical presets are unit-testable without HTTP (test/legal.test.mjs).
|
||||
import { renderLegalHtml } from '../dist/gameimage.mjs';
|
||||
|
||||
// renderPrivacy renders the privacy-policy markdown as the standalone /privacy/ page.
|
||||
export function renderPrivacy(markdown) {
|
||||
return renderLegalHtml(markdown, {
|
||||
title: 'Политика конфиденциальности — Эрудит',
|
||||
canonical: 'https://erudit-game.ru/privacy/',
|
||||
});
|
||||
}
|
||||
|
||||
// renderEula renders the end-user licence agreement markdown as the standalone /eula/ page.
|
||||
export function renderEula(markdown) {
|
||||
return renderLegalHtml(markdown, {
|
||||
title: 'Пользовательское соглашение — Эрудит',
|
||||
canonical: 'https://erudit-game.ru/eula/',
|
||||
});
|
||||
}
|
||||
+31
-7
@@ -2,19 +2,23 @@
|
||||
// renderers (bundled from ui/src/lib at image build time). It serves two surfaces:
|
||||
//
|
||||
// POST /render {game, moves, alphabet, labels, dateLocale, hostname, scale?} → image/png
|
||||
// GET /offer/ → text/html (the public offer page: ui/legal/offer_ru.md with the live catalog
|
||||
// price list spliced in — the only edge-exposed route; caddy routes /offer/ here)
|
||||
// GET /offer → 301 /offer/
|
||||
// GET /healthz → 200
|
||||
// GET /offer/ → text/html (the public offer page: ui/legal/offer_ru.md with the live catalog
|
||||
// price list spliced in; caddy routes /offer/ here)
|
||||
// GET /privacy/ → text/html (the privacy policy: ui/legal/privacy_ru.md, static)
|
||||
// GET /eula/ → text/html (the EULA: ui/legal/eula_ru.md, static)
|
||||
// GET /offer, /privacy, /eula → 301 to the trailing-slash form
|
||||
// GET /healthz → 200
|
||||
//
|
||||
// /render is internal-only (the backend calls it; authentication, participant checks and the signed
|
||||
// public URL live in the backend). /offer/ is reachable from the edge but read-only and unprivileged
|
||||
// — it fetches the price list from the backend's internal endpoint and renders the committed offer
|
||||
// markdown; no user input reaches it.
|
||||
// public URL live in the backend). The legal pages (/offer/, /privacy/, /eula/) are reachable from the
|
||||
// edge but read-only and unprivileged: /offer/ splices the price list fetched from the backend's
|
||||
// internal endpoint into the committed offer markdown; /privacy/ and /eula/ are static committed
|
||||
// markdown. No user input reaches any of them.
|
||||
import { createServer } from 'node:http';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { renderRequest } from './render.mjs';
|
||||
import { renderOffer } from './offer.mjs';
|
||||
import { renderPrivacy, renderEula } from './legal.mjs';
|
||||
|
||||
const PORT = Number(process.env.RENDERER_PORT || 8090);
|
||||
// A render request is a finished game's journal — generously capped.
|
||||
@@ -25,6 +29,10 @@ const MAX_BODY = 2 * 1024 * 1024;
|
||||
// allow-list) and serves the tables from its in-memory cache. RENDERER_OFFER_MD overrides the baked
|
||||
// path for a local run (point it at ../ui/legal/offer_ru.md).
|
||||
const OFFER_MD = readFileSync(process.env.RENDERER_OFFER_MD || new URL('../legal/offer_ru.md', import.meta.url), 'utf8');
|
||||
// The privacy policy (GET /privacy/) and EULA (GET /eula/): static owner-edited markdown, read once at
|
||||
// boot, no dynamic data. RENDERER_PRIVACY_MD / RENDERER_EULA_MD override the baked path for a local run.
|
||||
const PRIVACY_MD = readFileSync(process.env.RENDERER_PRIVACY_MD || new URL('../legal/privacy_ru.md', import.meta.url), 'utf8');
|
||||
const EULA_MD = readFileSync(process.env.RENDERER_EULA_MD || new URL('../legal/eula_ru.md', import.meta.url), 'utf8');
|
||||
const OFFER_PRICING_URL =
|
||||
(process.env.RENDERER_BACKEND_URL || 'http://backend:8080') + '/api/v1/internal/offer/pricing';
|
||||
|
||||
@@ -74,6 +82,22 @@ const server = createServer(async (req, res) => {
|
||||
.end(html);
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && (path === '/privacy' || path === '/eula')) {
|
||||
res.writeHead(301, { location: path + '/' }).end();
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && path === '/privacy/') {
|
||||
res
|
||||
.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' })
|
||||
.end(renderPrivacy(PRIVACY_MD));
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && path === '/eula/') {
|
||||
res
|
||||
.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' })
|
||||
.end(renderEula(EULA_MD));
|
||||
return;
|
||||
}
|
||||
if (req.method === 'POST' && path === '/render') {
|
||||
const png = await renderRequest(JSON.parse(await readBody(req)));
|
||||
res.writeHead(200, { 'content-type': 'image/png', 'content-length': png.length }).end(png);
|
||||
|
||||
Reference in New Issue
Block a user