Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Rezervo – online rezervace pro malé podniky (Next.js 16, Supabase, Cloudflare Workers)
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Rezervační systém pro kadeřnictví, fyzioterapie, masáže a podobné malé podniky. Majitel si nastaví služby a otevírací dobu, zákazníci si sami rezervují volný termín na veřejné stránce podniku a obě strany dostanou e-mail.
Portfolio projekt · Next.js 16 (App Router) · TypeScript · Tailwind CSS 4 · Supabase · Resend · Cloudflare Workers
https://rezervo.bukac-adam.workers.dev
| Veřejná rezervační stránka | /u-nuzek – fiktivní „Kadeřnictví U Nůžek“ |
| Administrace majitele | /login – [email protected] / demo-heslo-123 |
| API klíč demo podniku | po přihlášení v /dashboard/api |
| REST API | https://rezervo.bukac-adam.workers.dev/api/v1 – viz Veřejné REST API |
curl -H "Authorization: Bearer <API klíč>" https://rezervo.bukac-adam.workers.dev/api/v1/services
Testovací účet je sdílený, nevyplňujte skutečné osobní údaje. V ukázkovém účtu nelze měnit heslo, adresu stránky, e-mail pro upozornění ani API klíč; tyto údaje se navíc každou hodinu obnovují (/api/cron/demo-reset). Data (služby, rezervace) obnoví npm run seed.
| Veřejná rezervační stránka | Výběr termínu a údaje zákazníka |
|---|---|
Screenshoty jsou z produkčního nasazení s ukázkovými daty.
/u-nuzek), kontakty, služby (délka, cena), otevírací doba pro každý den v týdnu, krok nabízených časů./[slug] – výběr služby → (pracovníka) → dne → volného času → kontaktní údaje.EXCLUDE constraint nad časovým rozsahem pro každého pracovníka), takže to platí i pro dva zákazníky, kteří kliknou ve stejnou chvíli./rezervace/<id>/<token>, HMAC – nic se neukládá) na stránku, kde zákazník rezervaci zruší nejpozději 2 hodiny předem; majitel dostane e-mail.| Oblast | Řešení |
|---|---|
| Integrita dat | bookings_no_overlap – EXCLUDE USING gist (staff_id WITH =, tstzrange(starts_at, ends_at, '[)') WITH &&) WHERE status = 'confirmed'. Navazující termíny (10:00–11:00 a 11:00–12:00) jsou povolené; triggery hlídají, že pracovník i služba patří stejnému podniku. |
| Vytvoření rezervace | create_booking() – SECURITY DEFINER funkce, která ověří službu, pracovníka, otevírací dobu, zarovnání času a vloží záznam. Pro „Kdokoli“ zkouší pracovníky v pořadí a o obsazenosti rozhoduje atomicky exclusion constraint. Anonymní uživatel nemá k tabulce bookings žádný přímý přístup. |
| Obsazenost | get_staff_busy_intervals() vrací jen časové intervaly a pracovníka, žádná osobní data zákazníků. Volné časy = sjednocení volných časů pracovníků, kteří službu dělají (computeStaffSlots()). |
| Row Level Security | Majitel vidí a mění jen svůj podnik a jeho rezervace; anonym vidí jen veřejné sloupce podniku (bez e-mailu majitele). |
| Časová pásma | Vše se ukládá v UTC, sloty se počítají v časovém pásmu podniku (Europe/Prague) přes Intl – funguje i na serveru v UTC a přes přechody letního času. |
| Výpočet slotů | Čistá funkce computeAvailableSlots() v src/lib/slots.ts, pokrytá unit testy. |
| E-maily | Odesílají se přes after() až po odpovědi, takže pomalé SMTP nezdrží zákazníka. Bez RESEND_API_KEY se jen vypisují do konzole. |
| Veřejné API | Klíče se ověřují podle SHA-256 hashe; pro zobrazení v administraci jsou navíc uložené šifrovaně (AES-256-GCM, tajemství jen na serveru). Rotace je atomická (rotate_api_key()), rate limiting běží v Postgresu (api_rate_limit_hit()), takže platí napříč serverless instancemi. Handlery v src/lib/api/handlers.ts nemají přímou závislost na Supabase a jsou plně pokryté testy. |
| Bezpečnostní hlavičky | CSP (jen vlastní origin, frame-ancestors 'none'), HSTS, X-Content-Type-Options, , , COOP – v . |
src/
app/
page.tsx úvodní stránka
(auth)/ přihlášení, registrace, server actions
auth/callback/ potvrzení e-mailu ze Supabase
dashboard/setup/ onboarding nového podniku
dashboard/(app)/ kalendář, služby, otevírací doba, nastavení, API klíč
[slug]/ veřejná rezervační stránka + booking action
api/slots/ volné časy pro službu a den
api/cron/reminders/ denní připomínky
api/v1/ veřejné REST API (služby, sloty, rezervace)
lib/
slots.ts, time.ts výpočet slotů a práce s časovými pásmy
notifications.ts e-mailové notifikace
email/ Resend klient a šablony
supabase/ klienti (server, public, admin, proxy)
api/ API klíče (generování, šifrování), validace, handlery, napojení na Supabase
proxy.ts obnova session, ochrana /dashboard
supabase/migrations/ SQL schéma, RLS, funkce
supabase/tests/ test schématu v PGlite (in-memory Postgres)
scripts/seed.mts demo data „Kadeřnictví U Nůžek“ + API klíč
scripts/cf-strip-env.mjs odstraní serverová tajemství z bundlu workeru
custom-worker.ts vstup Cloudflare Workeru (OpenNext + cron pro připomínky)
wrangler.jsonc konfigurace Workeru (nodejs_compat, Images, cron)
Požadavky: Node.js 20+ a účet na supabase.com (free tier stačí).
npm install
supabase/migrations/ v pořadí podle názvu
(…_initial_schema.sql, …_public_api.sql, …_reserved_slugs.sql, …_closures.sql, …_staff.sql, …_staff_hours.sql, …_staff_closures.sql),
případně npx supabase link + npx supabase db push..env.example do .env.local a doplňte:
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY – Project Settings → API Keys (secret key je nutný pro e-maily, připomínky, seed a veřejné API)RESEND_API_KEY, EMAIL_FROM – volitelné; bez nich se e-maily jen vypisují do termináluCRON_SECRET – libovolný dlouhý náhodný řetězecAPI_KEY_ENCRYPTION_SECRET – náhodný řetězec (min. 32 znaků), kterým se šifrují uložené API klíče; vygenerujete ho příkazem node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))". Hodnotu později neměňte – jinak stávající klíč nepůjde v administraci zobrazit (fungovat ale bude dál). Slouží zároveň k podpisu odkazů „Zobrazit nebo zrušit rezervaci“ v e-mailech, pokud nenastavíte samostatný BOOKING_LINK_SECRET.BOOKING_LINK_SECRET – volitelné, min. 32 znaků; samostatné tajemství pro odkazy zákazníků. Jeho změna zneplatní odkazy v už odeslaných e-mailech.http://localhost:3000, do Redirect URLs přidejte http://localhost:3000/auth/callback.
(Pro rychlé testování lze v Authentication → Providers → Email vypnout „Confirm email“.)npm run seed
Vytvoří podnik Kadeřnictví U Nůžek na /u-nuzek, účet [email protected] / demo-heslo-123 a API klíč (vypíše ho; stejný klíč mezi běhy zachováte proměnnou ).npm test # unit testy (sloty, časová pásma, e-mailové šablony, veřejné API)
npm run test:db # SQL migrace + pravidla rezervací, RLS, API klíče a rate limit v in-memory Postgresu
npm run lint
Připomínky lze vyzkoušet ručně:
curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/reminders
API pro externí aplikace (AI asistent, vlastní web, integrace). Každý požadavek se vztahuje k podniku, kterému patří použitý API klíč.
/dashboard/api) a klikněte na Vygenerovat API klíč.rzv_live_…) se zobrazí hned po vygenerování a kdykoli později přes Zobrazit klíč (s tlačítkem Kopírovat). V administraci vidíte i datum vytvoření a posledního použití.Base URL demo: https://rezervo.bukac-adam.workers.dev/api/v1. Klíč posílejte v hlavičce každého požadavku:
Authorization: Bearer rzv_live_…
Klíč je tajný – patří na server (backend AI asistenta), ne do kódu v prohlížeči nebo mobilní aplikaci. API proto neposílá CORS hlavičky.
| Základní URL | https://<vaše-doména>/api/v1 (lokálně http://localhost:3000/api/v1) |
| Formát | JSON (UTF-8); požadavky s tělem vyžadují Content-Type: application/json, max. 10 kB |
| Čas | Vstupy i výstupy v ISO 8601. starts_at musí obsahovat časovou zónu (Z nebo +02:00). Odpovědi vrací UTC a navíc local_time / local_date v časovém pásmu podniku. |
| Úspěch | { "data": … } (u seznamu služeb navíc meta) |
| Chyba | { "error": { "code": "…", "message": "…", "details": … } } – code je stabilní, message je lidsky čitelný popis (anglicky) |
| Limit | Hodnota |
|---|---|
| Všechny požadavky | 120 / minutu na klíč |
| Vytvoření a zrušení rezervace | navíc 20 / minutu na klíč |
Každá ověřená odpověď obsahuje hlavičky RateLimit-Limit, RateLimit-Remaining a RateLimit-Reset (sekundy do obnovení). Po překročení API vrací 429 rate_limited s hlavičkou Retry-After.
GET /staff – týmcurl https://<doména>/api/v1/staff -H "Authorization: Bearer $REZERVO_API_KEY"
{
"data": [
{ "id": "7a1e…", "name": "Jana", "service_ids": ["5b0c6f8e-…"], "working_hours": null, "days_off": [] },
{ "id": "c03b…", "name": "Tomáš", "service_ids": ["5b0c6f8e-…"], "working_hours": [{ "weekday": 3, "opens_at": "12:00", "closes_at": "19:00" }], "days_off": [{ "starts_on": "2026-10-21", "ends_on": "2026-10-22" }] }
]
}
Aktivní pracovníci v pořadí, ve kterém se přidělují rezervace „Kdokoli“, služby, které dělají, a pracovní doba (null = celá otevírací doba podniku, jinak směny po dnech v týdnu, weekday 1 = pondělí; chybějící den = volno) a nadcházející dny volna days_off (včetně obou krajních dnů).
GET /services – seznam služebcurl https://<doména>/api/v1/services -H "Authorization: Bearer $REZERVO_API_KEY"
{
"data": [
{ "id": "5b0c6f8e-…", "name": "Pánský střih", "description": "Střih nůžkami i strojkem", "duration_minutes": 30, "price_czk": 450, "staff_ids": ["7a1e…"] }
],
"meta": { "business": { "name": "Kadeřnictví U Nůžek", "slug": "u-nuzek", "timezone": "Europe/Prague" } }
}
Vrací jen aktivní služby, které dělá aspoň jeden aktivní pracovník, v pořadí jako na rezervační stránce.
GET /slots?service_id=<uuid>&date=YYYY-MM-DD[&staff_id=<uuid>] – volné termínycurl "https://<doména>/api/v1/slots?service_id=5b0c6f8e-…&date=2026-10-14" -H "Authorization: Bearer $REZERVO_API_KEY"
{
"data": {
"date": "2026-10-14",
"timezone": "Europe/Prague",
"closed": false,
"service": { "id": "5b0c6f8e-…", "name": "Pánský střih", "duration_minutes": 30 },
"slots": [
{ "starts_at": "2026-10-14T07:00:00.000Z", "ends_at": "2026-10-14T07:30:00.000Z", "local_time": "09:00", "staff_ids": ["7a1e…"] }
]
}
}
date je kalendářní den v časovém pásmu podniku. Rezervovat lze od dneška do 60 dnů dopředu (jinak 422 date_out_of_range s details.min_date / details.max_date). Zavřený den vrací closed: true, prázdné slots a closed_reason ("regular" – mimo otevírací dny, "holiday" – dovolená/svátek). Termíny začínající dřív než za 30 minut se nenabízejí. Bez staff_id jsou to časy, kdy je volný aspoň jeden pracovník (staff_ids = kdo je volný); se staff_id jen časy daného pracovníka (404 staff_not_found, pokud službu nedělá).
POST /bookings – vytvoření rezervacecurl -X POST https://<doména>/api/v1/bookings \
-H "Authorization: Bearer $REZERVO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"service_id": "5b0c6f8e-…",
"staff_id": null,
"starts_at": "2026-10-14T09:00:00+02:00",
"customer": { "name": "Jana Nováková", "email": "[email protected]", "phone": "+420 777 123 456" }
}'
Odpověď 201 Created (+ hlavička Location):
{
"data": {
"id": "9d4f…",
"status": "confirmed",
"starts_at": "2026-10-14T07:00:00.000Z",
"ends_at": "2026-10-14T07:30:00.000Z",
"local_date": "2026-10-14",
"local_time": "09:00",
"timezone": "Europe/Prague",
"service": { "id": "5b0c6f8e-…", "name": "Pánský střih", "duration_minutes": 30, "price_czk": 450 },
"staff": { "id": "7a1e…", "name": "Jana" },
"customer": { "name": "Jana Nováková", "email": "[email protected]", "phone": "+420 777 123 456" },
"created_at": "2026-10-10T09:12:00.000Z"
}
}
starts_at použijte přesně z odpovědi /slots. staff_id je nepovinné – bez něj (nebo null) se přidělí první volný pracovník, který službu dělá; odpověď vždy obsahuje přiděleného staff. Zákazník i majitel dostanou stejné e-maily jako u rezervace z webu. Validace: customer.name 2–100 znaků, platný customer.email, customer.phone 6–30 znaků (číslice, mezery, (), -, volitelně + na začátku). Dvojí rezervaci téhož času brání databáze i při souběžných požadavcích – druhý dostane 409 slot_taken.
GET /bookings/{id} – detail rezervacecurl https://<doména>/api/v1/bookings/9d4f… -H "Authorization: Bearer $REZERVO_API_KEY"
Vrací stejný objekt jako vytvoření rezervace (včetně aktuálního status). Na tuto adresu ukazuje hlavička Location z POST /bookings.
POST /bookings/{id}/cancel – zrušení rezervacecurl -X POST https://<doména>/api/v1/bookings/9d4f…/cancel -H "Authorization: Bearer $REZERVO_API_KEY"
Vrací 200 a rezervaci se "status": "cancelled"; zákazník dostane e-mail o zrušení a termín se uvolní. Zrušit lze jen potvrzenou rezervaci, která ještě nezačala.
| HTTP | code | Kdy |
|---|---|---|
| 400 | validation_error | Neplatné parametry nebo tělo; details obsahuje seznam { field, message } |
| 400 | invalid_json | Tělo není platný JSON |
| 401 | missing_api_key | Chybí hlavička Authorization |
| 401 | invalid_authorization_header | Hlavička není ve tvaru Bearer <klíč> |
| 401 | invalid_api_key | Neznámý nebo zneplatněný klíč |
| 404 | service_not_found | Služba neexistuje, není aktivní nebo patří jinému podniku |
| 404 | staff_not_found | Pracovník neexistuje, není aktivní nebo službu nedělá |
| 404 | booking_not_found | Rezervace neexistuje nebo patří jinému podniku |
| 404 | not_found | Neznámý endpoint |
| 405 | – | Nepodporovaná HTTP metoda |
| 409 | slot_taken | Termín je už obsazený – načtěte znovu /slots |
| 409 | booking_already_cancelled | Rezervace už je zrušená |
| 409 | booking_in_past | Rezervace už začala nebo proběhla |
| 413 | payload_too_large | Tělo je větší než 10 kB |
| 415 | unsupported_media_type | Chybí Content-Type: application/json |
| 422 | slot_in_past | Termín je v minulosti |
| 422 | slot_too_soon | Termín začíná dřív než za 30 minut |
| 422 | date_out_of_range | Datum je mimo rezervační okno (dnes až +60 dní) |
| 422 | outside_opening_hours | Služba se nevejde do otevírací doby |
GET /services → nabídnout službu a zjistit timezone podniku. Pokud zákazník chce konkrétního pracovníka, GET /staff.GET /slots pro zvolený den (případně se staff_id) → nabídnout časy (local_time).POST /bookings se starts_at vybraného slotu. Při 409 slot_taken načíst sloty znovu a nabídnout jiný čas.data.id – stav zjistíte přes GET /bookings/{id}, zrušíte přes POST /bookings/{id}/cancel.Aplikace běží na Cloudflare Workers přes @opennextjs/cloudflare (nodejs_compat pokrývá node:crypto i AsyncLocalStorage).
npx wrangler login.env.production.local s NEXT_PUBLIC_SITE_URL=https://<worker>.<subdoména>.workers.dev
(hodnoty NEXT_PUBLIC_* se vkládají do bundlu při buildu)..env.local):
npx wrangler secret put SUPABASE_SECRET_KEY # stejně CRON_SECRET, API_KEY_ENCRYPTION_SECRET,
# EMAIL_FROM, RESEND_API_KEY, BOOKING_LINK_SECRET
npm run deploy (lokální náhled ve workerd: npm run preview).
Skript scripts/cf-strip-env.mjs po buildu odebere z bundlu vše kromě NEXT_PUBLIC_* – OpenNext jinak přibalí hodnoty ze všech .env* souborů.https://<vaše-doména>/auth/callback.wrangler.jsonc: připomínky denně v 15:00 UTC (17:00 SELČ) a obnova demo účtu každou hodinu – custom-worker.ts zavolá příslušnou route s CRON_SECRET.EMAIL_FROM (testovací [email protected] doručuje jen na e-mail vašeho Resend účtu).Poznámka: patches/@opennextjs+cloudflare+*.patch (aplikuje patch-package při npm install) doplňuje inlinování manifestu preview-props.json, který Next.js 16.4 načítá a OpenNext 1.20 zatím nezná.
Supabase ve výchozím nastavení posílá odkaz pro obnovu hesla v režimu PKCE – funguje jen v prohlížeči, ve kterém uživatel o obnovu požádal. Aby šel odkaz otevřít kdekoli (třeba žádost na počítači, e-mail v telefonu), změňte v Supabase Authentication → Emails → Reset Password odkaz na:
{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=recovery&next=/reset-password
(Stejně lze upravit šablonu Confirm signup s type=signup&next=/dashboard/setup.) Callback /auth/callback umí oba způsoby.
Referrer-PolicyPermissions-Policy| Sdílené demo | src/lib/demo.ts – blokuje změny, kterými by návštěvník zamkl účet ostatním nebo přesměroval e-maily; hodinový cron je vrací i při přímém volání Supabase. |
| Připomínky | /api/cron/reminders – rezervace se nejdřív „zabere“ nastavením reminder_sent_at, takže dva souběžné běhy nepošlou připomínku dvakrát. |
DEMO_API_KEYnpm run dev
Otevřete http://localhost:3000.| 422 | business_closed | Podnik má v tento den zavřeno (dovolená, svátek) |
| 422 | outside_staff_hours | V tento čas nepracuje žádný vhodný pracovník (nebo ten zvolený) |
| 422 | slot_misaligned | Čas neodpovídá nabízeným slotům (krok podniku) |
| 429 | rate_limited | Překročený limit, viz Retry-After |
| 500 | internal_error | Chyba na straně serveru (detaily jen v logu) |