@press2ai/engine 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ceidg-vertical.ts +33 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@press2ai/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Multi-tenant runtime + template contracts dla otwarty-* verticali. SSR na Cloudflare Workers, isomorphic renderer (SSG/browser w roadmapie).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/ceidg-vertical.ts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* i — fix względem starego trener-landinga — `cities` query też filtruje
|
|
23
23
|
* po kategorii (wcześniej trener pokazywał miasta z całej tabeli leads,
|
|
24
24
|
* mieszając wszystkie wertykale).
|
|
25
|
-
* - Opt-out jest **globalny**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* - Opt-out jest **globalny** i **hard delete**: DELETE FROM leads + wpis
|
|
26
|
+
* w `opted_out_nips` (blacklist NIP-ów żeby sync nie wprowadził z powrotem).
|
|
27
|
+
* RODO art. 17 — usunięcie danych osobowych, nie soft flag. Z karty profilu
|
|
28
|
+
* przez slug (jeden klik), bez formularza NIP.
|
|
28
29
|
*
|
|
29
30
|
* Hono jest peer dependency (każdy wertykal i tak go ma). Engine core nie
|
|
30
31
|
* importuje Hono — tylko ten subpath go używa.
|
|
@@ -53,7 +54,6 @@ export type CeidgLead = {
|
|
|
53
54
|
claimed: number;
|
|
54
55
|
external_site_url: string | null;
|
|
55
56
|
fetched_at: number;
|
|
56
|
-
opted_out_at: number | null;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
export type CeidgBindings = { DB: D1Database; CACHE: KVNamespace };
|
|
@@ -108,17 +108,15 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
108
108
|
// ─────────── SQL ───────────
|
|
109
109
|
// EXISTS subselect zamiast JOIN: lead z N PKDs nie multiplikuje wierszy.
|
|
110
110
|
// (SELECT pkd ... LIMIT 1) wybiera jeden PKD per lead — wystarczy do
|
|
111
|
-
// pkdToJobTitle.
|
|
112
|
-
//
|
|
111
|
+
// pkdToJobTitle. Świadome uproszczenie: lead z mieszanymi PKDs dostanie
|
|
112
|
+
// jeden label. Filtr opted-out zbędny — opt-out to hard delete (0010).
|
|
113
113
|
const leadsQuery = `
|
|
114
114
|
SELECT l.nip, l.first_name, l.last_name, c.name as city,
|
|
115
115
|
l.company_name, l.slug, l.claimed, l.external_site_url, l.fetched_at,
|
|
116
|
-
l.opted_out_at,
|
|
117
116
|
(SELECT pkd FROM lead_pkd WHERE nip = l.nip LIMIT 1) as pkd
|
|
118
117
|
FROM leads l
|
|
119
118
|
LEFT JOIN cities c ON l.city_id = c.id
|
|
120
|
-
WHERE l.
|
|
121
|
-
AND EXISTS (SELECT 1 FROM lead_categories WHERE lead_nip = l.nip AND category = ?)`;
|
|
119
|
+
WHERE EXISTS (SELECT 1 FROM lead_categories WHERE lead_nip = l.nip AND category = ?)`;
|
|
122
120
|
|
|
123
121
|
// Cities query filtruje po kategorii — fix względem starego trener landinga
|
|
124
122
|
// który pokazywał miasta z całej tabeli leads (mieszane wertykale).
|
|
@@ -127,7 +125,6 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
127
125
|
FROM leads l
|
|
128
126
|
JOIN cities c ON l.city_id = c.id
|
|
129
127
|
INNER JOIN lead_categories lc ON l.nip = lc.lead_nip AND lc.category = ?
|
|
130
|
-
WHERE l.opted_out_at IS NULL
|
|
131
128
|
GROUP BY c.name ORDER BY count DESC LIMIT 8`;
|
|
132
129
|
|
|
133
130
|
// ─────────── Content loader ───────────
|
|
@@ -261,10 +258,15 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
261
258
|
}
|
|
262
259
|
app.get('/opt-out', (c) => c.html(renderStatic(pages.optOutForm)));
|
|
263
260
|
|
|
261
|
+
// One-click opt-out: przycisk na karcie profilu POST-uje slug. Hard delete
|
|
262
|
+
// z `leads` (cascade do `lead_pkd`/`lead_categories`) + insert do blacklisty
|
|
263
|
+
// `opted_out_nips` żeby sync nie wprowadził NIP-a z powrotem. Rate limit
|
|
264
|
+
// 5/h/IP przeciw abuse — slug jest publiczny, ktokolwiek może nukeować,
|
|
265
|
+
// ale katalog to publiczne wizytówki z CEIDG, nie konta.
|
|
264
266
|
app.post('/opt-out', async (c) => {
|
|
265
267
|
const form = await c.req.formData();
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
+
const slug = String(form.get('slug') ?? '').trim();
|
|
269
|
+
if (!slug) return c.text('Brak slug', 400);
|
|
268
270
|
|
|
269
271
|
const ip = c.req.header('cf-connecting-ip') ?? '0.0.0.0';
|
|
270
272
|
const rlKey = `vertical:${category}:rl:optout:${ip}`;
|
|
@@ -272,16 +274,21 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
272
274
|
if (rlVal >= OPTOUT_LIMIT) return c.text('Zbyt wiele prób. Spróbuj ponownie za godzinę.', 429);
|
|
273
275
|
await c.env.CACHE.put(rlKey, String(rlVal + 1), { expirationTtl: OPTOUT_WINDOW_SEC });
|
|
274
276
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
const row = await c.env.DB
|
|
278
|
+
.prepare('SELECT nip FROM leads WHERE slug = ?')
|
|
279
|
+
.bind(slug)
|
|
280
|
+
.first<{ nip: string }>();
|
|
281
|
+
if (!row) return c.text('Nie znaleziono wpisu', 404);
|
|
282
|
+
|
|
283
|
+
const now = Math.floor(Date.now() / 1000);
|
|
284
|
+
const ipHash = await sha256Hex(ip + ':' + slug);
|
|
285
|
+
await c.env.DB.batch([
|
|
286
|
+
c.env.DB.prepare('INSERT OR IGNORE INTO opted_out_nips (nip, opted_out_at, ip_hash) VALUES (?, ?, ?)').bind(row.nip, now, ipHash),
|
|
287
|
+
c.env.DB.prepare('DELETE FROM lead_categories WHERE lead_nip = ?').bind(row.nip),
|
|
288
|
+
c.env.DB.prepare('DELETE FROM lead_pkd WHERE nip = ?').bind(row.nip),
|
|
289
|
+
c.env.DB.prepare('DELETE FROM leads WHERE nip = ?').bind(row.nip),
|
|
290
|
+
]);
|
|
283
291
|
|
|
284
|
-
if (!res.meta.changes) return c.text('Nie znaleziono wpisu', 404);
|
|
285
292
|
return c.html(renderStatic(pages.optOutDone));
|
|
286
293
|
});
|
|
287
294
|
|
|
@@ -303,3 +310,8 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
303
310
|
|
|
304
311
|
return app;
|
|
305
312
|
}
|
|
313
|
+
|
|
314
|
+
async function sha256Hex(input: string): Promise<string> {
|
|
315
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
|
|
316
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
317
|
+
}
|