229 lines
7.7 KiB
PHP
229 lines
7.7 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* สาธารณะ — ระบบ "รหัสผู้เล่น" สำหรับ sync โปรไฟล์ข้าม browser/เครื่อง (Guest)
|
||
* ผูกกับ jdPlayerKey (guest account: providerUserId = key) เหมือน player-coins.php
|
||
*
|
||
* GET ?action=code&playerKey=...&displayName=...&agentId=...
|
||
* → คืนรหัสของบัญชีนี้ (สร้างถ้ายังไม่มี) + อัปเดตชื่อ/agent ฝั่ง server
|
||
* { ok, code, codeDisplay }
|
||
*
|
||
* POST { action:'redeem', code }
|
||
* → ดึงโปรไฟล์จากรหัส → คืน playerKey + ข้อมูลโปรไฟล์ของบัญชีนั้น
|
||
* { ok, playerKey, coins, colorThemeIndex, skinToneIndex, displayName, agentId }
|
||
*
|
||
* รหัสที่ไม่ถูกใช้เกิน 30 วัน → หมดอายุ (ลบแบบ lazy ตอนมีการเรียก)
|
||
*/
|
||
require __DIR__ . '/_common.php';
|
||
|
||
const LINK_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // ตัด 0 O 1 I L ที่กำกวม
|
||
const LINK_CODE_LEN = 8;
|
||
const LINK_CODE_TTL_DAYS = 30;
|
||
|
||
function valid_player_key(string $key): bool
|
||
{
|
||
return (bool)preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key);
|
||
}
|
||
|
||
function sanitize_display_name(string $raw): string
|
||
{
|
||
$s = trim($raw);
|
||
$s = preg_replace('/[\x00-\x1f\x7f]/u', '', $s) ?? '';
|
||
if (function_exists('mb_substr')) {
|
||
$s = mb_substr($s, 0, 24);
|
||
} else {
|
||
$s = substr($s, 0, 24);
|
||
}
|
||
return $s;
|
||
}
|
||
|
||
function sanitize_agent_id(string $raw): string
|
||
{
|
||
return preg_match('/^\d{6}$/', trim($raw)) ? trim($raw) : '';
|
||
}
|
||
|
||
function normalize_code(string $raw): string
|
||
{
|
||
$s = strtoupper(trim($raw));
|
||
$s = str_replace([' ', '-', '_'], '', $s);
|
||
// กันสับสน: ผู้ใช้พิมพ์ 0/O/1/I/L → map เข้าตัวที่ใช้จริง
|
||
$s = strtr($s, ['0' => 'O', '1' => 'I']);
|
||
return $s;
|
||
}
|
||
|
||
function generate_code(): string
|
||
{
|
||
$n = strlen(LINK_CODE_ALPHABET);
|
||
$out = '';
|
||
for ($i = 0; $i < LINK_CODE_LEN; $i++) {
|
||
$out .= LINK_CODE_ALPHABET[random_int(0, $n - 1)];
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
function format_code_display(string $code): string
|
||
{
|
||
if (strlen($code) === 8) {
|
||
return substr($code, 0, 4) . '-' . substr($code, 4, 4);
|
||
}
|
||
return $code;
|
||
}
|
||
|
||
function code_is_expired($lastUsedAt): bool
|
||
{
|
||
if (!$lastUsedAt) {
|
||
return false;
|
||
}
|
||
$ts = strtotime((string)$lastUsedAt);
|
||
if ($ts === false) {
|
||
return false;
|
||
}
|
||
return (time() - $ts) > (LINK_CODE_TTL_DAYS * 86400);
|
||
}
|
||
|
||
/** ลบรหัสที่หมดอายุออกจากทุกบัญชี (lazy GC) — แก้ใน $store ตรง ๆ */
|
||
function gc_expired_codes(array &$store): bool
|
||
{
|
||
$changed = false;
|
||
foreach ($store['accounts'] as $i => $a) {
|
||
if (!empty($a['linkCode']) && code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
|
||
unset($store['accounts'][$i]['linkCode']);
|
||
$store['accounts'][$i]['linkCodeLastUsedAt'] = null;
|
||
$changed = true;
|
||
}
|
||
}
|
||
return $changed;
|
||
}
|
||
|
||
function code_in_use(array $store, string $code, ?string $exceptKey = null): bool
|
||
{
|
||
foreach ($store['accounts'] as $a) {
|
||
if (($a['linkCode'] ?? '') !== $code) {
|
||
continue;
|
||
}
|
||
if ($exceptKey !== null && ($a['providerUserId'] ?? '') === $exceptKey) {
|
||
continue;
|
||
}
|
||
if (!code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function profile_payload(array $a): array
|
||
{
|
||
return [
|
||
'playerKey' => $a['providerUserId'] ?? '',
|
||
'coins' => max(0, (int)($a['coins'] ?? 0)),
|
||
'colorThemeIndex' => ($v = (int)($a['lobbyColorThemeIndex'] ?? 0)) >= 1 && $v <= 8 ? $v : null,
|
||
'skinToneIndex' => ($s = (int)($a['lobbySkinToneIndex'] ?? 0)) >= 1 && $s <= 3 ? $s : null,
|
||
'displayName' => (string)($a['displayName'] ?? ''),
|
||
'agentId' => (string)($a['agentDisplayId'] ?? ''),
|
||
];
|
||
}
|
||
|
||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||
|
||
if ($method === 'GET') {
|
||
$action = (string)($_GET['action'] ?? 'code');
|
||
if ($action !== 'code') {
|
||
json_response(['ok' => false, 'error' => 'unknown action'], 400);
|
||
}
|
||
$key = trim((string)($_GET['playerKey'] ?? ''));
|
||
if (!valid_player_key($key)) {
|
||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||
}
|
||
$displayName = sanitize_display_name((string)($_GET['displayName'] ?? ''));
|
||
$agentId = sanitize_agent_id((string)($_GET['agentId'] ?? ''));
|
||
|
||
$store = read_store();
|
||
gc_expired_codes($store);
|
||
$now = gmdate('c');
|
||
$idx = -1;
|
||
foreach ($store['accounts'] as $i => $a) {
|
||
if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) {
|
||
$idx = $i;
|
||
break;
|
||
}
|
||
}
|
||
if ($idx < 0) {
|
||
$store['accounts'][] = [
|
||
'id' => new_id(),
|
||
'email' => '',
|
||
'displayName' => $displayName,
|
||
'loginType' => 'guest',
|
||
'providerUserId' => $key,
|
||
'notes' => 'auto: player-link',
|
||
'blocked' => false,
|
||
'coins' => 0,
|
||
'agentDisplayId' => $agentId,
|
||
'createdAt' => $now,
|
||
'updatedAt' => $now,
|
||
];
|
||
$idx = count($store['accounts']) - 1;
|
||
} else {
|
||
if ($displayName !== '') {
|
||
$store['accounts'][$idx]['displayName'] = $displayName;
|
||
}
|
||
if ($agentId !== '') {
|
||
$store['accounts'][$idx]['agentDisplayId'] = $agentId;
|
||
}
|
||
$store['accounts'][$idx]['updatedAt'] = $now;
|
||
}
|
||
|
||
$code = (string)($store['accounts'][$idx]['linkCode'] ?? '');
|
||
if ($code === '' || code_is_expired($store['accounts'][$idx]['linkCodeLastUsedAt'] ?? null)) {
|
||
do {
|
||
$code = generate_code();
|
||
} while (code_in_use($store, $code, $key));
|
||
$store['accounts'][$idx]['linkCode'] = $code;
|
||
}
|
||
$store['accounts'][$idx]['linkCodeLastUsedAt'] = $now;
|
||
|
||
if (!write_store($store)) {
|
||
json_response(['ok' => false, 'error' => 'สร้างรหัสไม่สำเร็จ'], 500);
|
||
}
|
||
json_response(['ok' => true, 'code' => $code, 'codeDisplay' => format_code_display($code)]);
|
||
}
|
||
|
||
if ($method === 'POST') {
|
||
$body = require_json_body();
|
||
$action = (string)($body['action'] ?? 'redeem');
|
||
if ($action !== 'redeem') {
|
||
json_response(['ok' => false, 'error' => 'unknown action'], 400);
|
||
}
|
||
$code = normalize_code((string)($body['code'] ?? ''));
|
||
if (!preg_match('/^[A-Z2-9]{' . LINK_CODE_LEN . '}$/', $code)) {
|
||
json_response(['ok' => false, 'error' => 'รูปแบบรหัสไม่ถูกต้อง'], 400);
|
||
}
|
||
|
||
$store = read_store();
|
||
$sweep = gc_expired_codes($store);
|
||
$now = gmdate('c');
|
||
$found = -1;
|
||
foreach ($store['accounts'] as $i => $a) {
|
||
if (($a['linkCode'] ?? '') === $code && !code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
|
||
$found = $i;
|
||
break;
|
||
}
|
||
}
|
||
if ($found < 0) {
|
||
if ($sweep) {
|
||
write_store($store);
|
||
}
|
||
json_response(['ok' => false, 'error' => 'ไม่พบรหัสนี้ หรือรหัสหมดอายุแล้ว'], 404);
|
||
}
|
||
|
||
$store['accounts'][$found]['linkCodeLastUsedAt'] = $now;
|
||
$store['accounts'][$found]['updatedAt'] = $now;
|
||
write_store($store);
|
||
|
||
$payload = profile_payload($store['accounts'][$found]);
|
||
$payload['ok'] = true;
|
||
json_response($payload);
|
||
}
|
||
|
||
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);
|