feat: enable React Compiler, add Redis caching for API routes, update README with requirements and install guide
Local Build and Deploy / deploy (push) Failing after 57s
Local Build and Deploy / deploy (push) Failing after 57s
This commit is contained in:
1 parent
f53d25e0a5
commit
ccd4994028
5 files changed
+248
-267
No files matched your search
@@ -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:[email protected]:3306/epicnext_cms
|
||||
AUTH_SECRET= # minimum 32 characters, random
|
||||
AUTH_SECRET=<random 32+ character string>
|
||||
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
|
||||
[email protected]
|
||||
```
|
||||
|
||||
### 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/...
|
||||
[email protected]
|
||||
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.
|
||||
@@ -45,6 +45,9 @@ const nextConfig: NextConfig = {
|
||||
"pino-pretty",
|
||||
],
|
||||
|
||||
// Enable React Compiler for automatic memoization
|
||||
reactCompiler: true,
|
||||
|
||||
// Compress responses with gzip
|
||||
compress: true,
|
||||
|
||||
|
||||
+26
-24
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -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<T>(
|
||||
key: string,
|
||||
ttlSeconds: number,
|
||||
fetch: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
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<void> {
|
||||
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}`;
|
||||
}
|
||||
Reference in new issue
Block a user