304 lines
10 KiB
PHP
304 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\GameAccountUser;
|
|
use App\Support\WowData;
|
|
use Illuminate\Database\Query\Builder;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AzerothCoreAccountService
|
|
{
|
|
public const ACCOUNT_TAB_PLAYERS = 'players';
|
|
|
|
public const ACCOUNT_TAB_BOTS = 'bots';
|
|
|
|
public function __construct(private readonly AzerothCoreSrpService $srp)
|
|
{
|
|
}
|
|
|
|
public function findUserById(int $accountId): ?GameAccountUser
|
|
{
|
|
$record = $this->authConnection()
|
|
->table('account')
|
|
->selectRaw('id, username, email, reg_mail, locked, last_login, joindate, HEX(salt) as salt_hex, HEX(verifier) as verifier_hex')
|
|
->where('id', $accountId)
|
|
->first();
|
|
|
|
if (! $record) {
|
|
return null;
|
|
}
|
|
|
|
$gmLevel = $this->accessLevelsFor([$accountId])[$accountId] ?? 0;
|
|
|
|
return $this->mapUser($record, $gmLevel);
|
|
}
|
|
|
|
public function findUserByUsername(string $username): ?GameAccountUser
|
|
{
|
|
$record = $this->authConnection()
|
|
->table('account')
|
|
->selectRaw('id, username, email, reg_mail, locked, last_login, joindate, HEX(salt) as salt_hex, HEX(verifier) as verifier_hex')
|
|
->where('username', $this->srp->normalizeUsername($username))
|
|
->first();
|
|
|
|
if (! $record) {
|
|
return null;
|
|
}
|
|
|
|
$gmLevel = $this->accessLevelsFor([(int) $record->id])[(int) $record->id] ?? 0;
|
|
|
|
return $this->mapUser($record, $gmLevel);
|
|
}
|
|
|
|
public function validatePassword(GameAccountUser $user, string $password): bool
|
|
{
|
|
return $this->srp->credentialsMatch($user->username, $password, $user->saltHex, $user->verifierHex);
|
|
}
|
|
|
|
public function updatePassword(GameAccountUser $user, string $newPassword): void
|
|
{
|
|
[$saltHex, $verifierHex] = $this->srp->makeRegistrationData($user->username, $newPassword);
|
|
|
|
$this->authConnection()->update(
|
|
'UPDATE account SET salt = UNHEX(?), verifier = UNHEX(?), session_key = NULL, failed_logins = 0 WHERE id = ?',
|
|
[$saltHex, $verifierHex, $user->id],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
|
|
*/
|
|
public function listAccounts(?string $search = null, string $accountTab = self::ACCOUNT_TAB_PLAYERS): Collection
|
|
{
|
|
$query = $this->baseAccountsQuery($search);
|
|
|
|
$this->applyAccountTabFilter($query, $accountTab);
|
|
|
|
$accounts = $query->get();
|
|
$accountIds = $accounts->pluck('id')->map(fn ($id): int => (int) $id)->all();
|
|
$accessLevels = $this->accessLevelsFor($accountIds);
|
|
$characters = $this->charactersForAccountIds($accountIds);
|
|
|
|
return $accounts->map(function (object $account) use ($accessLevels, $characters): array {
|
|
$accountId = (int) $account->id;
|
|
$gmLevel = $accessLevels[$accountId] ?? 0;
|
|
|
|
return [
|
|
'id' => $accountId,
|
|
'username' => $account->username,
|
|
'email' => $account->email,
|
|
'reg_mail' => $account->reg_mail,
|
|
'gm_level' => $gmLevel,
|
|
'access_label' => WowData::securityLevelName($gmLevel),
|
|
'locked' => (bool) $account->locked,
|
|
'online' => (bool) $account->online,
|
|
'joined_at' => $this->formatDate($account->joindate),
|
|
'last_login_at' => $this->formatDate($account->last_login),
|
|
'characters' => $characters->get($accountId, collect()),
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array{players: int, bots: int}
|
|
*/
|
|
public function accountTypeStats(?string $search = null): array
|
|
{
|
|
$playersQuery = $this->baseAccountsQuery($search);
|
|
$botsQuery = $this->baseAccountsQuery($search);
|
|
|
|
$this->applyAccountTabFilter($playersQuery, self::ACCOUNT_TAB_PLAYERS);
|
|
$this->applyAccountTabFilter($botsQuery, self::ACCOUNT_TAB_BOTS);
|
|
|
|
return [
|
|
self::ACCOUNT_TAB_PLAYERS => $playersQuery->count(),
|
|
self::ACCOUNT_TAB_BOTS => $botsQuery->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
|
|
*/
|
|
public function charactersForAccount(int $accountId): Collection
|
|
{
|
|
return $this->charactersForAccountIds([$accountId])->get($accountId, collect());
|
|
}
|
|
|
|
public function updateAccessLevel(int $accountId, int $gmLevel): void
|
|
{
|
|
$realmId = $this->targetRealmId();
|
|
|
|
$this->authConnection()->statement(
|
|
'INSERT INTO account_access (id, gmlevel, RealmID, comment) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE gmlevel = VALUES(gmlevel), comment = VALUES(comment)',
|
|
[
|
|
$accountId,
|
|
$gmLevel,
|
|
$realmId,
|
|
config('moonwell.portal.access_comment'),
|
|
],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $accountIds
|
|
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, array<string, mixed>>>
|
|
*/
|
|
private function charactersForAccountIds(array $accountIds): Collection
|
|
{
|
|
if ($accountIds === []) {
|
|
return collect();
|
|
}
|
|
|
|
return $this->charactersConnection()
|
|
->table('characters')
|
|
->select('guid', 'account', 'name', 'race', 'class', 'gender', 'level', 'online', 'money', 'totaltime')
|
|
->whereIn('account', $accountIds)
|
|
->orderBy('account')
|
|
->orderByDesc('level')
|
|
->orderBy('name')
|
|
->get()
|
|
->map(function (object $character): array {
|
|
return [
|
|
'guid' => (int) $character->guid,
|
|
'account' => (int) $character->account,
|
|
'name' => $character->name,
|
|
'level' => (int) $character->level,
|
|
'race' => WowData::raceName((int) $character->race),
|
|
'class' => WowData::className((int) $character->class),
|
|
'gender' => WowData::genderName((int) $character->gender),
|
|
'online' => (bool) $character->online,
|
|
'money' => WowData::formatMoney((int) $character->money),
|
|
'played' => WowData::formatPlayedTime((int) $character->totaltime),
|
|
];
|
|
})
|
|
->groupBy('account')
|
|
->map(fn (Collection $group): Collection => $group->values());
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $accountIds
|
|
* @return array<int, int>
|
|
*/
|
|
private function accessLevelsFor(array $accountIds): array
|
|
{
|
|
if ($accountIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$targetRealmId = $this->targetRealmId();
|
|
$realmIds = array_values(array_unique([$targetRealmId, -1]));
|
|
|
|
return $this->authConnection()
|
|
->table('account_access')
|
|
->select('id', 'gmlevel', 'RealmID')
|
|
->whereIn('id', $accountIds)
|
|
->whereIn('RealmID', $realmIds)
|
|
->get()
|
|
->groupBy('id')
|
|
->map(function (Collection $rows) use ($targetRealmId): int {
|
|
$exactMatch = $rows->firstWhere('RealmID', $targetRealmId);
|
|
|
|
return (int) ($exactMatch->gmlevel ?? $rows->firstWhere('RealmID', -1)?->gmlevel ?? 0);
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function mapUser(object $record, int $gmLevel): GameAccountUser
|
|
{
|
|
return new GameAccountUser(
|
|
id: (int) $record->id,
|
|
username: $record->username,
|
|
email: $record->email,
|
|
regMail: $record->reg_mail,
|
|
gmLevel: $gmLevel,
|
|
locked: (bool) $record->locked,
|
|
lastLoginAt: $record->last_login,
|
|
joinedAt: $record->joindate,
|
|
saltHex: strtoupper($record->salt_hex),
|
|
verifierHex: strtoupper($record->verifier_hex),
|
|
);
|
|
}
|
|
|
|
private function formatDate(?string $value): string
|
|
{
|
|
return $value ? Carbon::parse($value)->format('d.m.Y H:i') : '—';
|
|
}
|
|
|
|
private function baseAccountsQuery(?string $search = null): Builder
|
|
{
|
|
$query = $this->authConnection()
|
|
->table('account')
|
|
->select('id', 'username', 'email', 'reg_mail', 'locked', 'last_login', 'joindate', 'online')
|
|
->orderByDesc('id');
|
|
|
|
if ($search !== null && trim($search) !== '') {
|
|
$normalizedSearch = $this->srp->normalizeUsername($search);
|
|
|
|
$query->where(function ($builder) use ($normalizedSearch): void {
|
|
$builder
|
|
->where('username', 'like', "%{$normalizedSearch}%")
|
|
->orWhere('email', 'like', "%{$normalizedSearch}%")
|
|
->orWhere('reg_mail', 'like', "%{$normalizedSearch}%");
|
|
});
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
private function applyAccountTabFilter(Builder $query, string $accountTab): void
|
|
{
|
|
$botPrefix = $this->botAccountPrefix();
|
|
|
|
if ($botPrefix === '') {
|
|
return;
|
|
}
|
|
|
|
$pattern = $botPrefix.'%';
|
|
|
|
if ($accountTab === self::ACCOUNT_TAB_BOTS) {
|
|
$query->where('username', 'like', $pattern);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->where('username', 'not like', $pattern);
|
|
}
|
|
|
|
private function botAccountPrefix(): string
|
|
{
|
|
return strtoupper(trim((string) config('moonwell.portal.bot_account_prefix', 'RNDBOT')));
|
|
}
|
|
|
|
private function targetRealmId(): int
|
|
{
|
|
return (int) config('moonwell.registration.realm_id', -1);
|
|
}
|
|
|
|
public function onlinePlayerCount(): ?int
|
|
{
|
|
try {
|
|
return (int) $this->charactersConnection()
|
|
->table('characters')
|
|
->where('online', 1)
|
|
->count();
|
|
} catch (\Throwable $exception) {
|
|
report($exception);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function authConnection()
|
|
{
|
|
return DB::connection(config('moonwell.auth_connection'));
|
|
}
|
|
|
|
private function charactersConnection()
|
|
{
|
|
return DB::connection(config('moonwell.characters_connection'));
|
|
}
|
|
}
|