2026-04-11 20:45:51 +02:00
|
|
|
|
<?php declare(strict_types=1);
|
|
|
|
|
|
/*
|
|
|
|
|
|
* This file is part of the SplendidBear Websites' projects.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Copyright (c) 2026 @ www.splendidbear.org
|
|
|
|
|
|
*
|
|
|
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
|
|
* file that was distributed with this source code.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Controller;
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use App\Entity\PlayedGame;
|
2026-04-11 20:45:51 +02:00
|
|
|
|
use App\Entity\User;
|
2026-04-12 08:01:46 +02:00
|
|
|
|
use App\Repository\PlayedGameRepository;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use App\Service\BattleCardGenerator;
|
2026-04-12 15:19:03 +02:00
|
|
|
|
use App\Service\WebAuthnService;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use DateTime;
|
2026-04-13 15:50:28 +02:00
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use League\Flysystem\FilesystemException;
|
2026-04-13 15:50:28 +02:00
|
|
|
|
use League\Flysystem\FilesystemOperator;
|
|
|
|
|
|
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
|
|
use RuntimeException;
|
2026-04-11 20:45:51 +02:00
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
2026-04-13 15:50:28 +02:00
|
|
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
2026-04-13 15:50:28 +02:00
|
|
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
|
|
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
2026-04-11 20:45:51 +02:00
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
2026-04-12 08:01:46 +02:00
|
|
|
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
2026-04-11 20:45:51 +02:00
|
|
|
|
use Symfony\Component\Routing\Attribute\Route;
|
2026-04-14 18:54:44 +02:00
|
|
|
|
use Symfony\Component\Uid\Uuid;
|
|
|
|
|
|
use Throwable;
|
|
|
|
|
|
use function count;
|
2026-04-11 20:45:51 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Class ProfileController
|
|
|
|
|
|
*
|
|
|
|
|
|
* @package App\Controller
|
|
|
|
|
|
* @author Lang <https://www.splendidbear.org>
|
|
|
|
|
|
* @category Class
|
|
|
|
|
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
|
|
|
|
* @link www.splendidbear.org
|
|
|
|
|
|
* @since 2026. 04. 11.
|
|
|
|
|
|
*/
|
2026-04-12 08:01:46 +02:00
|
|
|
|
#[AsController]
|
2026-04-11 20:45:51 +02:00
|
|
|
|
class ProfileController extends AbstractController
|
|
|
|
|
|
{
|
2026-04-12 15:19:03 +02:00
|
|
|
|
public function __construct(
|
|
|
|
|
|
private readonly PlayedGameRepository $repo,
|
2026-04-14 18:54:44 +02:00
|
|
|
|
private readonly WebAuthnService $webAuthnService,
|
|
|
|
|
|
private readonly LoggerInterface $logger,
|
2026-04-13 15:50:28 +02:00
|
|
|
|
) {
|
2026-04-12 17:55:57 +02:00
|
|
|
|
}
|
2026-04-12 08:01:46 +02:00
|
|
|
|
|
2026-04-11 20:45:51 +02:00
|
|
|
|
#[Route('/profile', name: 'MineSeekerBundle_profile')]
|
2026-04-15 16:44:57 +02:00
|
|
|
|
public function index(CacheManager $cacheManager): Response
|
2026-04-11 20:45:51 +02:00
|
|
|
|
{
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
|
$user = $this->getUser();
|
2026-04-12 08:01:46 +02:00
|
|
|
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
2026-04-11 20:45:51 +02:00
|
|
|
|
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$total = $this->repo->countFinishedForUser($user);
|
|
|
|
|
|
$wins = $this->repo->countWinsForUser($user);
|
|
|
|
|
|
$losses = $this->repo->countLossesForUser($user);
|
|
|
|
|
|
$draws = $this->repo->countDrawsForUser($user);
|
2026-04-12 20:03:20 +02:00
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
/** Build monthly buckets for the last 6 months */
|
2026-04-12 20:03:20 +02:00
|
|
|
|
$monthlyData = [];
|
|
|
|
|
|
for ($i = 5; $i >= 0; $i--) {
|
2026-04-14 18:54:44 +02:00
|
|
|
|
$dt = new DateTime("first day of -$i months midnight");
|
2026-04-12 20:03:20 +02:00
|
|
|
|
$key = $dt->format('Y-m');
|
|
|
|
|
|
$monthlyData[$key] = ['label' => $dt->format('M'), 'wins' => 0, 'losses' => 0, 'draws' => 0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
$since = new DateTime('first day of -5 months midnight');
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$recentGames = $this->repo->findFinishedForUserSince($user, $since);
|
|
|
|
|
|
$userId = $user->getId();
|
2026-04-12 20:03:20 +02:00
|
|
|
|
|
|
|
|
|
|
foreach ($recentGames as $game) {
|
|
|
|
|
|
if (!$game->getUpdated()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$month = $game->getUpdated()->format('Y-m');
|
|
|
|
|
|
if (!isset($monthlyData[$month])) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$isRed = $game->getRed()?->getId() === $userId;
|
|
|
|
|
|
$myPts = $isRed ? $game->getRedPoints() : $game->getBluePoints();
|
|
|
|
|
|
$oppPts = $isRed ? $game->getBluePoints() : $game->getRedPoints();
|
|
|
|
|
|
$resign = $game->getResign();
|
|
|
|
|
|
$myColor = $isRed ? 'red' : 'blue';
|
|
|
|
|
|
$oppColor = $isRed ? 'blue' : 'red';
|
2026-04-12 20:03:20 +02:00
|
|
|
|
|
|
|
|
|
|
$result = 'draws';
|
|
|
|
|
|
if ($resign === $myColor) {
|
|
|
|
|
|
$result = 'losses';
|
|
|
|
|
|
} elseif ($resign === $oppColor) {
|
|
|
|
|
|
$result = 'wins';
|
|
|
|
|
|
} elseif ($myPts !== null && $oppPts !== null) {
|
2026-04-13 15:50:28 +02:00
|
|
|
|
if ($myPts > $oppPts) $result = 'wins';
|
|
|
|
|
|
elseif ($myPts < $oppPts) $result = 'losses';
|
2026-04-12 20:03:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$monthlyData[$month][$result]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$months = array_column(array_values($monthlyData), 'label');
|
|
|
|
|
|
|
2026-04-18 22:12:07 +02:00
|
|
|
|
$bonus = $this->repo->findBonusStatsForUser($user);
|
|
|
|
|
|
|
2026-04-11 20:45:51 +02:00
|
|
|
|
return $this->render('Security/profile.html.twig', [
|
2026-04-13 15:50:28 +02:00
|
|
|
|
'stats' => [
|
2026-04-18 22:12:07 +02:00
|
|
|
|
'total' => $total,
|
|
|
|
|
|
'wins' => $wins,
|
|
|
|
|
|
'losses' => $losses,
|
|
|
|
|
|
'draws' => $draws,
|
|
|
|
|
|
'minesHit' => $this->repo->findTotalMinesForUser($user),
|
|
|
|
|
|
'winRate' => $total > 0 ? (int)round($wins / $total * 100) : 0,
|
|
|
|
|
|
'avgScore' => $this->repo->findAvgScoreForUser($user),
|
|
|
|
|
|
'bonusPoints' => $bonus['totalBonusPoints'],
|
|
|
|
|
|
'avgBonus' => $bonus['avgBonusPoints'],
|
|
|
|
|
|
'bestChain' => $bonus['bestChain'],
|
|
|
|
|
|
'blindHits' => $bonus['totalBlindHits'],
|
|
|
|
|
|
'edgeMines' => $bonus['totalEdgeMines'],
|
2026-04-12 08:01:46 +02:00
|
|
|
|
],
|
2026-04-19 22:11:58 +02:00
|
|
|
|
'recent' => ($recent = $this->repo->findRecentFinishedForUser($user, 30)),
|
2026-04-15 16:44:57 +02:00
|
|
|
|
'gamesData' => array_map(function (PlayedGame $game) use ($userId, $cacheManager): array {
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$isRed = $game->getRed()?->getId() === $userId;
|
|
|
|
|
|
$resign = $game->getResign();
|
2026-04-12 20:03:20 +02:00
|
|
|
|
$myColor = $isRed ? 'red' : 'blue';
|
|
|
|
|
|
$oppColor = $isRed ? 'blue' : 'red';
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$myPts = $isRed ? $game->getRedPoints() : $game->getBluePoints();
|
|
|
|
|
|
$oppPts = $isRed ? $game->getBluePoints() : $game->getRedPoints();
|
2026-04-12 20:03:20 +02:00
|
|
|
|
$result = 'draw';
|
2026-04-13 15:50:28 +02:00
|
|
|
|
|
|
|
|
|
|
if ($resign === $myColor) $result = 'loss';
|
|
|
|
|
|
elseif ($resign === $oppColor) $result = 'win';
|
2026-04-12 20:03:20 +02:00
|
|
|
|
elseif ($myPts !== null && $oppPts !== null) {
|
2026-04-13 15:50:28 +02:00
|
|
|
|
if ($myPts > $oppPts) $result = 'win';
|
|
|
|
|
|
elseif ($myPts < $oppPts) $result = 'loss';
|
2026-04-12 20:03:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 16:44:57 +02:00
|
|
|
|
$redAvatarPath = $game->getRed()?->getAvatarPath();
|
|
|
|
|
|
$blueAvatarPath = $game->getBlue()?->getAvatarPath();
|
|
|
|
|
|
|
2026-04-12 20:03:20 +02:00
|
|
|
|
return [
|
2026-04-13 15:50:28 +02:00
|
|
|
|
'id' => $game->getId(),
|
2026-04-14 18:54:44 +02:00
|
|
|
|
'uuid' => $game->getUuid()?->toRfc4122(),
|
2026-04-13 15:50:28 +02:00
|
|
|
|
'redName' =>
|
|
|
|
|
|
$game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest',
|
|
|
|
|
|
'blueName' =>
|
|
|
|
|
|
$game->getBlue()?->getUsername() ?? $game->getBlueAnon()?->getUserName() ?? 'Guest',
|
2026-04-15 16:44:57 +02:00
|
|
|
|
'redAvatar' => $redAvatarPath ? $cacheManager->generateUrl($redAvatarPath, 'avatar_thumb') : null,
|
|
|
|
|
|
'blueAvatar' => $blueAvatarPath ? $cacheManager->generateUrl($blueAvatarPath, 'avatar_thumb') : null,
|
2026-04-13 15:50:28 +02:00
|
|
|
|
'redPoints' => $game->getRedPoints(),
|
|
|
|
|
|
'bluePoints' => $game->getBluePoints(),
|
|
|
|
|
|
'redExplodedBomb' => $game->getRedExplodedBomb(),
|
|
|
|
|
|
'blueExplodedBomb' => $game->getBlueExplodedBomb(),
|
|
|
|
|
|
'resign' => $resign,
|
|
|
|
|
|
'created' => $game->getCreated()?->format('Y-m-d H:i'),
|
|
|
|
|
|
'date' => $game->getUpdated()?->format('Y-m-d H:i'),
|
|
|
|
|
|
'isRed' => $isRed,
|
|
|
|
|
|
'result' => $result,
|
|
|
|
|
|
'myPoints' => $myPts,
|
|
|
|
|
|
'oppPoints' => $oppPts,
|
2026-04-18 13:44:15 +02:00
|
|
|
|
'redBonusPoints' => $game->getRedBonusPoints() ?? 0,
|
|
|
|
|
|
'blueBonusPoints' => $game->getBlueBonusPoints() ?? 0,
|
|
|
|
|
|
'redBonusStats' => $game->getRedBonusStats() ?? [],
|
|
|
|
|
|
'blueBonusStats' => $game->getBlueBonusStats() ?? [],
|
2026-04-12 20:03:20 +02:00
|
|
|
|
];
|
|
|
|
|
|
}, $recent),
|
|
|
|
|
|
'chartData' => [
|
2026-04-18 22:12:07 +02:00
|
|
|
|
'months' => $months,
|
|
|
|
|
|
'wins' => array_column(array_values($monthlyData), 'wins'),
|
|
|
|
|
|
'losses' => array_column(array_values($monthlyData), 'losses'),
|
|
|
|
|
|
'draws' => array_column(array_values($monthlyData), 'draws'),
|
|
|
|
|
|
'pieWins' => $wins,
|
|
|
|
|
|
'pieLosses' => $losses,
|
|
|
|
|
|
'pieDraws' => $draws,
|
|
|
|
|
|
'recentGames' => $this->buildRecentGamesSeries($user, $userId),
|
2026-04-12 20:03:20 +02:00
|
|
|
|
],
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 22:12:07 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Build per-game data for the last 15 finished games, oldest → newest.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array{labels:string[],mines:int[],bonus:float[]}
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function buildRecentGamesSeries(User $user, int $userId): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$recent = $this->repo->findRecentFinishedForUser($user, 15);
|
|
|
|
|
|
$recent = array_reverse($recent);
|
|
|
|
|
|
|
|
|
|
|
|
$labels = [];
|
|
|
|
|
|
$mines = [];
|
|
|
|
|
|
$bonus = [];
|
|
|
|
|
|
foreach ($recent as $i => $game) {
|
|
|
|
|
|
$isRed = $game->getRed()?->getId() === $userId;
|
|
|
|
|
|
$labels[] = '#' . ($i + 1);
|
2026-04-19 22:09:03 +02:00
|
|
|
|
$mines[] = (int)($isRed ? $game->getRedPoints() : $game->getBluePoints());
|
|
|
|
|
|
$bonus[] = (float)($isRed ? $game->getRedBonusPoints() : $game->getBlueBonusPoints()) ?: 0;
|
2026-04-18 22:12:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ['labels' => $labels, 'mines' => $mines, 'bonus' => $bonus];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
#[Route(
|
|
|
|
|
|
'/battle/{uuid}',
|
|
|
|
|
|
name: 'MineSeekerBundle_battle_share',
|
|
|
|
|
|
requirements: ['uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'],
|
|
|
|
|
|
methods: ['GET'],
|
|
|
|
|
|
)]
|
|
|
|
|
|
public function battleShare(Uuid $uuid): Response
|
2026-04-12 20:03:20 +02:00
|
|
|
|
{
|
2026-04-14 18:54:44 +02:00
|
|
|
|
$game = $this->repo->findOneBy(['uuid' => $uuid]);
|
2026-04-12 20:03:20 +02:00
|
|
|
|
if (!$game) {
|
|
|
|
|
|
throw $this->createNotFoundException('Battle not found.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
$redName = $game->getRed()?->getUsername() ?? ($game->getRedAnon() !== null ? 'Anonymous' : 'Guest');
|
|
|
|
|
|
$blueName = $game->getBlue()?->getUsername() ?? ($game->getBlueAnon() !== null ? 'Anonymous' : 'Guest');
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$redPts = $game->getRedPoints();
|
|
|
|
|
|
$bluePts = $game->getBluePoints();
|
|
|
|
|
|
$resign = $game->getResign();
|
2026-04-15 16:44:57 +02:00
|
|
|
|
$redAvatar = $game->getRed()?->getAvatarPath();
|
|
|
|
|
|
$blueAvatar = $game->getBlue()?->getAvatarPath();
|
2026-04-18 13:44:15 +02:00
|
|
|
|
$redBonusPoints = $game->getRedBonusPoints() ?? 0;
|
|
|
|
|
|
$blueBonusPoints = $game->getBlueBonusPoints() ?? 0;
|
|
|
|
|
|
$redBonusStats = $game->getRedBonusStats() ?? [];
|
|
|
|
|
|
$blueBonusStats = $game->getBlueBonusStats() ?? [];
|
2026-04-12 20:03:20 +02:00
|
|
|
|
|
|
|
|
|
|
if ($resign === 'red') {
|
|
|
|
|
|
$summary = "$redName resigned — $blueName wins";
|
|
|
|
|
|
} elseif ($resign === 'blue') {
|
|
|
|
|
|
$summary = "$blueName resigned — $redName wins";
|
|
|
|
|
|
} elseif ($redPts !== null && $bluePts !== null) {
|
2026-04-13 15:50:28 +02:00
|
|
|
|
if ($redPts > $bluePts) {
|
|
|
|
|
|
$summary = "$redName defeated $blueName ($redPts – $bluePts)";
|
|
|
|
|
|
} elseif ($bluePts > $redPts) {
|
|
|
|
|
|
$summary = "$blueName defeated $redName ($bluePts – $redPts)";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$summary = "$redName and $blueName drew ($redPts – $bluePts)";
|
|
|
|
|
|
}
|
2026-04-12 20:03:20 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
$summary = "$redName vs $blueName";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $this->render('Game/battle_share.html.twig', [
|
2026-04-19 22:09:03 +02:00
|
|
|
|
'game' => $game,
|
|
|
|
|
|
'redName' => $redName,
|
|
|
|
|
|
'blueName' => $blueName,
|
|
|
|
|
|
'redPts' => $redPts,
|
|
|
|
|
|
'bluePts' => $bluePts,
|
|
|
|
|
|
'resign' => $resign,
|
|
|
|
|
|
'redAvatar' => $redAvatar,
|
|
|
|
|
|
'blueAvatar' => $blueAvatar,
|
|
|
|
|
|
'redBonusPoints' => $redBonusPoints,
|
|
|
|
|
|
'blueBonusPoints' => $blueBonusPoints,
|
|
|
|
|
|
'redBonusStats' => $redBonusStats,
|
|
|
|
|
|
'blueBonusStats' => $blueBonusStats,
|
|
|
|
|
|
'ogTitle' => "MineSeeker · $summary",
|
|
|
|
|
|
'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.",
|
2026-04-13 15:50:28 +02:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
#[Route(
|
|
|
|
|
|
'/og/battle/{uuid}.png',
|
|
|
|
|
|
name: 'MineSeekerBundle_og_battle',
|
|
|
|
|
|
requirements: ['uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'],
|
|
|
|
|
|
methods: ['GET'],
|
|
|
|
|
|
)]
|
|
|
|
|
|
public function battleOgImage(Uuid $uuid, BattleCardGenerator $generator): BinaryFileResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$game = $this->repo->findOneBy(['uuid' => $uuid]);
|
|
|
|
|
|
if (!$game) {
|
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$path = $generator->generate($game);
|
|
|
|
|
|
$response = new BinaryFileResponse($path);
|
|
|
|
|
|
$response->headers->set('Content-Type', 'image/png');
|
|
|
|
|
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
|
|
|
|
|
|
$response->setMaxAge(86400 * 30);
|
|
|
|
|
|
$response->setPublic();
|
|
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 15:50:28 +02:00
|
|
|
|
#[Route('/profile/avatar', name: 'MineSeekerBundle_profile_avatar', methods: ['POST'])]
|
|
|
|
|
|
public function uploadAvatar(
|
|
|
|
|
|
Request $request,
|
|
|
|
|
|
EntityManagerInterface $em,
|
|
|
|
|
|
CacheManager $cacheManager,
|
|
|
|
|
|
#[Autowire(service: 'mineseeker.media.storage')] FilesystemOperator $mediaStorage,
|
|
|
|
|
|
): JsonResponse {
|
|
|
|
|
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
|
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
|
|
$file = $request->files->get('avatar');
|
|
|
|
|
|
if (!$file instanceof UploadedFile) {
|
|
|
|
|
|
return $this->json(['error' => 'No file uploaded.'], 400);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($file->getSize() > 2 * 1024 * 1024) {
|
|
|
|
|
|
return $this->json(['error' => 'File is too large. Maximum 2 MB.'], 400);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
|
|
|
|
if (!in_array($file->getMimeType(), $allowed, true)) {
|
|
|
|
|
|
return $this->json(['error' => 'Invalid type. Allowed: JPEG, PNG, GIF, WEBP.'], 400);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$ext = $file->guessExtension() ?? 'jpg';
|
2026-04-14 16:53:16 +02:00
|
|
|
|
$newPath = sprintf('avatar/%s.%s', Uuid::v4()->toRfc4122(), $ext);
|
2026-04-13 15:50:28 +02:00
|
|
|
|
$oldPath = $user->getAvatarPath();
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
/** Remove old file and any cached thumbnails */
|
2026-04-13 15:50:28 +02:00
|
|
|
|
if ($oldPath) {
|
2026-04-14 16:53:16 +02:00
|
|
|
|
try {
|
|
|
|
|
|
$mediaStorage->delete($oldPath);
|
2026-04-14 18:54:44 +02:00
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
|
$this->logger->error('Unable to delete old avatar: ' . $oldPath);
|
2026-04-13 15:50:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
$cacheManager->remove($oldPath, 'avatar_thumb');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
|
/** Upload original to MinIO media/avatar/ */
|
|
|
|
|
|
$stream = fopen($file->getPathname(), 'rb');
|
|
|
|
|
|
try {
|
|
|
|
|
|
$mediaStorage->writeStream($newPath, $stream);
|
|
|
|
|
|
} catch (FilesystemException $e) {
|
|
|
|
|
|
$this->logger->error('Unable to write new avatar: ' . $e->getMessage());
|
|
|
|
|
|
throw new RuntimeException('Unable to write new avatar: ' . $e->getMessage());
|
|
|
|
|
|
}
|
2026-04-13 15:50:28 +02:00
|
|
|
|
fclose($stream);
|
|
|
|
|
|
|
|
|
|
|
|
$user->setAvatarPath($newPath);
|
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
|
|
return $this->json([
|
|
|
|
|
|
'thumbUrl' => $cacheManager->generateUrl($newPath, 'avatar_thumb'),
|
2026-04-11 20:45:51 +02:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
2026-04-12 15:19:03 +02:00
|
|
|
|
|
|
|
|
|
|
#[Route('/profile/security', name: 'MineSeekerBundle_profile_security')]
|
|
|
|
|
|
public function security(): Response
|
|
|
|
|
|
{
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
|
|
|
|
|
|
|
|
|
|
|
$credentials = $this->webAuthnService->getCredentialsForUser($user);
|
2026-04-12 17:55:57 +02:00
|
|
|
|
$credentialsData = array_map(fn($cred) => [
|
|
|
|
|
|
'id' => $cred->getId(),
|
|
|
|
|
|
'credentialName' => $cred->getCredentialName(),
|
|
|
|
|
|
'createdAt' => $cred->getCreatedAt()?->format('Y-m-d H:i:s'),
|
|
|
|
|
|
'lastUsedAt' => $cred->getLastUsedAt()?->format('Y-m-d H:i:s'),
|
|
|
|
|
|
'isBackupEligible' => $cred->isBackupEligible(),
|
2026-04-12 15:19:03 +02:00
|
|
|
|
'isBackupAuthenticated' => $cred->isBackupAuthenticated(),
|
|
|
|
|
|
], $credentials);
|
|
|
|
|
|
|
|
|
|
|
|
return $this->render('Security/profile_security.html.twig', [
|
2026-04-12 17:55:57 +02:00
|
|
|
|
'credentials' => $credentialsData,
|
|
|
|
|
|
'isTotpEnabled' => $user->isTotpAuthenticationEnabled(),
|
2026-04-14 18:54:44 +02:00
|
|
|
|
'backupCodesCount' => count($user->getBackupCodes()),
|
2026-04-12 15:19:03 +02:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
2026-04-11 20:45:51 +02:00
|
|
|
|
}
|