diff --git a/README.md b/README.md index 022eab26..1a5ac713 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,62 @@ -# EpicNext-CMS . +# EpicNext-CMS -A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Prisma 7**. Designed to integrate seamlessly with the Polaris MySQL/MariaDB database. +A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Prisma 7** and **React 19**. Designed to integrate seamlessly with Polaris / Arcturus Morningstar MySQL/MariaDB databases. -Features full public-facing website, administrative panel, NextAuth authentication (argon2id/bcrypt with MD5-to-argon2id upgrade path), real-time RCON communication with the emulator, and extensive extensibility. +Features a full public-facing website, an administrative panel, NextAuth authentication (argon2id/bcrypt with MD5-to-argon2id upgrade), real-time RCON communication with the emulator, Server-Sent Events for live radio data, smooth page transitions, and extensive extensibility. --- -## Requirements +## System Requirements -| Component | Version | -| --------------- | ---------------------------------- | -| Node.js | >= 22 | -| pnpm | >= 10.33.4 | -| MySQL / MariaDB | 8.0+ / 10.6+ | -| Redis | Optional (caching / rate limiting) | +| Component | Version | Notes | +| --------------- | -------------- | ---------------------------------------- | +| Node.js | >= 22 | Required by Next.js 16 | +| pnpm | >= 10.33.4 | Package manager (npm/yarn not supported) | +| MySQL / MariaDB | 8.0+ / 10.6+ | Shared with the emulator | +| Redis | 7.x+ | Optional — caching, rate limiting, SSE | +| Java | 17+ | Required only if building the emulator | +| Maven | 3.9+ | Required only if building the emulator | --- -## 1. Database Setup +## Quick Start -EpicNext-CMS shares a database with the Polaris emulator. You may use an existing or empty database. +### 1. Clone & Install -**Create a new database:** +```bash +git clone https://gitlab.epicnabbo.nl/remco/EpicNext-Cms.git +cd EpicNext-Cms +pnpm install +``` + +### 2. Database Setup + +The CMS shares a database with the Polaris/Arcturus emulator. Use an existing database or create a new one: ```sql CREATE DATABASE IF NOT EXISTS epicnext_cms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` -**Use an existing emulator database:** -Point `DATABASE_URL` to your existing database — the CMS reads emulator tables (`users`, `users_currency`, `bans`, etc.) directly. +The CMS reads emulator-owned tables (`users`, `items`, `rooms`, `bans`, etc.) directly. It never creates, alters, or drops them. -**Create CMS tables:** -After configuring `.env`, run the migration script (step 5). This creates only CMS-owned tables (`website_*`, `radio_*`, etc.), leaving the emulator schema untouched. - ---- - -## 2. Environment Variables - -Copy `.env.example` to `.env` and configure: +### 3. Configure Environment ```bash cp .env.example .env ``` -### Required +Edit `.env` with at minimum: ```dotenv DATABASE_URL=mysql://user:password@127.0.0.1:3306/epicnext_cms -AUTH_SECRET= # minimum 32 characters, random +AUTH_SECRET= HOTEL_NAME=YourHotel APP_URL=http://localhost:3000 ``` -### Database Pool (Tunable) +See `.env.example` for all optional variables (RCON, email, Redis, OAuth, PayPal, etc.). -```dotenv -DATABASE_POOL_SIZE=40 -DATABASE_IDLE_TIMEOUT_MS=300000 -DATABASE_CONNECT_TIMEOUT_MS=10000 -``` - -### Password Settings - -```dotenv -CONVERT_PASSWORDS=false # enable to upgrade legacy MD5 hashes to argon2id on login -PASSWORD_HASH=bcrypt # bcrypt (default) or argon2id -``` - -### Laravel APP_KEY (2FA Migration) - -Required only for reading existing Laravel-encrypted 2FA secrets: - -```dotenv -APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx== -``` - -### RCON (Emulator Bridge) - -Enables live credits, badges, motto changes, rank changes, kick/ban: - -```dotenv -RCON_HOST=127.0.0.1 -RCON_PORT=3001 -``` - -### Badge Upload (Emulator Directory) - -```dotenv -BADGE_UPLOAD_DIR=/path/to/emulator/assets/c_images/album1584 -``` - -### Email (Password Reset / Notifications) - -Two options — **Resend** (recommended) or **SMTP**: - -```dotenv -# Resend (preferred) -RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx - -# OR SMTP: -SMTP_HOST=smtp.example.com -SMTP_PORT=587 -SMTP_USER=user -SMTP_PASSWORD=password -SMTP_FROM=noreply@yourhotel.nl -``` - -### OAuth (Discord / Google) - -```dotenv -DISCORD_CLIENT_ID= -DISCORD_CLIENT_SECRET= -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -``` - -### PayPal (Credit Purchases) - -```dotenv -PAYPAL_CLIENT_ID= -PAYPAL_SECRET= -PAYPAL_API=https://api-m.sandbox.paypal.com # sandbox for testing; live: https://api-m.paypal.com -``` - -### Redis (Caching / Rate Limiting) - -```dotenv -REDIS_URL=redis://127.0.0.1:6379 -``` - -### AI Content Moderation (Comments / Guestbook) - -```dotenv -OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx -``` - -### Notifications & Logging - -```dotenv -DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... -ALERT_EMAIL=admin@yourhotel.nl -LOG_LEVEL=info # debug | info | warn | error -``` - -### Emulator JAR Backup (Jobs Worker) - -```dotenv -EMULATOR_JAR_PATH=/path/to/emulator.jar -EMULATOR_BACKUP_DIR=/path/to/backups -EMULATOR_BACKUP_KEEP=7 # number of backups to retain -``` - ---- - -## 3. Install Dependencies - -```bash -pnpm install -``` - ---- - -## 4. Generate Prisma Client +### 4. Generate Prisma Client ```bash pnpm prisma:generate @@ -169,90 +64,117 @@ pnpm prisma:generate Generates TypeScript types in `src/generated/prisma/`. ---- - -## 5. Run CMS Migrations +### 5. Run CMS Migrations ```bash pnpm db:migrate ``` -Executes SQL files in `prisma/migrations/` against the database, creating all CMS-owned tables. +Creates all CMS-owned tables (`website_*`, `radio_*`, `acl_*`, `admin_audit_log`, etc.) via idempotent SQL files in `prisma/migrations/`. Emulator tables are never touched. -**Check migration status:** +Check migration status: ```bash pnpm db:migrate:status ``` ---- - -## 6. Configure Website Settings - -After starting the server (step 7) and logging in as an administrator: - -1. Navigate to **Admin → CMS Settings** (`/admin/settings`). -2. Configure the following essentials: - - `hotel_name` — your hotel name - - `habbo_imaging_url` — avatar image URL (default: `https://www.habbo.com/habbo-imaging/avatarimage`) - - `nitro_client_url` — Nitro client URL (refer to `update-Nitrov3.sh`) - - `logo_url` — path to your logo - - `cms_favicon` — favicon URL - - `min_staff_rank` — minimum rank required for admin access (default: 7) - - Optionally: theme colors, CAPTCHA, VPN blocking, and more. - ---- - -## 7. Start the Server - -### Development (Hot Reload) +### 6. Build & Start ```bash +# Development (hot reload) pnpm dev -``` -### Production - -```bash +# Production pnpm build && pnpm start ``` -### TypeScript Check +Open `http://localhost:3000` in your browser. -```bash -pnpm typecheck -``` +### 7. First Login -### Run Tests - -```bash -pnpm test -``` +1. Register an account at `/register`, or log in with an existing emulator account. +2. Grant yourself admin access: `UPDATE users SET rank = 7 WHERE username = 'yourname';` +3. Visit `/admin` and configure your hotel via **Admin → CMS Settings**. --- -## 8. Jobs Worker (Background Tasks) +## Scripts -Run as a persistent process (e.g., via systemd, screen, or PM2): - -```bash -pnpm jobs:worker -``` - -Scheduled tasks: - -- **Daily at 03:00** — Emulator JAR backup (when `EMULATOR_JAR_PATH` and `EMULATOR_BACKUP_DIR` are configured) -- **Daily at 04:00** — Cleanup of login logs older than 30 days and expired password reset tokens older than 7 days +| Command | Description | +| ---------------------- | -------------------------------------------------- | +| `pnpm dev` | Start development server (hot reload) | +| `pnpm build` | Production build | +| `pnpm start` | Start production server | +| `pnpm typecheck` | Run TypeScript type checking | +| `pnpm test` | Run all tests (Vitest) | +| `pnpm db:migrate` | Apply pending SQL migrations | +| `pnpm db:migrate:status` | Show migration status | +| `pnpm analyze` | Build + open bundle analyzer | +| `pnpm jobs:worker` | Start background task worker (systemd / PM2) | +| `pnpm biome:check` | Lint and format code | --- -## 9. First Login +## Performance Features -1. Open `http://localhost:3000` in your browser. -2. Register an account via `/register`, or log in with an existing account. -3. To obtain administrator access, your rank must equal or exceed `min_staff_rank` (default: 7). - - Set it directly in the database: `UPDATE users SET rank = 7 WHERE username = 'yourname';` -4. Access the admin panel at `/admin`. +| Feature | Description | +| ------------------------------ | -------------------------------------------------------------- | +| **React Compiler** | Automatic memoization — reduces unnecessary re-renders | +| **View Transitions API** | Native browser transitions between page navigations | +| **Lenis Smooth Scroll** | Fluid, customizable scrolling (respects `prefers-reduced-motion`) | +| **Server-Sent Events** | Real-time radio now-playing & listeners via SSE (no polling) | +| **Redis Caching** | Caches API responses (home, radio config) up to 30s in Redis | +| **Bundle Analyzer** | Run `pnpm analyze` to visualize and optimize bundle sizes | +| **AnimatePresence** | Framer Motion exit animations on modals, dialogs, and lightbox | +| **RCON (TCP Socket)** | Live commands to the emulator (credits, badges, kick, ban) | +| **Streaming & Suspense** | Next.js App Router streaming for fast page loads | +| **Automatic Image Optimization** | `next/image` with Sharp for resizing and WebP/AVIF | +| **CSS-based Animations** | Base UI components use `data-open`/`data-closed` CSS animations | +| **Compression** | Gzip compression enabled on all responses | +| **Stale Times** | Optimized router cache (30s dynamic, 180s static) | + +--- + +## Architecture + +``` +├── prisma/ +│ ├── schema.prisma # ~190 models (emulator + CMS) +│ └── migrations/ # 16 SQL migrations for CMS tables +├── scripts/ +│ ├── apply-migrations.ts # Custom migration runner +│ ├── jobs-worker.ts # Background task scheduler +│ └── sql-statements.ts # SQL parsing utilities +├── src/ +│ ├── app/ # Next.js App Router (pages & API routes) +│ ├── actions/ # Server Actions +│ ├── components/ # UI components +│ ├── lib/ +│ │ ├── auth/ # NextAuth, password hashing, 2FA, SSO tickets +│ │ ├── services/ # RCON, email, currency, PayPal, alerts +│ │ ├── prisma.ts # Database connection singleton +│ │ ├── redis.ts # Redis client (ioredis) +│ │ ├── redis-cache.ts # Redis caching utility for API routes +│ │ ├── cache.ts # In-memory cache fallback +│ │ ├── motion.ts # Framer Motion animation variants +│ │ └── use-event-source.ts # React hook for SSE subscriptions +│ ├── messages/ # i18n translations (en, nl, de, fr, es, it) +│ └── env.ts # Zod-validated environment schema +├── public/ +│ ├── assets/ # Images, icons, fonts +│ └── scripts/ # Client-side scripts (theme-init.js) +├── update-Nitrov3.sh # Emulator & Nitro updater utility +├── next.config.ts # Next.js configuration +└── .env.example # Environment template with all options +``` + +### Database Ownership + +| Component | Type | Migrations | +| ------------------------------------------------- | ----------------------- | ----------------------------------- | +| Emulator tables (`users`, `items`, `rooms`, etc.) | Existing Polaris schema | None — CMS reads/writes only | +| CMS tables (`website_*`, `radio_*`, etc.) | CMS-owned | `prisma/migrations/*.sql` (16 files) | +| Migration tracking | `cms_migrations` table | Auto-created by migration runner | --- @@ -260,70 +182,66 @@ Scheduled tasks: ### Emulator — RCON -For live actions (credits, badges, rank changes, kick/ban), the Polaris emulator must be running with RCON enabled and accessible at the configured `RCON_HOST:RCON_PORT`. +For live actions (credits, badges, rank changes, kick, ban, hotel alerts). The emulator must be running with RCON enabled at `RCON_HOST:RCON_PORT`. ### Client — Nitro -The `/client` page loads the Nitro client. Configure the client URL via **Admin → CMS Settings** (`nitro_client_url`). +The `/client` page loads the Nitro client. Configure the client URL via **Admin → CMS Settings** (`nitro_client_url`). See `update-Nitrov3.sh` for deployment. ### Radio -For use with a streaming radio (e.g., Azuracast): +Supports any streaming radio (e.g., Azuracast). Configure endpoints via CMS Settings: - `radio_now_playing_api_url` - `radio_listeners_api_url` +The radio player uses SSE for real-time updates (10s interval, no polling). + +### Background Jobs + +Run as a persistent process: + +```bash +pnpm jobs:worker +``` + +Scheduled tasks: +- **Daily 03:00** — Emulator JAR backup (requires `EMULATOR_JAR_PATH` + `EMULATOR_BACKUP_DIR`) +- **Daily 04:00** — Cleanup login logs (>30 days) and expired password reset tokens (>7 days) + ### CAPTCHA -Supports Cloudflare Turnstile and Google reCAPTCHA. Configure via **Admin → CMS Settings**: - -- `captcha_provider` — `turnstile` or `recaptcha` -- `turnstile_site_key` / `turnstile_secret` -- `recaptcha_site_key` / `recaptcha_secret` +Supports Cloudflare Turnstile and Google reCAPTCHA. Configure via CMS Settings. ### Theming -12 preset themes are available via **Admin → Theme** (`/admin/theme`), along with fully customizable color schemes. +12 preset themes with fully customizable colors via **Admin → Theme** (`/admin/theme`). + +### AI Content Moderation + +Optional OpenAI-powered moderation for comments and guestbook posts. Set `OPENAI_API_KEY`. --- -## Database Architecture - -| Component | Type | Migrations | -| ------------------------------------------------- | ----------------------- | ----------------------------------- | -| Emulator tables (`users`, `items`, `rooms`, etc.) | Existing Polaris schema | None — read-only for CMS | -| CMS tables (`website_*`, `radio_*`, etc.) | CMS-owned | `prisma/migrations/*.sql` (9 files) | -| Migration tracking | `cms_migrations` table | Auto-created | - ---- - -## Project Structure +## Development +```bash +pnpm dev # Start with hot reload +pnpm typecheck # Type check all files +pnpm test # Run test suite (312+ tests) +pnpm analyze # Build and analyze bundle sizes +pnpm biome:check # Lint and format ``` -├── prisma/ -│ ├── schema.prisma # ~190 models (emulator + CMS) -│ └── migrations/ # 9 SQL migrations for CMS tables -├── scripts/ -│ ├── apply-migrations.ts # migration runner -│ └── jobs-worker.ts # background task scheduler -├── src/ -│ ├── app/ # Next.js App Router (pages & API routes) -│ ├── actions/ # Server Actions -│ ├── components/ # UI components (header, navigation, etc.) -│ ├── lib/ -│ │ ├── auth/ # NextAuth with password hashing & 2FA -│ │ ├── services/ # RCON, email, currency, PayPal, etc. -│ │ ├── prisma.ts # database connection singleton -│ │ └── redis.ts # Redis client abstraction -│ ├── messages/ # i18n translations (en, nl, de, fr, es, it) -│ └── env.ts # Zod-validated environment schema -├── public/assets/ # images, icons, fonts -├── .env.example # environment template -└── update-Nitrov3.sh # emulator & Nitro updater utility -``` + +### Contributing + +1. Ensure typecheck and tests pass: `pnpm typecheck && pnpm test` +2. Follow existing code conventions (Server Components where possible, minimal client boundaries) +3. Use the `src/lib/motion.ts` animation variants for consistent animations +4. SQL migrations in `prisma/migrations/` must be idempotent --- ## License -This project is licensed under the **CC BY-NC-SA 4.0** license. See `LICENSE` for details. +CC BY-NC-SA 4.0. See `LICENSE` for details. diff --git a/next.config.ts b/next.config.ts index 62e5a44b..ecb681eb 100644 --- a/next.config.ts +++ b/next.config.ts @@ -45,6 +45,9 @@ const nextConfig: NextConfig = { "pino-pretty", ], + // Enable React Compiler for automatic memoization + reactCompiler: true, + // Compress responses with gzip compress: true, diff --git a/src/app/api/home/route.ts b/src/app/api/home/route.ts index ca28bf5e..3d7d1ece 100644 --- a/src/app/api/home/route.ts +++ b/src/app/api/home/route.ts @@ -1,5 +1,6 @@ import { apiJson } from "@/lib/api"; import { prisma } from "@/lib/prisma"; +import { redisCache, apiCacheKey } from "@/lib/redis-cache"; import { siteSettings } from "@/lib/services/site-settings"; export const dynamic = "force-dynamic"; @@ -10,31 +11,32 @@ export const dynamic = "force-dynamic"; * used by the public pages and the admin dashboard. */ export async function GET(_req: Request) { - let articles: unknown[] = []; - let online = 0; - let hotelName = "Atom"; - try { - [articles, online, hotelName] = await Promise.all([ - prisma.websiteArticles.findMany({ - select: { - id: true, - title: true, - slug: true, - shortStory: true, - image: true, - createdAt: true, - }, - orderBy: { createdAt: "desc" }, - take: 4, - }), - prisma.user.count({ where: { online: "1" } }), - siteSettings.get("hotel_name", "Atom").then((v) => v ?? "Atom"), - ]); - - return apiJson({ articles, online, hotelName }); + const data = await redisCache( + apiCacheKey("home"), + 15, + async () => { + const [articles, online, hotelName] = await Promise.all([ + prisma.websiteArticles.findMany({ + select: { + id: true, + title: true, + slug: true, + shortStory: true, + image: true, + createdAt: true, + }, + orderBy: { createdAt: "desc" }, + take: 4, + }), + prisma.user.count({ where: { online: "1" } }), + siteSettings.get("hotel_name", "Atom").then((v) => v ?? "Atom"), + ]); + return { articles, online, hotelName }; + }, + ); + return apiJson(data); } catch { - // DB unavailable — return an empty payload instead of a 500. - return apiJson({ articles: [], online: 0, hotelName }, { status: 200 }); + return apiJson({ articles: [], online: 0, hotelName: "Atom" }, { status: 200 }); } } diff --git a/src/app/api/radio/config/route.ts b/src/app/api/radio/config/route.ts index 60c0a643..3009e5ec 100644 --- a/src/app/api/radio/config/route.ts +++ b/src/app/api/radio/config/route.ts @@ -1,5 +1,6 @@ import { apiJson } from "@/lib/api"; import { prisma } from "@/lib/prisma"; +import { redisCache, apiCacheKey } from "@/lib/redis-cache"; // Radio player config: the subset of radio_* website_settings the front-end // player needs (stream URL, name, autoplay, enabled, widget visibility) as a @@ -27,19 +28,23 @@ const CONFIG_KEYS = new Set([ export async function GET(_req: Request) { try { - const rows = await prisma.websiteSetting.findMany({ - where: { key: { in: Array.from(CONFIG_KEYS) } }, - select: { key: true, value: true }, - }); - - const config: Record = {}; - for (const row of rows) { - config[row.key] = row.value; - } - + const config = await redisCache( + apiCacheKey("radio:config"), + 30, + async () => { + const rows = await prisma.websiteSetting.findMany({ + where: { key: { in: Array.from(CONFIG_KEYS) } }, + select: { key: true, value: true }, + }); + const result: Record = {}; + for (const row of rows) { + result[row.key] = row.value; + } + return result; + }, + ); return apiJson(config); } catch { - // DB unavailable — serve an empty config rather than a 500. return apiJson({}, { status: 200 }); } } diff --git a/src/lib/redis-cache.ts b/src/lib/redis-cache.ts new file mode 100644 index 00000000..84561bc6 --- /dev/null +++ b/src/lib/redis-cache.ts @@ -0,0 +1,53 @@ +import "server-only"; + +import { redis } from "@/lib/redis"; + +/** + * Cache the result of a fetch function in Redis. + * Falls back to the fresh fetch if Redis is unavailable. + */ +export async function redisCache( + key: string, + ttlSeconds: number, + fetch: () => Promise, +): Promise { + if (!redis) return fetch(); + + try { + const cached = await redis.get(key); + if (cached !== null) { + return JSON.parse(cached) as T; + } + } catch { + // cache miss or error — fall through to fresh fetch + } + + const fresh = await fetch(); + + try { + await redis.setex(key, ttlSeconds, JSON.stringify(fresh)); + } catch { + // ignore write errors + } + + return fresh; +} + +/** + * Invalidate a cached key. No-op if Redis is unavailable. + */ +export async function invalidateCache(key: string): Promise { + if (!redis) return; + try { + await redis.del(key); + } catch { + // ignore + } +} + +/** + * Build a namespaced cache key for API routes. + */ +export function apiCacheKey(path: string): string { + return `api:${path}`; +}