From 851d82f93f33f68d2239ca031f02fb3549e0c4cc Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 6 May 2026 06:27:40 +0200 Subject: [PATCH] Document secure runtime configuration --- docs/local-development-setup.en.md | 279 +++++++++++++ docs/local-development-setup.md | 279 +++++++++++++ docs/secure-production-setup.en.md | 365 ++++++++++++++++ docs/secure-production-setup.md | 365 ++++++++++++++++ public/configuration/news.example | 12 + public/configuration/renderer-config.example | 140 ++++--- public/configuration/ui-config.example | 414 +++++++++++++------ src/bootstrap.ts | 24 ++ src/components/login/LoginView.tsx | 4 + 9 files changed, 1701 insertions(+), 181 deletions(-) create mode 100644 docs/local-development-setup.en.md create mode 100644 docs/local-development-setup.md create mode 100644 docs/secure-production-setup.en.md create mode 100644 docs/secure-production-setup.md create mode 100644 public/configuration/news.example diff --git a/docs/local-development-setup.en.md b/docs/local-development-setup.en.md new file mode 100644 index 0000000..b873850 --- /dev/null +++ b/docs/local-development-setup.en.md @@ -0,0 +1,279 @@ +# Local Development Setup with `yarn start` + +This guide explains how to run Nitro locally with Vite, using: + +- local UI on `http://localhost:5173`; +- local API/emulator on `http://localhost:2096`; +- local WebSocket on `ws://localhost:2096`; +- remote plain assets and gamedata, so you do not need to copy the full `client/nitro` folder locally. + +## 1. Start the emulator + +Inside `Arcturus-Morningstar-Extended/Emulator`, start the emulator with WebSocket enabled. + +Recommended local `config.ini` values: + +```ini +ws.enabled=true +ws.host=0.0.0.0 +ws.port=2096 +ws.whitelist=* +ws.ip.header= + +crypto.ws.enabled=0 + +nitro.secure.assets.enabled=false +nitro.secure.api.enabled=false +``` + +For local development, it is easier to disable: + +- `crypto.ws.enabled`; +- `nitro.secure.assets.enabled`; +- `nitro.secure.api.enabled`. + +This keeps debugging simple and avoids the secure runtime layer. + +## 2. `public/configuration/client-mode.json` + +File: + +```txt +Nitro-V3/public/configuration/client-mode.json +``` + +Recommended local config: + +```json +{ + "distObfuscationEnabled": true, + "secureAssetsEnabled": false, + "secureApiEnabled": false, + "apiBaseUrl": "http://localhost:2096", + "plainConfigBaseUrl": "http://localhost:5173/configuration/", + "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" +} +``` + +Notes: + +- `secureAssetsEnabled=false` avoids `/nitro-sec/file`. +- `secureApiEnabled=false` avoids encrypted `/api/*` requests. +- `apiBaseUrl` must point to your local emulator. +- `plainGamedataBaseUrl` can stay remote if you do not have gamedata copied locally. + +If you want everything local, use: + +```json +"plainGamedataBaseUrl": "http://localhost:5173/client/nitro/gamedata/" +``` + +but the files must really exist under: + +```txt +Nitro-V3/public/client/nitro/gamedata/ +``` + +## 3. `public/configuration/renderer-config.json` + +File: + +```txt +Nitro-V3/public/configuration/renderer-config.json +``` + +Minimum local values: + +```json +{ + "socket.url": "ws://localhost:2096", + "api.url": "http://localhost:2096", + "crypto.ws.enabled": false, + "gamedata.url": "https://hotel.example.com/client/nitro/gamedata", + "external.texts.url": [ + "${gamedata.url}/ExternalTexts.json", + "${gamedata.url}/UITexts.json" + ], + "furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%", + "productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%", + "login.endpoint": "${api.url}/api/auth/login", + "login.register.endpoint": "${api.url}/api/auth/register", + "login.forgot.endpoint": "${api.url}/api/auth/forgot-password", + "login.logout.endpoint": "${api.url}/api/auth/logout", + "login.remember.endpoint": "${api.url}/api/auth/remember", + "login.health.endpoint": "${api.url}/api/health", + "login.health.method": "GET", + "login.check-email.endpoint": "${api.url}/api/auth/check-email", + "login.check-username.endpoint": "${api.url}/api/auth/check-username", + "login.register.imaging.url": "${api.url}/api/avatar/imaging", + "login.news.url": "${api.url}/api/auth/news", + "badges.custom.list.endpoint": "${api.url}/api/badges/custom", + "badges.custom.create.endpoint": "${api.url}/api/badges/custom", + "badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts" +} +``` + +Important: + +- Do not use `https://localhost:2096/nitro-sec/file` locally if `secureAssetsEnabled=false`. +- Do not use `ws://192.168.x.x/:2096`; it is malformed. Use `ws://localhost:2096` or `ws://192.168.x.x:2096`. + +## 4. `public/configuration/ui-config.json` + +File: + +```txt +Nitro-V3/public/configuration/ui-config.json +``` + +For the login view, you can use remote plain images: + +```json +{ + "loginview": { + "images": { + "background": "https://hotel.example.com/client/nitro/images/reception/background_gradient_apr25.png", + "background.colour": "#6eadc8", + "drape": "https://hotel.example.com/client/nitro/images/reception/drape.png", + "left": "https://hotel.example.com/client/nitro/images/reception/mute_reception_backdrop_left.png", + "right": "https://hotel.example.com/client/nitro/images/reception/background_right.png" + } + } +} +``` + +If you see `ERR_NAME_NOT_RESOLVED`, the configured domain does not exist or is not reachable. + +## 5. Database-backed news + +Login news should come from the database through the emulator. + +In renderer config use: + +```json +"login.news.url": "${api.url}/api/auth/news" +``` + +The emulator reads from: + +```txt +ui_news +``` + +Reference SQL: + +```txt +Arcturus-Morningstar-Extended/Database Updates/013_UI_Client_News.sql +``` + +Main columns: + +- `title` +- `body` +- `image` +- `link_text` +- `link_url` +- `enabled` +- `sort_order` + +`public/configuration/news.json` can stay as a mock/fallback only, but it is not the correct production flow. + +## 6. Start Nitro + +Inside `Nitro-V3`: + +```bash +yarn start +``` + +Open: + +```txt +http://localhost:5173 +``` + +Recommendation: use `localhost`, not `192.168.x.x`, because cookies and API sessions are host-based and can otherwise cause `401 Unauthorized`. + +## 7. Common errors + +### `Unable to load renderer-config.json` + +Check: + +```txt +public/configuration/client-mode.json +``` + +It must contain: + +```json +"secureAssetsEnabled": false +``` + +### `Invalid JSON ... Unexpected token '<'` + +The client requested JSON, but Vite returned HTML. + +This happens when a URL points to a file that does not exist, for example: + +```txt +http://localhost:5173/client/nitro/gamedata/ExternalTexts.json +``` + +Fix: + +- use remote plain gamedata; +- or copy the gamedata files into `public/client/nitro/gamedata`. + +### WebSocket `1006` + +Check: + +```json +"socket.url": "ws://localhost:2096" +``` + +and emulator config: + +```ini +ws.enabled=true +ws.port=2096 +``` + +### Custom badges `401 Unauthorized` + +This is normal if you are not logged in or if you open Nitro from a different host. + +Use: + +```txt +http://localhost:5173 +``` + +and API: + +```txt +http://localhost:2096 +``` + +## 8. Difference from production + +Local `yarn start`: + +```html + +``` + +Production build: + +```html + +``` + +Do not mix the two flows. diff --git a/docs/local-development-setup.md b/docs/local-development-setup.md new file mode 100644 index 0000000..b788e2e --- /dev/null +++ b/docs/local-development-setup.md @@ -0,0 +1,279 @@ +# Setup locale con `yarn start` + +Questa guida serve per avviare Nitro in locale con Vite, usando: + +- UI locale su `http://localhost:5173`; +- API/emulatore locale su `http://localhost:2096`; +- WebSocket locale su `ws://localhost:2096`; +- asset e gamedata remoti plain, così non devi copiare tutta la cartella `client/nitro`. + +## 1. Avvia l'emulatore + +Nel repo `Arcturus-Morningstar-Extended/Emulator`, avvia l'emulatore con WebSocket attivo. + +Nel tuo `config.ini` locale usa valori tipo: + +```ini +ws.enabled=true +ws.host=0.0.0.0 +ws.port=2096 +ws.whitelist=* +ws.ip.header= + +crypto.ws.enabled=0 + +nitro.secure.assets.enabled=false +nitro.secure.api.enabled=false +``` + +Per il locale è meglio tenere spenti: + +- `crypto.ws.enabled`; +- `nitro.secure.assets.enabled`; +- `nitro.secure.api.enabled`. + +Così puoi debuggare senza layer secure in mezzo. + +## 2. `public/configuration/client-mode.json` + +File: + +```txt +Nitro-V3/public/configuration/client-mode.json +``` + +Config locale consigliato: + +```json +{ + "distObfuscationEnabled": true, + "secureAssetsEnabled": false, + "secureApiEnabled": false, + "apiBaseUrl": "http://localhost:2096", + "plainConfigBaseUrl": "http://localhost:5173/configuration/", + "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" +} +``` + +Note: + +- `secureAssetsEnabled=false` evita `/nitro-sec/file`. +- `secureApiEnabled=false` evita cifratura `/api/*`. +- `apiBaseUrl` deve puntare all'emulatore locale. +- `plainGamedataBaseUrl` può rimanere remoto se non hai gamedata copiato in locale. + +Se vuoi tutto locale, usa: + +```json +"plainGamedataBaseUrl": "http://localhost:5173/client/nitro/gamedata/" +``` + +ma devi avere davvero i file sotto: + +```txt +Nitro-V3/public/client/nitro/gamedata/ +``` + +## 3. `public/configuration/renderer-config.json` + +File: + +```txt +Nitro-V3/public/configuration/renderer-config.json +``` + +Valori minimi locali: + +```json +{ + "socket.url": "ws://localhost:2096", + "api.url": "http://localhost:2096", + "crypto.ws.enabled": false, + "gamedata.url": "https://hotel.example.com/client/nitro/gamedata", + "external.texts.url": [ + "${gamedata.url}/ExternalTexts.json", + "${gamedata.url}/UITexts.json" + ], + "furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%", + "productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%", + "login.endpoint": "${api.url}/api/auth/login", + "login.register.endpoint": "${api.url}/api/auth/register", + "login.forgot.endpoint": "${api.url}/api/auth/forgot-password", + "login.logout.endpoint": "${api.url}/api/auth/logout", + "login.remember.endpoint": "${api.url}/api/auth/remember", + "login.health.endpoint": "${api.url}/api/health", + "login.health.method": "GET", + "login.check-email.endpoint": "${api.url}/api/auth/check-email", + "login.check-username.endpoint": "${api.url}/api/auth/check-username", + "login.register.imaging.url": "${api.url}/api/avatar/imaging", + "login.news.url": "${api.url}/api/auth/news", + "badges.custom.list.endpoint": "${api.url}/api/badges/custom", + "badges.custom.create.endpoint": "${api.url}/api/badges/custom", + "badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts" +} +``` + +Importante: + +- Non usare `https://localhost:2096/nitro-sec/file` in locale se `secureAssetsEnabled=false`. +- Non usare `ws://192.168.x.x/:2096`: è malformato. Usa `ws://localhost:2096` oppure `ws://192.168.x.x:2096`. + +## 4. `public/configuration/ui-config.json` + +File: + +```txt +Nitro-V3/public/configuration/ui-config.json +``` + +Per la login view puoi usare immagini remote plain: + +```json +{ + "loginview": { + "images": { + "background": "https://hotel.example.com/client/nitro/images/reception/background_gradient_apr25.png", + "background.colour": "#6eadc8", + "drape": "https://hotel.example.com/client/nitro/images/reception/drape.png", + "left": "https://hotel.example.com/client/nitro/images/reception/mute_reception_backdrop_left.png", + "right": "https://hotel.example.com/client/nitro/images/reception/background_right.png" + } + } +} +``` + +Se vedi `ERR_NAME_NOT_RESOLVED`, il dominio configurato non esiste o non è raggiungibile. + +## 5. News dal database + +Le news della login devono arrivare dal database tramite l'emulatore. + +Nel renderer config usa: + +```json +"login.news.url": "${api.url}/api/auth/news" +``` + +L'emulatore legge dalla tabella: + +```txt +ui_news +``` + +SQL di riferimento: + +```txt +Arcturus-Morningstar-Extended/Database Updates/013_UI_Client_News.sql +``` + +Colonne principali: + +- `title` +- `body` +- `image` +- `link_text` +- `link_url` +- `enabled` +- `sort_order` + +`public/configuration/news.json` può rimanere solo come mock/fallback, ma non è il flow corretto. + +## 6. Avvio Nitro + +Nel repo `Nitro-V3`: + +```bash +yarn start +``` + +Apri: + +```txt +http://localhost:5173 +``` + +Consiglio: usa `localhost`, non `192.168.x.x`, perché cookie e sessioni API possono cambiare host e causare `401 Unauthorized`. + +## 7. Errori comuni + +### `Unable to load renderer-config.json` + +Controlla: + +```txt +public/configuration/client-mode.json +``` + +Deve avere: + +```json +"secureAssetsEnabled": false +``` + +### `Invalid JSON ... Unexpected token '<'` + +Vuol dire che il client ha chiesto un JSON, ma Vite ha risposto HTML. + +Succede quando un URL punta a un file che non esiste, per esempio: + +```txt +http://localhost:5173/client/nitro/gamedata/ExternalTexts.json +``` + +Soluzione: + +- usa gamedata remoto plain; +- oppure copia davvero i gamedata in `public/client/nitro/gamedata`. + +### WebSocket `1006` + +Controlla: + +```json +"socket.url": "ws://localhost:2096" +``` + +e nel config emulator: + +```ini +ws.enabled=true +ws.port=2096 +``` + +### Custom badges `401 Unauthorized` + +È normale se non sei loggato o se apri Nitro da un host diverso. + +Usa: + +```txt +http://localhost:5173 +``` + +e API: + +```txt +http://localhost:2096 +``` + +## 8. Differenza con produzione + +Locale con `yarn start`: + +```html + +``` + +Produzione buildata: + +```html + +``` + +Non mischiare i due flow. diff --git a/docs/secure-production-setup.en.md b/docs/secure-production-setup.en.md new file mode 100644 index 0000000..ab86bfd --- /dev/null +++ b/docs/secure-production-setup.en.md @@ -0,0 +1,365 @@ +# Secure Runtime Production Setup + +Quick setup guide for running Nitro with: + +- configuration and gamedata served through `/nitro-sec/file`; +- encrypted runtime `/api/*` calls; +- obfuscated production bundles loaded as `.dat`. + +Replace the example domains with your real domains: + +- `https://hotel.example.com` +- `https://nitro.example.com:2096` + +## 1. Build Nitro + +Inside the `Nitro-V3` repository: + +```bash +yarn build +``` + +Then publish the `dist` folder to your web server, for example: + +```txt +C:/inetpub/wwwroot/hotel/nitro +``` + +The deployed folder should contain at least: + +```txt +configuration/ +assets/ +asset-loader.js +index.html +src/ +``` + +## 2. `configuration/client-mode.json` + +File: + +```txt +Nitro-V3/dist/configuration/client-mode.json +``` + +Secure production configuration: + +```json +{ + "distObfuscationEnabled": true, + "secureAssetsEnabled": true, + "secureApiEnabled": true, + "apiBaseUrl": "https://nitro.example.com:2096", + "plainConfigBaseUrl": "https://hotel.example.com/configuration/", + "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" +} +``` + +Meaning: + +- `distObfuscationEnabled: true` loads `app.js.dat` and `app.css.dat`. +- `secureAssetsEnabled: true` loads `renderer-config.json`, `ui-config.json`, and gamedata through `/nitro-sec/file`. +- `secureApiEnabled: true` automatically encrypts `/api/*` requests. +- `apiBaseUrl` must point to the emulator/API. +- `plainConfigBaseUrl` and `plainGamedataBaseUrl` are fallbacks when secure assets are disabled. + +## 3. `configuration/renderer-config.json` + +File: + +```txt +Nitro-V3/dist/configuration/renderer-config.json +``` + +Important values: + +```json +{ + "socket.url": "wss://nitro.example.com:2096", + "api.url": "https://nitro.example.com:2096", + "gamedata.url": "https://nitro.example.com:2096/nitro-sec/file?kind=gamedata&file=", + "external.texts.url": [ + "${gamedata.url}/ExternalTexts.json", + "${gamedata.url}/UITexts.json" + ], + "furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%", + "productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%", + "crypto.ws.enabled": true +} +``` + +If you are not using WebSocket crypto yet, use: + +```json +"crypto.ws.enabled": false +``` + +## 4. `configuration/ui-config.json` + +File: + +```txt +Nitro-V3/dist/configuration/ui-config.json +``` + +Static image and camera URLs can remain plain: + +```json +{ + "camera.url": "https://hotel.example.com/client/camera/", + "thumbnails.url": "https://hotel.example.com/client/camera/thumbnail/%thumbnail%.png" +} +``` + +Non-sensitive images can stay static. JSON configuration and gamedata should go through the secure endpoint. + +## 5. Emulator `config.ini` + +Inside `Arcturus-Morningstar-Extended`, edit the emulator config: + +```txt +Emulator/config.ini +``` + +Production example: + +```ini +ws.enabled=true +ws.host=0.0.0.0 +ws.port=2096 +ws.whitelist=https://hotel.example.com +ws.ip.header=CF-Connecting-IP + +crypto.ws.enabled=1 + +nitro.secure.assets.enabled=true +nitro.secure.api.enabled=true +nitro.secure.config.root=C:/inetpub/wwwroot/hotel/nitro/configuration +nitro.secure.gamedata.root=C:/inetpub/wwwroot/hotel/nitro/client/nitro/gamedata +nitro.secure.master_key=change-this-to-a-long-random-secret + +login.remember.enabled=true +login.remember.duration.days=30 +login.remember.jwt.secret=change-this-too-if-you-use-remember-me +``` + +Notes: + +- `nitro.secure.config.root` must point to the folder containing `renderer-config.json`, `ui-config.json`, and `client-mode.json`. +- `nitro.secure.gamedata.root` must point to the live gamedata folder. +- Files are read live from disk: if you update a JSON file, a new browser refresh reads the new version. +- `nitro.secure.master_key` must be secret and stable. Never put it in public files. + +## 6. Cloudflare + +If you use Cloudflare: + +1. Keep the proxy enabled for the website domain `hotel.example.com`. +2. Make sure Cloudflare supports/proxies the port used by `nitro.example.com:2096`. +3. Always use HTTPS/WSS in the browser: + +```json +"api.url": "https://nitro.example.com:2096", +"socket.url": "wss://nitro.example.com:2096" +``` + +If you get CORS errors, check: + +```ini +ws.whitelist=https://hotel.example.com +``` + +## 7. IIS / `.dat` MIME type + +If obfuscated `.dat` assets are enabled, IIS must serve them correctly. + +Add this MIME type: + +```txt +Extension: .dat +MIME type: application/octet-stream +``` + +Without it, the browser can receive 404 even when the file exists. + +## 8. Final checklist + +- `client-mode.json` has `secureAssetsEnabled=true`. +- `client-mode.json` has `secureApiEnabled=true`. +- `renderer-config.json` uses `/nitro-sec/file?kind=gamedata&file=`. +- `api.url` points to `https://nitro.example.com:2096`. +- `socket.url` points to `wss://nitro.example.com:2096`. +- `config.ini` has the correct `nitro.secure.config.root`. +- `config.ini` has the correct `nitro.secure.gamedata.root`. +- `config.ini` has a stable `nitro.secure.master_key`. +- IIS knows the `.dat` MIME type. +- Restart the emulator after changing `config.ini`. +- Refresh the browser after changing JSON files in `configuration` or `gamedata`. + +## 9. Temporarily disable secure mode + +For quick debugging, only change `client-mode.json`: + +```json +{ + "distObfuscationEnabled": false, + "secureAssetsEnabled": false, + "secureApiEnabled": false, + "apiBaseUrl": "https://nitro.example.com:2096", + "plainConfigBaseUrl": "https://hotel.example.com/configuration/", + "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" +} +``` + +Then hard refresh the browser. + +## 10. `configuration/bootstrap.js` + +File: + +```txt +Nitro-V3/dist/configuration/bootstrap.js +``` + +This is the first loader when you use the external secure mode. + +It does three things: + +1. opens an ECDH session with the emulator through `/nitro-sec/bootstrap`; +2. downloads encrypted `client-mode.json` through `/nitro-sec/file?kind=config`; +3. downloads encrypted `asset-loader.js` and imports it as a JavaScript module. + +### Value to check + +Inside `bootstrap.js` there is: + +```js +const API_BASE = "https://nitro.example.com:2096"; +``` + +It must point to your public emulator/API URL. + +In production: + +```js +const API_BASE = "https://nitro.example.com:2096"; +``` + +In local development: + +```js +const API_BASE = "http://localhost:2096"; +``` + +If `bootstrap.js` fails, it automatically falls back to the plain loader: + +```txt +configuration/asset-loader.js +``` + +So `asset-loader.js` must always exist inside the `configuration` folder. + +## 11. `configuration/asset-loader.js` + +File: + +```txt +Nitro-V3/dist/configuration/asset-loader.js +``` + +This loader loads the actual bundle: + +- if `distObfuscationEnabled=true` + - it loads `app.css.dat`; + - it loads `app.js.dat`; + - it decodes, decompresses, and imports the bundle from a blob. + +- if `distObfuscationEnabled=false` + - it loads `assets/app.css`; + - it loads `assets/app.js`. + +### Required files in production + +With obfuscation enabled, these files must exist: + +```txt +assets/app.css.dat +assets/app.js.dat +configuration/asset-loader.js +configuration/bootstrap.js +configuration/client-mode.json +``` + +With obfuscation disabled, these files must exist: + +```txt +assets/app.css +assets/app.js +configuration/asset-loader.js +configuration/client-mode.json +``` + +## 12. `index.html` + +`index.html` should stay minimal. + +Secure production example: + +```html +
+ +``` + +Vite development example: + +```html +
+ +``` + +Do not mix the two flows: + +- production build: use `configuration/bootstrap.js`; +- `yarn start` development: use `/src/bootstrap.ts`. + +## 13. Files inside `/configuration` + +The `configuration` folder should contain: + +```txt +asset-loader.js +bootstrap.js +client-mode.json +renderer-config.json +ui-config.json +adsense.json optional +hotlooks.json if register hot looks are enabled +UITexts.json if separate UI texts are enabled +``` + +Login news should not live in `news.json` in production. They come from the database through: + +```json +"login.news.url": "${api.url}/api/auth/news" +``` + +The emulator reads from the `ui_news` table. + +With `secureAssetsEnabled=true`, client-loaded files go through: + +```txt +https://nitro.example.com:2096/nitro-sec/file?kind=config&file=... +``` + +The emulator reads them from: + +```ini +nitro.secure.config.root=C:/inetpub/wwwroot/hotel/nitro/configuration +``` + +If you add new JSON/JS files inside `configuration` and want to protect them, they must be requested through the secure endpoint or loaded through `bootstrap.js`. diff --git a/docs/secure-production-setup.md b/docs/secure-production-setup.md new file mode 100644 index 0000000..eb76a6a --- /dev/null +++ b/docs/secure-production-setup.md @@ -0,0 +1,365 @@ +# Setup Secure Runtime in produzione + +Guida rapida per avviare Nitro con: + +- configurazioni e gamedata serviti da `/nitro-sec/file`; +- API `/api/*` cifrate dal wrapper runtime; +- bundle buildati offuscati come `.dat`. + +Negli esempi usa i tuoi domini reali al posto di: + +- `https://hotel.example.com` +- `https://nitro.example.com:2096` + +## 1. Build Nitro + +Nel repo `Nitro-V3`: + +```bash +yarn build +``` + +Poi pubblica la cartella `dist` nel web server del sito, ad esempio: + +```txt +C:/inetpub/wwwroot/hotel/nitro +``` + +La struttura pubblicata deve contenere almeno: + +```txt +configuration/ +assets/ +asset-loader.js +index.html +src/ +``` + +## 2. `configuration/client-mode.json` + +File: + +```txt +Nitro-V3/dist/configuration/client-mode.json +``` + +Configurazione produzione secure: + +```json +{ + "distObfuscationEnabled": true, + "secureAssetsEnabled": true, + "secureApiEnabled": true, + "apiBaseUrl": "https://nitro.example.com:2096", + "plainConfigBaseUrl": "https://hotel.example.com/configuration/", + "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" +} +``` + +Significato: + +- `distObfuscationEnabled: true` carica `app.js.dat` e `app.css.dat`. +- `secureAssetsEnabled: true` carica `renderer-config.json`, `ui-config.json` e gamedata da `/nitro-sec/file`. +- `secureApiEnabled: true` cifra automaticamente le chiamate `/api/*`. +- `apiBaseUrl` deve puntare all'emulatore/API. +- `plainConfigBaseUrl` e `plainGamedataBaseUrl` restano fallback quando spegni secure assets. + +## 3. `configuration/renderer-config.json` + +File: + +```txt +Nitro-V3/dist/configuration/renderer-config.json +``` + +Valori importanti: + +```json +{ + "socket.url": "wss://nitro.example.com:2096", + "api.url": "https://nitro.example.com:2096", + "gamedata.url": "https://nitro.example.com:2096/nitro-sec/file?kind=gamedata&file=", + "external.texts.url": [ + "${gamedata.url}/ExternalTexts.json", + "${gamedata.url}/UITexts.json" + ], + "furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%", + "productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%", + "crypto.ws.enabled": true +} +``` + +Se non usi ancora WebSocket crypto, metti: + +```json +"crypto.ws.enabled": false +``` + +## 4. `configuration/ui-config.json` + +File: + +```txt +Nitro-V3/dist/configuration/ui-config.json +``` + +Qui puoi lasciare immagini e camera su URL statici normali: + +```json +{ + "camera.url": "https://hotel.example.com/client/camera/", + "thumbnails.url": "https://hotel.example.com/client/camera/thumbnail/%thumbnail%.png" +} +``` + +Le immagini non sensibili possono rimanere statiche. I JSON/gamedata invece passano dal secure endpoint. + +## 5. `config.ini` dell'emulatore + +Nel repo `Arcturus-Morningstar-Extended`, file usato dall'emulatore: + +```txt +Emulator/config.ini +``` + +Esempio produzione: + +```ini +ws.enabled=true +ws.host=0.0.0.0 +ws.port=2096 +ws.whitelist=https://hotel.example.com +ws.ip.header=CF-Connecting-IP + +crypto.ws.enabled=1 + +nitro.secure.assets.enabled=true +nitro.secure.api.enabled=true +nitro.secure.config.root=C:/inetpub/wwwroot/hotel/nitro/configuration +nitro.secure.gamedata.root=C:/inetpub/wwwroot/hotel/nitro/client/nitro/gamedata +nitro.secure.master_key=change-this-to-a-long-random-secret + +login.remember.enabled=true +login.remember.duration.days=30 +login.remember.jwt.secret=change-this-too-if-you-use-remember-me +``` + +Note: + +- `nitro.secure.config.root` deve puntare alla cartella dove ci sono `renderer-config.json`, `ui-config.json`, `client-mode.json`. +- `nitro.secure.gamedata.root` deve puntare alla cartella live dei gamedata. +- I file vengono letti live da disco: se cambi un JSON, un nuovo refresh pagina legge la nuova versione. +- `nitro.secure.master_key` deve restare segreta e stabile. Non metterla nei file pubblici. + +## 6. Cloudflare + +Se usi Cloudflare: + +1. Lascia la nuvoletta attiva sul dominio web `hotel.example.com`. +2. Per `nitro.example.com:2096`, assicurati che Cloudflare supporti/proxy il traffico sulla porta usata. +3. Usa sempre HTTPS/WSS lato browser: + +```json +"api.url": "https://nitro.example.com:2096", +"socket.url": "wss://nitro.example.com:2096" +``` + +Se vedi errori CORS, controlla: + +```ini +ws.whitelist=https://hotel.example.com +``` + +## 7. IIS / MIME `.dat` + +Se usi gli asset offuscati `.dat`, IIS deve servirli. + +Aggiungi MIME type: + +```txt +Extension: .dat +MIME type: application/octet-stream +``` + +Senza questo, il browser può dare 404 anche se il file esiste davvero. + +## 8. Checklist finale + +- `client-mode.json` ha `secureAssetsEnabled=true`. +- `client-mode.json` ha `secureApiEnabled=true`. +- `renderer-config.json` usa `/nitro-sec/file?kind=gamedata&file=`. +- `api.url` punta a `https://nitro.example.com:2096`. +- `socket.url` punta a `wss://nitro.example.com:2096`. +- `config.ini` ha `nitro.secure.config.root` corretto. +- `config.ini` ha `nitro.secure.gamedata.root` corretto. +- `config.ini` ha `nitro.secure.master_key` stabile. +- IIS conosce il MIME `.dat`. +- Dopo modifiche a `config.ini`, riavvia l'emulatore. +- Dopo modifiche ai JSON in `configuration` o `gamedata`, basta refresh pagina. + +## 9. Spegnere temporaneamente secure + +Per debug rapido, cambia solo `client-mode.json`: + +```json +{ + "distObfuscationEnabled": false, + "secureAssetsEnabled": false, + "secureApiEnabled": false, + "apiBaseUrl": "https://nitro.example.com:2096", + "plainConfigBaseUrl": "https://hotel.example.com/configuration/", + "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" +} +``` + +Poi fai hard refresh. + +## 10. `configuration/bootstrap.js` + +File: + +```txt +Nitro-V3/dist/configuration/bootstrap.js +``` + +Questo è il primo loader quando usi la modalità secure esterna. + +Fa tre cose: + +1. apre una sessione ECDH con l'emulatore tramite `/nitro-sec/bootstrap`; +2. scarica `client-mode.json` cifrato da `/nitro-sec/file?kind=config`; +3. scarica `asset-loader.js` cifrato e lo importa come modulo JavaScript. + +### Valore da controllare + +Dentro `bootstrap.js` esiste: + +```js +const API_BASE = "https://nitro.example.com:2096"; +``` + +Deve puntare all'emulatore/API pubblico. + +In produzione: + +```js +const API_BASE = "https://nitro.example.com:2096"; +``` + +In locale: + +```js +const API_BASE = "http://localhost:2096"; +``` + +Se `bootstrap.js` fallisce, prova automaticamente fallback plain su: + +```txt +configuration/asset-loader.js +``` + +Quindi `asset-loader.js` deve esistere sempre nella cartella `configuration`. + +## 11. `configuration/asset-loader.js` + +File: + +```txt +Nitro-V3/dist/configuration/asset-loader.js +``` + +Questo loader carica il bundle vero: + +- se `distObfuscationEnabled=true` + - carica `app.css.dat`; + - carica `app.js.dat`; + - decodifica, decomprime e importa il bundle da blob. + +- se `distObfuscationEnabled=false` + - carica `assets/app.css`; + - carica `assets/app.js`. + +### File richiesti in produzione + +Con offuscamento attivo devono esistere: + +```txt +assets/app.css.dat +assets/app.js.dat +configuration/asset-loader.js +configuration/bootstrap.js +configuration/client-mode.json +``` + +Con offuscamento spento devono esistere: + +```txt +assets/app.css +assets/app.js +configuration/asset-loader.js +configuration/client-mode.json +``` + +## 12. `index.html` + +Il file `index.html` deve rimanere minimale. + +Esempio secure: + +```html +
+ +``` + +Esempio dev Vite: + +```html +
+ +``` + +Non mischiare i due flow: + +- produzione buildata: usa `configuration/bootstrap.js`; +- sviluppo con `yarn start`: usa `/src/bootstrap.ts`. + +## 13. File dentro `/configuration` + +La cartella `configuration` deve contenere: + +```txt +asset-loader.js +bootstrap.js +client-mode.json +renderer-config.json +ui-config.json +adsense.json opzionale +hotlooks.json se usi register hot looks +UITexts.json se usi testi UI separati +``` + +Le news login non devono stare in `news.json` in produzione: arrivano dal database tramite: + +```json +"login.news.url": "${api.url}/api/auth/news" +``` + +L'emulatore legge dalla tabella `ui_news`. + +Con `secureAssetsEnabled=true`, i file letti dal client passano da: + +```txt +https://nitro.example.com:2096/nitro-sec/file?kind=config&file=... +``` + +Quindi l'emulatore li legge da: + +```ini +nitro.secure.config.root=C:/inetpub/wwwroot/hotel/nitro/configuration +``` + +Se aggiungi nuovi file JSON/JS in `configuration` e vuoi proteggerli, devono essere richiesti passando dal secure endpoint o caricati tramite `bootstrap.js`. diff --git a/public/configuration/news.example b/public/configuration/news.example new file mode 100644 index 0000000..4003700 --- /dev/null +++ b/public/configuration/news.example @@ -0,0 +1,12 @@ +{ + "news": [ + { + "id": 1, + "title": "Welcome to Nitro", + "body": "This news entry is loaded from configuration/news.json.", + "image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png", + "link": "", + "linkText": "Read more" + } + ] +} diff --git a/public/configuration/renderer-config.example b/public/configuration/renderer-config.example index 4a294f2..8ee77b6 100644 --- a/public/configuration/renderer-config.example +++ b/public/configuration/renderer-config.example @@ -1,53 +1,91 @@ { - "socket.url": "wss://nitro.example.com:2096", - "api.url": "https://nitro.example.com:2096", - "asset.url": "https://hotel.example.com/client/nitro/bundled", - "image.library.url": "https://hotel.example.com/client/c_images/", - "hof.furni.url": "https://hotel.example.com/client/c_images/dcr/hof_furni", - "images.url": "https://hotel.example.com/client/nitro/images", - "gamedata.url": "https://nitro.example.com:2096/nitro-sec/file?kind=gamedata&file=", - "sounds.url": "${asset.url}/sounds/%sample%.mp3", - "external.texts.url": [ - "${gamedata.url}/ExternalTexts.json", - "${gamedata.url}/UITexts.json" - ], - "external.texts.translation.url": "${gamedata.url}/text_translate/ExternalTexts_%locale%.json?t=%timestamp%", - "external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3", - "furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%", - "furnidata.translation.url": "${gamedata.url}/furniture_translate/FurnitureData_%locale%.json?t=%timestamp%", - "productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%", - "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%", - "avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%", - "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%", - "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%", - "avatar.asset.url": "${asset.url}/figure/%libname%.nitro", - "avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro", - "furni.asset.url": "${asset.url}/furniture/%libname%.nitro", - "furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png", - "pet.asset.url": "${asset.url}/pets/%libname%.nitro", - "generic.asset.url": "${asset.url}/generic/%libname%.nitro", - "badge.asset.url": "${image.library.url}album1584/%badgename%.gif", - "furni.rotation.bounce.steps": 20, - "furni.rotation.bounce.height": 0.0625, - "enable.avatar.arrow": false, - "system.log.debug": true, - "system.log.warn": true, - "system.log.error": true, - "system.log.events": false, - "system.log.packets": true, - "system.fps.animation": 24, - "system.fps.max": 60, - "system.pong.manually": true, - "system.pong.interval.ms": 20000, - "room.color.skip.transition": true, - "room.landscapes.enabled": true, - "room.zoom.enabled": true, - "login.screen.enabled": true, - "login.endpoint": "${api.url}/api/auth/login", - "login.register.endpoint": "${api.url}/api/auth/register", - "login.forgot.endpoint": "${api.url}/api/auth/forgot-password", - "login.logout.endpoint": "${api.url}/api/auth/logout", - "login.remember.endpoint": "${api.url}/api/auth/remember", - "login.turnstile.enabled": false, - "login.turnstile.sitekey": "" + "socket.url": "wss://nitro.example.com:2096", + "api.url": "https://nitro.example.com:2096", + "asset.url": "https://hotel.example.com/client/nitro/bundled", + "image.library.url": "https://hotel.example.com/client/c_images/", + "hof.furni.url": "https://hotel.example.com/client/c_images/dcr/hof_furni", + "images.url": "https://hotel.example.com/client/nitro/images", + "gamedata.url": "https://nitro.example.com:2096/nitro-sec/file?kind=gamedata&file=", + "sounds.url": "${asset.url}/sounds/%sample%.mp3", + "external.texts.url": [ + "${gamedata.url}/ExternalTexts.json", + "${gamedata.url}/UITexts.json" + ], + "external.texts.translation.url": "${gamedata.url}/text_translate/ExternalTexts_%locale%.json?t=%timestamp%", + "external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3", + "furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%", + "furnidata.translation.url": "${gamedata.url}/furniture_translate/FurnitureData_%locale%.json?t=%timestamp%", + "productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%", + "avatar.asset.url": "${asset.url}/figure/%libname%.nitro", + "avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro", + "furni.asset.url": "${asset.url}/furniture/%libname%.nitro", + "furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png", + "pet.asset.url": "${asset.url}/pets/%libname%.nitro", + "generic.asset.url": "${asset.url}/generic/%libname%.nitro", + "badge.asset.url": "${image.library.url}album1584/%badgename%.gif", + "furni.rotation.bounce.steps": 20, + "furni.rotation.bounce.height": 0.0625, + "enable.avatar.arrow": false, + "system.log.debug": true, + "system.log.warn": true, + "system.log.error": true, + "system.log.events": false, + "system.log.packets": true, + "system.fps.animation": 24, + "system.fps.max": 60, + "system.pong.manually": true, + "system.pong.interval.ms": 20000, + "room.color.skip.transition": true, + "room.landscapes.enabled": true, + "room.zoom.enabled": true, + "login.screen.enabled": true, + "login.endpoint": "${api.url}/api/auth/login", + "login.register.endpoint": "${api.url}/api/auth/register", + "login.forgot.endpoint": "${api.url}/api/auth/forgot-password", + "login.logout.endpoint": "${api.url}/api/auth/logout", + "login.remember.endpoint": "${api.url}/api/auth/remember", + "login.turnstile.enabled": false, + "login.turnstile.sitekey": "", + "avatar.mandatory.libraries": [ + "bd:1", + "li:0" + ], + "avatar.mandatory.effect.libraries": [ + "dance.1", + "dance.2", + "dance.3", + "dance.4" + ], + "avatar.default.figuredata": { + "palettes": [], + "setTypes": [] + }, + "avatar.default.actions": { + "actions": [] + }, + "pet.types": [], + "preload.assets.urls": [ + "${asset.url}/generic/avatar_additions.nitro", + "${asset.url}/generic/group_badge.nitro", + "${asset.url}/generic/floor_editor.nitro", + "${images.url}/loading_icon.png", + "${images.url}/clear_icon.png", + "${images.url}/big_arrow.png" + ], + "login.health.endpoint": "${api.url}/api/health", + "login.health.method": "GET", + "login.check-email.endpoint": "${api.url}/api/auth/check-email", + "login.check-username.endpoint": "${api.url}/api/auth/check-username", + "login.register.imaging.url": "${api.url}/api/avatar/imaging", + "crypto.ws.enabled": true, + "login.news.url": "${api.url}/api/auth/news", + "badges.custom.list.endpoint": "${api.url}/api/badges/custom", + "badges.custom.create.endpoint": "${api.url}/api/badges/custom", + "badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts" } diff --git a/public/configuration/ui-config.example b/public/configuration/ui-config.example index 7510024..5503c2c 100644 --- a/public/configuration/ui-config.example +++ b/public/configuration/ui-config.example @@ -37,145 +37,202 @@ "left": "${asset.url}/c_images/reception/ts.png", "right": "${asset.url}/c_images/reception/US_right.png", "right.repeat": "${asset.url}/c_images/reception/US_top_right.png" + }, + "widgets": { + "slot.1.widget": "promoarticle", + "slot.1.conf": {}, + "slot.2.widget": "widgetcontainer", + "slot.2.conf": { + "image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png", + "texts": "2021NitroPromo", + "btnLink": "" + }, + "slot.3.widget": "", + "slot.3.conf": {}, + "slot.4.widget": "", + "slot.4.conf": {}, + "slot.5.widget": "", + "slot.5.conf": {}, + "slot.6.widget": "", + "slot.6.conf": { + "campaign": "" + }, + "slot.7.widget": "", + "slot.7.conf": {} } }, - "navigator.room.models": [{ + "navigator.room.models": [ + { "clubLevel": 0, "tileSize": 104, "name": "a" - }, { + }, + { "clubLevel": 0, "tileSize": 94, "name": "b" - }, { + }, + { "clubLevel": 0, "tileSize": 36, "name": "c" - }, { + }, + { "clubLevel": 0, "tileSize": 84, "name": "d" - }, { + }, + { "clubLevel": 0, "tileSize": 80, "name": "e" - }, { + }, + { "clubLevel": 0, "tileSize": 80, "name": "f" - }, { + }, + { "clubLevel": 0, "tileSize": 416, "name": "i" - }, { + }, + { "clubLevel": 0, "tileSize": 320, "name": "j" - }, { + }, + { "clubLevel": 0, "tileSize": 448, "name": "k" - }, { + }, + { "clubLevel": 0, "tileSize": 352, "name": "l" - }, { + }, + { "clubLevel": 0, "tileSize": 384, "name": "m" - }, { + }, + { "clubLevel": 0, "tileSize": 372, "name": "n" - }, { + }, + { "clubLevel": 1, "tileSize": 80, "name": "g" - }, { + }, + { "clubLevel": 1, "tileSize": 74, "name": "h" - }, { + }, + { "clubLevel": 1, "tileSize": 416, "name": "o" - }, { + }, + { "clubLevel": 1, "tileSize": 352, "name": "p" - }, { + }, + { "clubLevel": 1, "tileSize": 304, "name": "q" - }, { + }, + { "clubLevel": 1, "tileSize": 336, "name": "r" - }, { + }, + { "clubLevel": 1, "tileSize": 748, "name": "u" - }, { + }, + { "clubLevel": 1, "tileSize": 438, "name": "v" - }, { + }, + { "clubLevel": 2, "tileSize": 540, "name": "t" - }, { + }, + { "clubLevel": 2, "tileSize": 512, "name": "w" - }, { + }, + { "clubLevel": 2, "tileSize": 396, "name": "x" - }, { + }, + { "clubLevel": 2, "tileSize": 440, "name": "y" - }, { + }, + { "clubLevel": 2, "tileSize": 456, "name": "z" - }, { + }, + { "clubLevel": 2, "tileSize": 208, "name": "0" - }, { + }, + { "clubLevel": 2, "tileSize": 1009, "name": "1" - }, { + }, + { "clubLevel": 2, "tileSize": 1044, "name": "2" - }, { + }, + { "clubLevel": 2, "tileSize": 183, "name": "3" - }, { + }, + { "clubLevel": 2, "tileSize": 254, "name": "4" - }, { + }, + { "clubLevel": 2, "tileSize": 1024, "name": "5" - }, { + }, + { "clubLevel": 2, "tileSize": 801, "name": "6" - }, { + }, + { "clubLevel": 2, "tileSize": 354, "name": "7" - }, { + }, + { "clubLevel": 2, "tileSize": 888, "name": "8" - }, { + }, + { "clubLevel": 2, "tileSize": 926, "name": "9" @@ -260,325 +317,379 @@ "catalog.headers": false, "chat.input.maxlength": 100, "chat.styles.disabled": [], - "chat.styles": [{ + "chat.styles": [ + { "styleId": 0, "minRank": 0, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 1, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 2, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 3, "minRank": 0, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 4, "minRank": 0, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 5, "minRank": 0, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 6, "minRank": 0, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 7, "minRank": 0, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 8, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 9, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 10, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 11, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 12, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 13, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 14, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 15, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 16, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 17, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 18, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 19, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 20, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 21, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 22, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 23, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 24, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 25, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 26, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 27, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 28, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 29, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 30, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 31, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 32, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 33, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 34, "minRank": 5, "isSystemStyle": true, "isHcOnly": false, "isAmbassadorOnly": false - }, { + }, + { "styleId": 35, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 36, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 37, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 38, "minRank": 0, "isSystemStyle": false, "isHcOnly": true, "isAmbassadorOnly": false - }, { + }, + { "styleId": 39, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 40, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 41, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 42, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 43, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 44, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 45, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 46, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 47, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 48, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 49, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 50, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 51, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 52, "minRank": 5, "isSystemStyle": false, "isHcOnly": false, "isAmbassadorOnly": true - }, { + }, + { "styleId": 53, "minRank": 5, "isSystemStyle": false, @@ -586,7 +697,8 @@ "isAmbassadorOnly": true } ], - "camera.available.effects": [{ + "camera.available.effects": [ + { "name": "dark_sepia", "colorMatrix": [ 0.4, @@ -612,7 +724,8 @@ ], "minLevel": 0, "enabled": true - }, { + }, + { "name": "increase_saturation", "colorMatrix": [ 2, @@ -638,7 +751,8 @@ ], "minLevel": 0, "enabled": true - }, { + }, + { "name": "increase_contrast", "colorMatrix": [ 1.5, @@ -664,13 +778,15 @@ ], "minLevel": 0, "enabled": true - }, { + }, + { "name": "shadow_multiply_02", "colorMatrix": [], "minLevel": 0, "blendMode": 2, "enabled": true - }, { + }, + { "name": "color_1", "colorMatrix": [ 0.393, @@ -696,7 +812,8 @@ ], "minLevel": 1, "enabled": true - }, { + }, + { "name": "hue_bright_sat", "colorMatrix": [ 1, @@ -722,25 +839,29 @@ ], "minLevel": 1, "enabled": true - }, { + }, + { "name": "hearts_hardlight_02", "colorMatrix": [], "minLevel": 1, "blendMode": 9, "enabled": true - }, { + }, + { "name": "texture_overlay", "colorMatrix": [], "minLevel": 1, "blendMode": 4, "enabled": true - }, { + }, + { "name": "pinky_nrm", "colorMatrix": [], "minLevel": 1, "blendMode": 0, "enabled": true - }, { + }, + { "name": "color_2", "colorMatrix": [ 0.333, @@ -766,7 +887,8 @@ ], "minLevel": 2, "enabled": true - }, { + }, + { "name": "night_vision", "colorMatrix": [ 0, @@ -792,37 +914,43 @@ ], "minLevel": 2, "enabled": true - }, { + }, + { "name": "stars_hardlight_02", "colorMatrix": [], "minLevel": 2, "blendMode": 9, "enabled": true - }, { + }, + { "name": "coffee_mpl", "colorMatrix": [], "minLevel": 2, "blendMode": 2, "enabled": true - }, { + }, + { "name": "security_hardlight", "colorMatrix": [], "minLevel": 3, "blendMode": 9, "enabled": true - }, { + }, + { "name": "bluemood_mpl", "colorMatrix": [], "minLevel": 3, "blendMode": 2, "enabled": true - }, { + }, + { "name": "rusty_mpl", "colorMatrix": [], "minLevel": 3, "blendMode": 2, "enabled": true - }, { + }, + { "name": "decr_conrast", "colorMatrix": [ 0.5, @@ -848,7 +976,8 @@ ], "minLevel": 4, "enabled": true - }, { + }, + { "name": "green_2", "colorMatrix": [ 0.5, @@ -874,13 +1003,15 @@ ], "minLevel": 4, "enabled": true - }, { + }, + { "name": "alien_hrd", "colorMatrix": [], "minLevel": 4, "blendMode": 9, "enabled": true - }, { + }, + { "name": "color_3", "colorMatrix": [ 0.609, @@ -906,7 +1037,8 @@ ], "minLevel": 5, "enabled": true - }, { + }, + { "name": "color_4", "colorMatrix": [ 0.8, @@ -932,13 +1064,15 @@ ], "minLevel": 5, "enabled": true - }, { + }, + { "name": "toxic_hrd", "colorMatrix": [], "minLevel": 5, "blendMode": 9, "enabled": true - }, { + }, + { "name": "hypersaturated", "colorMatrix": [ 2, @@ -964,7 +1098,8 @@ ], "minLevel": 6, "enabled": true - }, { + }, + { "name": "Yellow", "colorMatrix": [ 1, @@ -990,13 +1125,15 @@ ], "minLevel": 6, "enabled": true - }, { + }, + { "name": "misty_hrd", "colorMatrix": [], "minLevel": 6, "blendMode": 9, "enabled": true - }, { + }, + { "name": "x_ray", "colorMatrix": [ 0, @@ -1022,7 +1159,8 @@ ], "minLevel": 7, "enabled": true - }, { + }, + { "name": "decrease_saturation", "colorMatrix": [ 0.7, @@ -1048,55 +1186,64 @@ ], "minLevel": 7, "enabled": true - }, { + }, + { "name": "drops_mpl", "colorMatrix": [], "minLevel": 8, "blendMode": 2, "enabled": true - }, { + }, + { "name": "shiny_hrd", "colorMatrix": [], "minLevel": 9, "blendMode": 9, "enabled": true - }, { + }, + { "name": "glitter_hrd", "colorMatrix": [], "minLevel": 10, "blendMode": 9, "enabled": true - }, { + }, + { "name": "frame_gold", "colorMatrix": [], "minLevel": 10, "blendMode": 0, "enabled": true - }, { + }, + { "name": "frame_gray_4", "colorMatrix": [], "minLevel": 10, "blendMode": 0, "enabled": true - }, { + }, + { "name": "frame_black_2", "colorMatrix": [], "minLevel": 10, "blendMode": 0, "enabled": true - }, { + }, + { "name": "frame_wood_2", "colorMatrix": [], "minLevel": 10, "blendMode": 0, "enabled": true - }, { + }, + { "name": "finger_nrm", "colorMatrix": [], "minLevel": 10, "blendMode": 0, "enabled": true - }, { + }, + { "name": "color_5", "colorMatrix": [ 3.309, @@ -1122,7 +1269,8 @@ ], "minLevel": 10, "enabled": true - }, { + }, + { "name": "black_white_negative", "colorMatrix": [ -0.5, @@ -1148,7 +1296,8 @@ ], "minLevel": 10, "enabled": true - }, { + }, + { "name": "blue", "colorMatrix": [ 0.5, @@ -1174,7 +1323,8 @@ ], "minLevel": 10, "enabled": true - }, { + }, + { "name": "red", "colorMatrix": [ 0.5, @@ -1200,7 +1350,8 @@ ], "minLevel": 10, "enabled": true - }, { + }, + { "name": "green", "colorMatrix": [ 0.5, @@ -1336,5 +1487,8 @@ "display": "BUBBLE", "image": "${image.library.url}/album1584/X1517.gif" } - } + }, + "backgrounds.data": [], + "stands.data": [], + "overlays.data": [] } diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 395f3b2..7513077 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -16,6 +16,30 @@ const setBootDebug = (message: string) => setBootDebug('boot: secure fetch installed'); +const loadClientMode = async () => +{ + try + { + if((window as any).__nitroClientMode) return; + + const url = new URL('configuration/client-mode.json', `${ window.location.origin }/`); + url.searchParams.set('v', Date.now().toString(36)); + + const response = await fetch(url.toString()); + + if(!response.ok) throw new Error(`HTTP ${ response.status }`); + + (window as any).__nitroClientMode = await response.json(); + setBootDebug('boot: client-mode loaded'); + } + catch(error) + { + setBootDebug(`boot: client-mode fallback ${ error?.message || error }`); + } +}; + +await loadClientMode(); + const search = new URLSearchParams(window.location.search); const clientMode = getClientMode(); const cacheBustUrl = (path: string): string => diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index dd08aed..2da9120 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -13,6 +13,7 @@ import flagNl from '../../assets/images/flag_icon/flag_icon_nl.png'; import flagSelected from '../../assets/images/flag_icon/flag_icon_selected.png'; import flagTr from '../../assets/images/flag_icon/flag_icon_tr.png'; import { applyTextTranslationLocale } from '../../hooks/translation/useTranslation'; +import { NewsWindow } from './components/NewsWindow'; import { TurnstileWidget } from './TurnstileWidget'; type DialogMode = 'login' | 'register' | 'forgot'; @@ -229,6 +230,7 @@ export const LoginView: FC = ({ onAuthenticated, isEntering = fa const loginUrl = GetConfigurationValue('login.endpoint', '/api/auth/login'); const registerUrl = GetConfigurationValue('login.register.endpoint', '/api/auth/register'); const forgotUrl = GetConfigurationValue('login.forgot.endpoint', '/api/auth/forgot-password'); + const newsUrl = interpolate(GetConfigurationValue('login.news.url', '')); const turnstileSiteKey = GetConfigurationValue('login.turnstile.sitekey', ''); const rawTurnstileEnabled = GetConfigurationValue('login.turnstile.enabled', false); const turnstileEnabled = (rawTurnstileEnabled === true @@ -678,6 +680,8 @@ export const LoginView: FC = ({ onAuthenticated, isEntering = fa }) } } + { newsUrl && } +
Choose your language