Skip to content

Latest commit

ย 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

L2 Final Epilogue โ€” Web Panel

A drop-in PHP web panel for L2J Final Epilogue game servers. Player dashboard, voting, donations, multi-language support, and a two-tier admin system โ€” all in a single self-contained folder.

PHP 7.4+ MySQL License Languages

Dark fantasy aesthetic ยท CSRF-protected ยท Mobile-first ยท Zero JS dependencies for core flows


Table of Contents


Features

Public landing page

  • Live server-status banner (TCP probe to game-server port, falls back to DB-reachable)
  • Hall of Fame (top 5 by level), Top Voters of the week
  • Configurable hero copy, server rates, and feature toggles โ€” all editable from the admin UI
  • Inline login form, register CTA
  • Auto-detected language with manual override (7 locales)

Player dashboard

  • Account summary: total Adena across all characters, character count, highest level
  • Per-character roster with level, class, PvP/PK, Adena, online status
  • Auto-detects active character if multiple on the account

Voting

  • Hopzone integration with three verification modes: trust (no check), json_api, text_api
  • Per-account cooldown (default 12h, configurable)
  • Item rewards delivered directly to the chosen character's inventory
  • Audit log + per-week top voters leaderboard

Donations

  • PayPal (Smart Buttons SDK with server-side capture verification)
  • Stripe (hosted Checkout + HMAC-verified webhooks)
  • Editable package catalog (CRUD in admin UI โ€” price, item, quantity, sort order)
  • Three layers of fraud protection (see Anti-fraud guarantees)
  • Audit log of every donation attempt (paid, pending, failed, mismatch)

Two-tier admin

  • Admin (accessLevel โ‰ฅ 7): ban/unban, character moderation, edit non-sensitive settings
  • Super-Admin (accessLevel โ‰ฅ 100): promote/demote other admins, edit payment & reCAPTCHA keys, delete accounts
  • Visual lock indicators (๐Ÿ”’) on restricted UI; server-side gating regardless of UI state

Internationalization

  • 7 languages: English (default), Spanish, Russian, Portuguese, French, German, Slovak
  • Browser-language auto-detection via Accept-Language header
  • Manual override stored in 1-year cookie
  • Easy to add more languages โ€” just drop a file in lang/

Security

  • CSRF tokens on every state-changing form (hash_equals comparison)
  • POST โ†’ redirect โ†’ GET to prevent resubmission
  • Session regeneration on login
  • Constant-time password comparison
  • Webhook signature verification (Stripe HMAC; PayPal server-side capture)

Screenshots

Live previews ship with the zip. Open index-{lang}-preview.html in any browser to see each language render without booting PHP.

Public landing Player dashboard Admin overview
Hero with login + status banner Adena ledger + character roster Ban / promote / character moderation

Requirements

PHP 7.4 or newer (8.x recommended)
PHP extensions pdo_mysql, curl, json, mbstring, openssl
Database MySQL 5.7+ / MariaDB 10.3+ โ€” same DB the L2J server uses
Web server Apache 2.4+ or nginx 1.18+ with PHP-FPM
HTTPS Required for production (Stripe/PayPal webhooks won't fire on HTTP)

Quick Start

# 1. Drop the panel folder onto your web root
unzip l2-panel.zip -d /var/www/

# 2. Edit DB credentials
cd /var/www/l2-panel
nano inc/config.php      # set DB_HOST, DB_NAME, DB_USER, DB_PASS

# 3. Bootstrap your first super-admin in MySQL
mysql -u root -p l2jdb -e "
  UPDATE accounts SET accessLevel = 100 WHERE login = 'your_login';
"

# 4. (Optional) Set ownership for the web user
chown -R www-data:www-data /var/www/l2-panel

# 5. Browse to https://your-domain/
#    Sign in with your L2J account โ†’ click 'Admin' in the nav

That's it. The custom panel tables (web_settings, web_votes, web_vote_config, web_donate_packages, web_donations) are created automatically on first read โ€” no schema migration needed.


Configuration

Database connection

Edit inc/config.php โ€” the only file you must touch:

define('DB_HOST', '127.0.0.1');
define('DB_PORT', 3306);
define('DB_NAME', 'l2jdb');         // Login server DB (`accounts` table)
define('DB_USER', 'root');
define('DB_PASS', '');

// If your login + game DBs are split, set the game-server schema here.
// Most L2J Final Epilogue setups use one DB โ€” leave it the same.
define('GAME_DB_NAME', 'l2jdb');

Everything else is editable from the admin panel โ€” no PHP edits required for currency, server status probe, donation packages, voting config, forum URL, reCAPTCHA, or copy.

Site settings (in the panel)

Sign in as an admin โ†’ click Admin โ†’ Settings. The page is organized into fieldsets:

Section What lives here
Branding & copy Site name, hero tagline, footer note, forum URL
Game Server Silver item ID, silver label, game-server host + port for live status probe
Server rates Decorative rate display on the landing page
Sections to show Hall of Fame, Top Voters, Chronicle features, Server rates
reCAPTCHA ๐Ÿ”’ Enable + site/secret keys (super-admin only)

Admin tiers

Two access levels, mapped to L2J's accessLevel column:

Tier accessLevel Bootstrap Can do
Player 0 (default) Login, vote, donate
Admin (GM) โ‰ฅ 7 UPDATE accounts SET accessLevel = 7 WHERE login = 'gm'; Ban/unban, reset passwords, character moderation, non-sensitive settings
Super-Admin โ‰ฅ 100 UPDATE accounts SET accessLevel = 100 WHERE login = 'owner'; All admin actions plus promote/demote others, edit reCAPTCHA & payment keys, delete accounts

The constants ADMIN_ACCESS_LEVEL and SUPER_ADMIN_ACCESS_LEVEL in inc/config.php can be tuned if your L2J fork uses different conventions.


Voting (Hopzone)

Configure under Admin โ†’ Voting.

Field Description
Status Master enable/disable
Hopzone URL Full URL to your server's Hopzone listing
Cooldown Hours between votes per account (default 12)
Verify mode trust / json_api / text_api (see below)
API key If using json_api or text_api
Reward item ID L2J item id (default 57 = Adena)
Reward count Quantity granted per vote

Verify modes

  • trust โ€” claim the reward immediately on return. Easiest setup, but can be gamed by anyone who reloads the claim page.
  • json_api โ€” checks Hopzone's JSON API. Recommended.
  • text_api โ€” for legacy Hopzone deployments returning plain text.

The verify URL accepts {KEY} and {IP} placeholders that get substituted at request time.


Donations

Configure under Admin โ†’ Donations. The page has four blocks:

  1. Stat cards โ€” today / 30-day / all-time totals
  2. Package catalog โ€” edit price, item id, count, sort, enabled flag
  3. Master toggle + currency + thank-you message
  4. PayPal + Stripe provider blocks (both ๐Ÿ”’ super-admin only)

Stripe setup

  1. Stripe Dashboard โ†’ Developers โ†’ Webhooks โ†’ Add endpoint:
    https://your-domain/path-to-panel/stripe-webhook.php
    
  2. Subscribe to event: checkout.session.completed
  3. Copy the signing secret (whsec_โ€ฆ)
  4. In the panel: paste your pk_live_โ€ฆ, sk_live_โ€ฆ, and whsec_โ€ฆ keys
  5. The "โœ“ Active" indicator turns green when all three are filled

PayPal setup

  1. PayPal Developer Dashboard โ†’ My Apps & Credentials
  2. Create a REST API app, note the Client ID + Secret
  3. In the panel: paste them in, choose Sandbox (testing) or Live
  4. The "โœ“ Active" indicator turns green when both fields are filled

PayPal uses the JavaScript SDK with server-side capture verification โ€” no webhook URL required (the browser-side flow is finalized by a server call from donate-paypal.php).

Anti-fraud guarantees

Three independent layers โ€” each works on its own:

  1. Server-generated reference IDs. Clients never propose them โ€” the panel mints a 24-char random hex per donation attempt.
  2. Provider transaction-ID dedup. The web_donations table has a UNIQUE(provider, tx_id) constraint. Replays fail at the database level.
  3. Amount + currency re-check. Before granting items, the panel re-validates the captured amount and currency against the original package. Mismatches go to status mismatch and refuse to grant.

The receiving character should ideally be offline when the webhook fires โ€” L2J's IdFactory race is well known and unrelated to this panel. The thank-you page nudges users in that direction.


reCAPTCHA

Optional Google reCAPTCHA v2 ("I'm not a robot") on login + register.

  1. Get keys at Google reCAPTCHA admin (use v2 Checkbox)
  2. Sign in to the panel as a super-admin
  3. Admin โ†’ Settings โ†’ reCAPTCHA: paste site key + secret key, tick "Enable"

Regular admins see the fieldset locked with a ๐Ÿ”’ badge โ€” they can read but not modify. Server-side gating drops any tampered POSTs.


Internationalization

Supported languages out of the box:

๐Ÿ‡ฌ๐Ÿ‡ง English ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน ๐Ÿ‡ต๐Ÿ‡น Portuguรชs ๐Ÿ‡ซ๐Ÿ‡ท Franรงais ๐Ÿ‡ฉ๐Ÿ‡ช Deutsch ๐Ÿ‡ธ๐Ÿ‡ฐ Slovenฤina

How it works

?lang=de   โ†’   cookie l2_lang   โ†’   Accept-Language header   โ†’   en (default)

The first signal that resolves to a supported locale wins. Explicit choices via the picker set a 1-year cookie at Path=/, SameSite=Lax.

Adding a new language

  1. Copy lang/en.php to lang/<code>.php (e.g. lang/it.php)
  2. Translate the values โ€” leave keys untouched
  3. Add an entry to supported_languages() in auth.php:
    'it' => ['name' => 'Italiano', 'flag' => '๐Ÿ‡ฎ๐Ÿ‡น'],

Missing keys in any locale automatically fall back to English โ€” partial translations are safe.

Scope

Translation covers player-facing pages only: index, login, register, dashboard, vote, donate, donate-paypal, donate-return. Admin pages remain English by design โ€” they're a tool for one or two operators, not for end-users.


File Layout

l2-panel/
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ style.css                    โ† single-file stylesheet (~2700 lines)
โ”‚
โ”‚   โ”€โ”€ Public + player pages (entry points / bookmarkable URLs) โ”€โ”€
โ”œโ”€โ”€ index.php                    โ† public landing page
โ”œโ”€โ”€ login.php                    โ† sign-in
โ”œโ”€โ”€ register.php                 โ† sign-up
โ”œโ”€โ”€ logout.php
โ”œโ”€โ”€ dashboard.php                โ† player dashboard (chars + Adena)
โ”œโ”€โ”€ vote.php                     โ† Hopzone voting
โ”œโ”€โ”€ donate.php                   โ† donation package picker
โ”œโ”€โ”€ donate-paypal.php            โ† PayPal SDK interim page
โ”œโ”€โ”€ donate-return.php            โ† post-payment landing (Stripe success_url)
โ”œโ”€โ”€ lang.php                     โ† language switcher endpoint
โ”‚
โ”‚   โ”€โ”€ Webhooks (URLs registered with Stripe / PayPal โ€” DO NOT MOVE) โ”€โ”€
โ”œโ”€โ”€ stripe-webhook.php           โ† Stripe HMAC-verified webhook
โ”œโ”€โ”€ paypal-confirm.php           โ† PayPal server-side order capture
โ”‚
โ”œโ”€โ”€ admin/                       โ† admin pages โ€” gated by require_admin()
โ”‚   โ”œโ”€โ”€ index.php                โ† server overview
โ”‚   โ”œโ”€โ”€ accounts.php             โ† account moderation (ban/promote/etc)
โ”‚   โ”œโ”€โ”€ characters.php           โ† character moderation
โ”‚   โ”œโ”€โ”€ voting.php               โ† Hopzone config + audit log
โ”‚   โ”œโ”€โ”€ donations.php            โ† Stripe/PayPal config + audit log
โ”‚   โ””โ”€โ”€ settings.php             โ† site copy, rates, reCAPTCHA, game-server probe
โ”‚
โ”œโ”€โ”€ inc/                         โ† shared PHP partials (web access denied)
โ”‚   โ”œโ”€โ”€ .htaccess                โ† deny all (Apache 2.2 + 2.4)
โ”‚   โ”œโ”€โ”€ config.php               โ† DB credentials, tier constants, session setup
โ”‚   โ”œโ”€โ”€ auth.php                 โ† all helpers โ€” auth, CSRF, voting,
โ”‚   โ”‚                              donations, settings, i18n, base_url()
โ”‚   โ”œโ”€โ”€ nav.php                  โ† shared player+admin navigation
โ”‚   โ”œโ”€โ”€ admin-header.php         โ† admin page chrome (top)
โ”‚   โ””โ”€โ”€ admin-footer.php         โ† admin page chrome (bottom)
โ”‚
โ””โ”€โ”€ lang/                        โ† translation tables (web access denied)
    โ”œโ”€โ”€ .htaccess                โ† deny all
    โ”œโ”€โ”€ en.php                   โ† canonical English (181 keys)
    โ”œโ”€โ”€ es.php                   โ† Spanish
    โ”œโ”€โ”€ ru.php                   โ† Russian
    โ”œโ”€โ”€ pt.php                   โ† Portuguese
    โ”œโ”€โ”€ fr.php                   โ† French
    โ”œโ”€โ”€ de.php                   โ† German
    โ””โ”€โ”€ sk.php                   โ† Slovak

Why this split? Player pages stay at root so existing bookmarks (/dashboard.php, /vote.php, /donate.php) keep working. Webhook endpoints stay at root so Stripe/PayPal configurations don't need to be re-registered. Admin pages move to admin/ so URLs become cleaner (/admin/accounts.php instead of /admin-accounts.php). Shared partials move to inc/ with an .htaccess deny rule so they can't be hit directly via URL โ€” they're only loaded via require_once from other pages. Translation tables in lang/ likewise.

All cross-page links use the base_url() helper so the panel works the same whether deployed at /, /panel/, or any other subdirectory.


Database Schema

The panel reads two existing L2J tables and creates five of its own. Custom tables are created automatically โ€” no migration script needed.

Existing L2J tables (read-only mostly)

Table Used for
accounts login, password, accessLevel, lastactive, lastIP
characters char list, level, classid, online, pvpkills, pkkills
items summing inventory/paperdoll/warehouse for "total Adena"

Tables the panel creates

Table Purpose
web_settings k/v store for site settings (currency, copy, reCAPTCHA, payment keys)
web_vote_config per-topsite voting config (Hopzone)
web_votes audit log of every vote claim
web_donate_packages editable donation catalog
web_donations every donation attempt with status (pending, paid, failed, mismatch)

The web_donations table has UNIQUE(provider, tx_id) to prevent webhook replay attacks at the database level.


Security

Passwords L2J's native Base64(SHA-1(plaintext)) โ€” same hash the game uses, so panel and client share a single credential
Constant-time login verify_account() always runs hash_equals against either the real or a dummy hash, so non-existent accounts take the same time as wrong-password attempts (closes the user-enumeration timing oracle)
CSRF Token in session + hidden form field; hash_equals comparison; X-CSRF-Token header accepted for JSON endpoints
Sessions Cookie hardened with HttpOnly, SameSite=Lax, and Secure on HTTPS (auto-detected including X-Forwarded-Proto); regenerated on login AND register (anti-fixation); name configurable via SESSION_NAME
Language cookie SameSite=Lax + Secure on HTTPS; the lang.php switcher strips CRLF/null from ?return= and rejects non-relative paths to prevent open-redirect and response-splitting
POST-redirect-GET Every admin action redirects after the DB write to prevent resubmission
Webhook verification Stripe HMAC signature with constant-time compare; PayPal server-side order-capture verification
Idempotency UNIQUE(provider, tx_id) on donations + status='paid' short-circuit in fulfill_donation()
Self-protection Admins can't ban/demote themselves; regular admins can't act on super-admins
Server-side gating Tampered POSTs that try to escalate privilege or write super-only fields are dropped silently
Output escaping All user data goes through htmlspecialchars(); placeholder substitution in t() escapes too
No raw SQL with user input All queries use PDO prepared statements
No DB-error leakage to users Public pages (login, register, dashboard, vote, donate) catch PDOException and show a generic "Database error" message; the actual exception is error_log()'d for the admin to inspect
CSS browser parity All backdrop-filter rules paired with -webkit-backdrop-filter so glassmorphism effects render correctly on Safari iOS/macOS

Customization

Branding

Most copy is editable in the admin Settings page โ€” no PHP edits needed.

Visual theme

Single stylesheet at style.css (~2,650 lines). Design tokens at the top:

:root {
    --gold:     #d4a857;
    --gold-hi:  #e6c074;
    --gold-lo:  #8a6f3a;
    --emerald:  #6fae7a;
    --crimson:  #b94a4a;
    --bg-0:     #07070b;
    --bg-1:     #0e0d14;
    /* โ€ฆmore */
}

Tweak these, the rest of the system follows.

Currency

L2 Final Epilogue uses Adena (item id 57) by default. To use a custom coin (Donate Coin, Silver Coin, etc.), edit Admin โ†’ Settings โ†’ Game Server โ†’ Silver item ID + Silver label. No file edits required.

Adding admin pages

Each admin page follows the same template:

<?php
require_once __DIR__ . '/auth.php';
require_admin();           // or require_super_admin() for sensitive ones

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    check_csrf();
    // โ€ฆhandle actionโ€ฆ
    header('Location: ' . basename(__FILE__));
    exit;
}

require __DIR__ . '/admin-header.php';
?>

<h1 class="page-title">Your Page</h1>
<!-- markup -->

<?php require __DIR__ . '/admin-footer.php'; ?>

Then add a sub-nav entry in nav.php under $adminItems.


Troubleshooting

"Database error: SQLSTATE[HY000] [2002] Connection refused"

Your DB_HOST / DB_PORT is wrong, MySQL isn't running, or it's not listening on TCP. Test with:

mysql -h 127.0.0.1 -P 3306 -u $DB_USER -p $DB_NAME -e 'SELECT 1'
"Forbidden โ€” this page is for game masters only" after I set accessLevel=7

The session is cached. Sign out, sign back in, then try again. The account's accessLevel is checked fresh on each request, but session identity isn't refreshed unless you log out.

The Online/Offline pill always shows OFFLINE

The TCP probe needs a configured host. Go to Admin โ†’ Settings โ†’ Game Server, fill in gameserver_host (e.g. 127.0.0.1) and gameserver_port (default 7777), save. The "โœ“ Game server is responding" indicator below confirms the probe works.

Empty host = probe disabled = falls back to "DB-reachable" check, which will only show OFFLINE if MySQL itself is down.

Stripe payments succeed but the reward never lands

Your webhook isn't reaching the panel. Check:

  1. HTTPS โ€” Stripe refuses to deliver to http://
  2. Endpoint URL โ€” must match exactly what's in Admin โ†’ Donations
  3. Signing secret โ€” copy from Stripe dashboard, no leading/trailing whitespace
  4. Server logs โ€” look for stripe-webhook.php requests; 401 = bad signature, 200 = good
  5. Subscribed event โ€” must include checkout.session.completed

You can re-trigger a webhook from Stripe's dashboard for any past test payment without making a new charge.

PayPal payments succeed but the reward never lands

PayPal uses server-side capture, not webhooks. If the JS SDK's onApprove callback fires but the panel doesn't grant the reward, check the browser console โ€” paypal-confirm.php returns JSON with the result. Most common cause: client ID/secret mismatch between what the SDK loads (public key) and what the server uses (secret key).

"That action requires super-admin privileges" but I'm an admin

Promotion/demotion, account deletion, payment keys, and reCAPTCHA keys are restricted to super-admins (accessLevel โ‰ฅ 100). Run:

UPDATE accounts SET accessLevel = 100 WHERE login = 'your_login';

Then sign out and back in.

The language switcher doesn't persist between visits

The cookie is l2_lang at Path=/, SameSite=Lax. If your panel lives at a sub-path and the cookie's Path doesn't match, browsers won't send it back. The default Path=/ covers all cases โ€” only change it if you know what you're doing.

"Cannot delete account โ€” still has characters"

Intentional. Delete the characters in-game (or via the admin UI's character moderation page) before deleting the account. This protects against orphan rows in characters that would otherwise crash the L2J server on next boot.

Character is online during donation/vote โ€” they don't see the items

L2J's IdFactory caches inventory in-memory while a character is online. Items inserted via SQL won't appear until the character relogs. The thank-you page already mentions this โ€” you can pre-empt the support ticket by reminding players up-front in your forum.


License

MIT โ€” do whatever you want, attribution appreciated but not required.

Lineage II is a registered trademark of NCSOFT Corp. This panel is an unofficial fan project and is not affiliated with NCSOFT.


Contributing

PRs welcome. Before submitting:

  • Run the audit script in auth.php (CSRF coverage, brace balance, key parity)
  • Test responsive layout at 375 / 768 / 1280 px
  • If you add UI strings, update all 7 locale files โ€” en.php is the source of truth, missing keys fall back to English but UI consistency suffers

For new features, open an issue first to discuss scope.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages