Skip to content

Repository files navigation

PortfolioCMS

Személyes portfólió oldal és a hozzá tartozó saját CMS. Két ASP.NET Core host, két Vue 3 SPA, Postgres és Redis; egy VPS-en, Docker Compose-zal, kifelé cloudflared tunnelen.

A rendszer szándékosan két hostra van osztva:

Host Mit ad Hogyan érhető el
web (publikus) a portfólió oldal, olvasó API, kapcsolatfelvétel, látogatás-beacon cloudflared tunnel → 127.0.0.1:8080
admin (dashboard) minden írás (CRUD), csatolmánykezelés, statisztika, contact inbox tunnel + Cloudflare Access → 127.0.0.1:8090

A lényeg nem a kényelem, hanem hogy a publikus originon nem léteznek író endpointok — nem le vannak tiltva, hanem az a controller-készlet nincs betöltve. Az admin host a védelmét nem a portkötésre bízza: a Cloudflare Access assertionjét maga is ellenőrzi (CloudflareAccessMiddleware).

Architektúra

PortfolioCMS.Core            közös mag: EF Core modell + migrációk, olvasó controllerek,
                             middleware-ek (CSP, bot, Access), ContentCache, SEO (sitemap,
                             útvonal-index, oldalankénti meta), indítási kiterjesztések
PortfolioCMS.Server          publikus host: Core + kapcsolat, analytics beacon, sitemap/robots,
                             SPA fallback (valódi 404 + szerveroldali meta)
PortfolioCMS.Admin           admin host: Core + CRUD és statisztika controllerek, Cloudflare Access
PortfolioCMS.Tests           xUnit: unit tesztek, TestServer-es pipeline tesztek, API tesztek
                             eldobható Postgres konténerben (Testcontainers)

portfoliocms.client          publikus SPA (Vue 3 + Vite)
portfoliocms.admin.client    dashboard SPA (Vue 3 + Vite)
portfoliocms.shared          a két SPA közös kódja: API kliens, content store, i18n, formázók.
                             Saját package.json / tsconfig / vitest — önmagában lintelhető,
                             típusellenőrizhető és tesztelhető. A két appba nem npm
                             függőségként, hanem a `@shared` aliasból épül be.

e2e/                         Playwright end-to-end a összeállított stacken (docker compose),
                             a publikus oldalra. Saját package.json — nem kerül belőle semmi
                             egyik appba sem.

deploy/                      üzemeltetés: runbook, mentés/visszaállítás, smoke teszt,
                             cloudflared példakonfiguráció, systemd unitok

Adattárolás:

  • Postgres — tartalom (Works, Skills, Experiences, About), csatolmányok bytea-ban, látogatás-események, kapcsolati üzenetek. A migrációk a Core-ban vannak, és mindkét host induláskor migrál (MigrateAndSeedAsync).
  • Redis — olvasási cache (ContentCache). Szándékosan fail-open: ha nem érhető el, minden kérés az adatbázisra megy, de az oldal áll. Minden admin írás explicit invalidál, a TTL csak biztonsági háló.

Kiszolgálás: mindkét host a saját wwwroot-jából adja a SPA-t. A /api alatti nem létező útvonal 404, nem index.html. A publikus hoston a MapSpaFallback dönt a status kódról (a nem létező slug valódi 404-et kap) és írja a kért oldal saját meta/OG tagjeit a kiszolgált HTML-be — a SPA ugyanezt megteszi futásidőben, de a szkriptet nem futtató crawler csak ezt látja.

Lokális fejlesztés

Kell hozzá: .NET 10 SDK, Node 22, Docker Desktop.

1. Konfiguráció

cp .env.example .env

Amit ki kell tölteni (a többinek van használható defaultja): POSTGRES_PASSWORD, ANALYTICS_HASH_SECRET, TURNSTILE_*, CF_ACCESS_*. A fájl minden változónál leírja, mire jó.

2. A teljes stack konténerben

docker compose up --build
  • publikus oldal: http://localhost:8080
  • admin: http://localhost:8090 — konténerben Production környezet fut, tehát az Access assertion ellenőrzés éles. Helyi admin munkához a hostot IDE-ből futtasd (lásd lentebb): Development-ben, CloudflareAccess értékek nélkül a middleware nem kerül be a pipeline-ba.

3. Backend IDE-ből, frontend Vite dev serverrel

Ez a szokásos fejlesztői kör. A PortfolioCMS.slnx mind a hat projektet tartalmazza, a két host Microsoft.AspNetCore.SpaProxy-val indul, tehát a npm run dev-et maga hívja.

Csak az adatbázist és a cache-t hozd fel konténerben:

docker compose up -d db redis

A db és a redis a compose-ban internal hálózaton van, port nélkül — a hoszt felől nem látszanak. A publikálásukhoz kell egy docker-compose.override.yml a repó gyökerében (a compose automatikusan betölti, és a .gitignore kihagyja, mert csak fejlesztéshez való):

services:
  db:
    ports:
      - "127.0.0.1:5432:5432"
  redis:
    ports:
      - "127.0.0.1:6379:6379"

A szerveren e nélkül kell deployolni: docker compose -f docker-compose.yml up -d.

Ezután a hostok konfigurációja appsettings.Development.json-ból jön (gitignorolt, minden fejlesztő a sajátját írja):

{
  "ConnectionStrings": {
    "Default": "Host=localhost;Port=5432;Database=portfoliocms;Username=portfolio;Password=<a .env-beli jelszó>",
    "Redis": "localhost:6379"
  },
  "Turnstile": {
    "SiteKey": "1x00000000000000000000AA",
    "SecretKey": "1x0000000000000000000000000000000AA"
  },
  "SeedMode": "dev"
}

A Turnstile értékek a Cloudflare mindig-átmenő teszt kulcsai. A SeedMode: dev demo tartalmat seedel; prod üres adatbázissal indul.

Indítás:

dotnet run --project PortfolioCMS.Server --launch-profile https

A Vite konfiguráció a dev tanúsítványt magától exportálja (dotnet dev-certs https), az első futás előtt viszont egyszer meg kell bízni benne:

dotnet dev-certs https --trust

Két környezeti változó a dev serverre:

  • DEV_SERVER_HTTP=1 — sima HTTP-t szolgál ki, ha valami nem fogadja el az önaláírt tanúsítványt
  • DEV_SERVER_PORT=<port> — a Vite port felülírása (alap: 53774 / 53775)

4. Tesztek

dotnet test PortfolioCMS.Tests/PortfolioCMS.Tests.csproj

A séma jsonb kolumnákat és FILTER aggregátumot használ, ezért az API tesztek egy eldobható Postgres konténert indítanak. Docker nélküli gépen ezek a tesztek kimaradnak (nem hasalnak el), a CI-ban futnak.

Frontend, mindkét app könyvtárában:

npm ci
npm run type-check
npx oxlint . && npx eslint .
npm test

A lock fájlok Linuxon készültek — a natív csomagok wasm fallback ágát a Windowson futó npm kihagyja a lockból, és akkor a CI npm ci "not in sync" hibára fut.

A portfoliocms.shared ugyanez, csak npm install-lal (a saját lockja Windowson készült):

cd portfoliocms.shared
npm install
npm run type-check && npx oxlint . && npx eslint . && npm test

A három package.json közös csomagjainak verziója nem csúszhat el — semmi nem köti őket egymáshoz, ezért a CI külön ellenőrzi:

node scripts/check-dep-sync.mjs

End-to-end (e2e/). Playwright a összeállított stacken, a publikus oldalra. Azt fedi, amit a fenti teszttípusok egyike sem ér el: a szerveroldali head-írás (SpaShell) és a kliensoldali useDocumentMeta együtt, a valódi 404 vs. SPA fallback (státuszkód és a megjelenített panel), a kapcsolati űrlap teljes köre, a nyelvváltás és a témaválasztás megmaradása, a ?notrack=1 analytics-kizárás, és a Redis leállítása mellett is működő oldal.

Előbb fel kell húzni a stacket seedelt tartalommal (.env a .env.example alapján, benne SEED_MODE=dev és a Cloudflare mindig-átmenő Turnstile teszt kulcspárja — a widget nélkül a küldés-teszt nem tud végigmenni):

docker compose -f docker-compose.yml up -d --wait --wait-timeout 180 web
cd e2e
npm install
npx playwright install --with-deps chromium
npm test

A cache-kimaradás tesztjei külön futnak, mert leállított Redist kérnek — egy spec ezt nem teheti meg magával anélkül, hogy a suite többi részét eltörné:

docker compose -f docker-compose.yml stop redis && npm run test:outage; docker compose -f docker-compose.yml start redis

A deploy/smoke-test.sh marad a mai, gyors HTTP-ellenőrzés: az e2e a PR-t védi, a smoke a kiadást. Hibánál a CI feltölti a trace-t és a képernyőképeket playwright-report artifactként. Az admin folyamatai kimaradnak az első körből: azokhoz Cloudflare Access assertion kell.

5. NuGet csomagok

A négy .NET projekt feloldott csomaggráfja packages.lock.json-ban van kikötve, és az image build --locked-mode-ban restore-ol — tehát egy elavult lock a buildet buktatja, nem csendben old fel mást. PackageReference változás után a lock fájlt is commitolni kell:

dotnet restore PortfolioCMS.slnx /p:RestoreForceEvaluate=true

Környezeti változók

A hitelesnek tekintendő lista a kommentezett .env.example. Amelyik nélkül a host el sem indul:

Változó Mi hasalna el nélküle
POSTGRES_PASSWORD a compose (:?), tehát mindkét host
ANALYTICS_HASH_SECRET (min. 16 byte) a publikus host — a látogató-hash sózása
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY a publikus host — üres secret csendben kikapcsolná a captchát
CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD az admin host — enélkül hitelesítés nélkül írható lenne

A többi (PUBLIC_SITE_URL, ALLOWED_HOSTS, SEED_MODE, CLIENT_ERRORS_ENABLED, retention és mentés beállítások) defaultolódik. Development-ben a captcha és az Access értékek elhagyhatók.

A RATE_LIMIT_PER_CLIENT / RATE_LIMIT_PER_PATH élesben maradjon 0-n: 0 = a hostba épített limit (240/perc kliensenként a publikus oldalon, 300 az adminban, 60 kliens+útvonal páronként), és az éles oldal ezekre van hangolva. Azért vannak, hogy egy CDN nélküli környezet — az e2e stack — feljebb vihesse őket: Cloudflare mögött az edge cache-eli a hash-elt asseteket, e nélkül viszont minden oldalbetöltés a teljes asset-készletbe kerül az originnél.

Deploy

A master-re való push indítja a deploy.yml-t:

  1. ci — a ci.yml meghívása: backend tesztek, mindkét frontend típusellenőrzése, lintje, npm audit-ja és tesztjei, a shared csomag ugyanez, a Playwright e2e a felhúzott stacken, valamint a NuGet sebezhetőség-ellenőrzés. Egy közvetlen master push sem deployol tesztek nélkül. Az image build (és vele a Trivy szkennelés) itt kimarad: azt a következő job végzi — az e2e viszont fut, a saját, cache-ből épülő image-ével.
  2. build-and-push — a két image (portfoliocms, portfoliocms-admin) buildje és pusholása GHCR-be, latest és sha-<commit> taggel.
  3. deploydocker-compose.yml és a deploy/ szkriptek másolása a VPS /opt/portfoliocms könyvtárába, majd SSH-n: .env írása a repository secretekből, GHCR login, pull, deploy előtti adatbázis dump (deploy/backup.sh), up -d, deploy/smoke-test.sh, végül a hétnél régebbi image-ek takarítása.

A dump azért van a séma hozzáérése előtt, mert a hostok induláskor migrálnak, és egy régebbi image nem tudja lebontani az újabb sémát — a visszalépés dump nélkül nem lenne elég.

Ehhez szükséges repository secretek:

  • SSH: SSH_HOST, SSH_USER, SSH_KEY, SSH_PORT
  • GHCR (a VPS oldali pullhoz): GHCR_USER, GHCR_TOKEN
  • alkalmazás: POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, ANALYTICS_HASH_SECRET, ANALYTICS_RETENTION_DAYS, CLIENT_ERRORS_ENABLED, ATTACHMENT_ORPHAN_GRACE_DAYS, CONTACT_RETENTION_DAYS, CONTACT_KEEP_UNREAD, PUBLIC_SITE_URL, SEED_MODE, ALLOWED_HOSTS, TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY, CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD
  • mentés: BACKUP_RETENTION_DAYS, BACKUP_AGE_RECIPIENT, BACKUP_RCLONE_REMOTE

Visszalépés

A rollback.yml kézzel indítható (workflow_dispatch). SHA megadása nélkül az eggyel korábbi image párra lép vissza: a deploy minden alkalommal kimenti a futó image referenciákat a VPS .env.previous fájljába. A séma miatt a visszalépés általában visszaállítást is igényel — a menete a runbookban van.

Függőségfrissítés

A .github/dependabot.yml hetente négy ökoszisztémát néz: nuget, npm (a három frontend manifest), github-actions és docker (a két Dockerfile és a docker-compose.yml base image-ei). A PR-ek csoportosítva jönnek, minor/patch és major külön — egy Microsoft.* patch kiadás enélkül nyolc külön PR-t nyitna.

A három frontend manifest szándékosan egy PR-be esik: a scripts/check-dep-sync.mjs elhasal, ha ugyanaz a csomag máshol más ranget kap, tehát csak együtt léptethetők.

A CI oldali ellenőrzés ehhez: npm audit --audit-level=high mindkét frontendre, a dotnet list package --vulnerable --include-transitive a négy .NET projektre, és Trivy (HIGH,CRITICAL, ignore-unfixed) a PR-ben buildelt image-ekre.

Üzemeltetés

Amit a kódból nem lehet látni, mert a hoszton él, a deploy/RUNBOOK.md-ban van: a forgalom útja, a cloudflared konfiguráció (példa), a Cloudflare Access alkalmazás és AUD, az ellenőrzőlista, a deploy és a visszalépés menete, az adatbázis mentése (titkosítás, offsite tároló, systemd timer) és visszaállítása, a csatolmányok életciklusa, az erőforrás-korlátok és a logrotáció, valamint a kliensoldali hibák kiolvasása a logból és a stack trace visszafejtése.

Megjegyzés a dokumentációhoz

A 2026-08-11-i biztonsági és optimalizálási átvizsgálás (TODO.md) tételei vagy meg vannak oldva, vagy GitHub issue-ként vannak nyitva — a fájl ezért nem maradt a repóban. Ami a döntések miértjét rögzíti, az a kód mellett van: a docker-compose.yml, a két Program.cs és a Core/Services XML kommentjei szándékosan bőbeszédűek.

About

Saját üzemeltetésű portfólió CMS: .NET 10 API + két Vue 3 SPA (publikus oldal és admin), PostgreSQL + Redis, kétnyelvű tartalom, cookie nélküli látogatói statisztika, kapcsolat inbox. Admin Cloudflare Access mögött, Docker Compose deploy.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages