Implermented login
This commit is contained in:
parent
923d6ca242
commit
51c20b55a0
30 changed files with 2495 additions and 39 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -123,4 +123,5 @@ Thumbs.db
|
|||
# ------------
|
||||
|
||||
# ignore public directory (we build it on server)
|
||||
/public
|
||||
/public
|
||||
/data/
|
||||
|
|
|
@ -12,6 +12,14 @@
|
|||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-httphandlerrunner": "^2.10",
|
||||
"ralouphie/mimey": "^1.0",
|
||||
"dotenv-org/phpdotenv-vault": "^0.2.4"
|
||||
"dotenv-org/phpdotenv-vault": "^0.2.4",
|
||||
"doctrine/orm": "^3.2",
|
||||
"doctrine/dbal": "^4.0",
|
||||
"symfony/uid": "^7.1",
|
||||
"php-curl-class/php-curl-class": "^9.19",
|
||||
"symfony/cache": "^7.1",
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-xdebug": "*"
|
||||
}
|
||||
}
|
||||
|
|
1777
composer.lock
generated
1777
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@
|
|||
|
||||
$main-container-max-width: 992px;
|
||||
|
||||
.navbar :first-child,
|
||||
.navigation-container,
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: $main-container-max-width;
|
||||
|
@ -11,3 +11,30 @@ main {
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ratio-1 {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 3rem;
|
||||
|
||||
.avatar-login-method-icon {
|
||||
scale: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
.mode-switch {
|
||||
text-align: center;
|
||||
padding-top: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.navigation-container {
|
||||
.navbar-brand {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
import "./theme";
|
||||
import "./theme";
|
||||
|
||||
import "bootstrap/js/src/collapse";
|
79
src/php/Api/DiscordAPI.php
Normal file
79
src/php/Api/DiscordAPI.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Api;
|
||||
|
||||
use Curl\Curl;
|
||||
use Exception;
|
||||
use GamesShop\Environment\EnvironmentHandler;
|
||||
use GamesShop\Errors\ExtendedException;
|
||||
|
||||
final class DiscordAPI
|
||||
{
|
||||
const string OAUTH_TOKEN_URL = "https://discord.com/api/oauth2/token";
|
||||
const string USER_ME_URL = 'https://discord.com/api/users/@me';
|
||||
|
||||
public function __construct(
|
||||
private readonly EnvironmentHandler $env
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, global_name: string, avatar: string, discriminator: int}
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getUserFromCode(string $code, string $redirectUri): array {
|
||||
$discordEnv = $this->env->getDiscordEnvironment();
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
$curl->setBasicAuthentication($discordEnv->clientId, $discordEnv->clientSecret);
|
||||
|
||||
$data =[
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $redirectUri
|
||||
];
|
||||
$curl->post(self::OAUTH_TOKEN_URL, $data);
|
||||
|
||||
if ($curl->error) {
|
||||
$curl->diagnose();
|
||||
|
||||
throw new ExtendedException($curl->errorMessage, [ 'response' => $curl->response, 'data' => $data ]);
|
||||
}
|
||||
|
||||
$accessToken = $curl->response->access_token;
|
||||
$tokenType = $curl->response->token_type;
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setHeader("authorization", "$tokenType $accessToken");
|
||||
$curl->get(self::USER_ME_URL);
|
||||
|
||||
if ($curl->error) {
|
||||
$curl->diagnose();
|
||||
|
||||
throw new ExtendedException($curl->errorMessage, [ 'response' => $curl->response, ]);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $curl->response->id,
|
||||
'global_name' => $curl->response->global_name,
|
||||
'avatar' => $curl->response->avatar,
|
||||
'discriminator' => (int) $curl->response->discriminator
|
||||
];
|
||||
}
|
||||
|
||||
public function getAvatarURL(string $userId, string|int $avatarHash) {
|
||||
if (is_int($avatarHash)) {
|
||||
return "https://cdn.discordapp.com/embed/avatars/{$avatarHash}.png";
|
||||
}
|
||||
|
||||
$extension = 'png';
|
||||
if (str_starts_with($avatarHash, 'a_')) {
|
||||
$extension = 'gif';
|
||||
}
|
||||
|
||||
return "https://cdn.discordapp.com/avatars/{$userId}/{$avatarHash}.{$extension}";
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ final class ContainerHandler
|
|||
private static function createInstance()
|
||||
{
|
||||
self::$instance = new Container();
|
||||
self::$instance->delegate(new ReflectionContainer(true));
|
||||
$reflectionContainer = new ReflectionContainer(true);
|
||||
self::$instance->delegate($reflectionContainer);
|
||||
}
|
||||
}
|
33
src/php/DoctrineManager.php
Normal file
33
src/php/DoctrineManager.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\ORMSetup;
|
||||
use GamesShop\Environment\EnvironmentHandler;
|
||||
|
||||
final class DoctrineManager
|
||||
{
|
||||
public function setup() {
|
||||
$container = ContainerHandler::getInstance();
|
||||
|
||||
$environmentHandler = $container->get(EnvironmentHandler::class);
|
||||
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration(
|
||||
paths: [ Paths::PHP_SOURCE_PATH . '/Entities' ],
|
||||
isDevMode: !$environmentHandler->isProduction()
|
||||
);
|
||||
|
||||
$dbEnvironment = $environmentHandler->getDatabaseEnvironment();
|
||||
$connection = DriverManager::getConnection($dbEnvironment->getDoctrineConfig());
|
||||
|
||||
$entityManager = new EntityManager($connection, $config);
|
||||
|
||||
$container->addShared(EntityManager::class, $entityManager);
|
||||
$container->addShared(Connection::class, $connection);
|
||||
}
|
||||
|
||||
}
|
85
src/php/Entities/Account/User.php
Normal file
85
src/php/Entities/Account/User.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Entities\Account;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use GamesShop\Login\LoginMethod;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Component\Uid\UuidV4;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: "users")]
|
||||
final class User
|
||||
{
|
||||
|
||||
#[ORM\Id()]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
private int|null $id = null;
|
||||
#[ORM\Column(type: 'integer', enumType: LoginMethod::class)]
|
||||
private LoginMethod $loginMethod;
|
||||
#[ORM\Column]
|
||||
private string|null $foreignLoginId = null;
|
||||
#[ORM\Column]
|
||||
private string $name;
|
||||
#[ORM\Column]
|
||||
private string $profilePictureUrl;
|
||||
#[ORM\Column]
|
||||
private UserPermission $permission;
|
||||
|
||||
public function __construct(LoginMethod $loginMethod, ?string $foreignLoginId, string $name, string $profilePictureUrl, UserPermission $permission)
|
||||
{
|
||||
$this->loginMethod = $loginMethod;
|
||||
$this->foreignLoginId = $foreignLoginId;
|
||||
$this->name = $name;
|
||||
$this->profilePictureUrl = $profilePictureUrl;
|
||||
$this->permission = $permission;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLoginMethod(): LoginMethod
|
||||
{
|
||||
return $this->loginMethod;
|
||||
}
|
||||
|
||||
public function getForeignLoginId(): ?string
|
||||
{
|
||||
return $this->foreignLoginId;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getProfilePictureUrl(): string
|
||||
{
|
||||
return $this->profilePictureUrl;
|
||||
}
|
||||
|
||||
public function getPermission(): UserPermission
|
||||
{
|
||||
return $this->permission;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function setProfilePictureUrl(string $profilePictureUrl): void
|
||||
{
|
||||
$this->profilePictureUrl = $profilePictureUrl;
|
||||
}
|
||||
|
||||
public function setPermission(UserPermission $permission): void
|
||||
{
|
||||
$this->permission = $permission;
|
||||
}
|
||||
}
|
34
src/php/Entities/SystemAttribute.php
Normal file
34
src/php/Entities/SystemAttribute.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'attributes')]
|
||||
final class SystemAttribute
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column]
|
||||
private string $value;
|
||||
|
||||
public function __construct(string $name, string $value)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
21
src/php/Environment/DatabaseEnvironment.php
Normal file
21
src/php/Environment/DatabaseEnvironment.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Environment;
|
||||
|
||||
final readonly class DatabaseEnvironment
|
||||
{
|
||||
public function __construct(
|
||||
public string $driver,
|
||||
public string $path
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDoctrineConfig() {
|
||||
return [
|
||||
'driver' => $this->driver,
|
||||
'path' => $this->path
|
||||
];
|
||||
}
|
||||
}
|
|
@ -25,4 +25,16 @@ final class EnvironmentHandler
|
|||
$_SERVER['DISCORD_CLIENT_LOGIN_URI'],
|
||||
);
|
||||
}
|
||||
|
||||
public function getDatabaseEnvironment(): DatabaseEnvironment
|
||||
{
|
||||
return new DatabaseEnvironment(
|
||||
$_SERVER['DB_DRIVER'],
|
||||
$_SERVER['DB_PATH']
|
||||
);
|
||||
}
|
||||
|
||||
public function isProduction(): bool {
|
||||
return $_SERVER['PRODUCTION'] === 'true';
|
||||
}
|
||||
}
|
26
src/php/Errors/ExtendedException.php
Normal file
26
src/php/Errors/ExtendedException.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace GamesShop\Errors;
|
||||
|
||||
use Exception;
|
||||
use GamesShop\ContainerHandler;
|
||||
use Whoops\Handler\HandlerInterface;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
|
||||
|
||||
|
||||
final class ExtendedException extends Exception
|
||||
{
|
||||
public function __construct(string $message = "", array $additionals = [], int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$handler = ContainerHandler::get(HandlerInterface::class);
|
||||
|
||||
if (!($handler instanceof PrettyPageHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handler->addDataTable("Additional Info", $additionals);
|
||||
}
|
||||
}
|
8
src/php/Errors/WhoopsHandler.php
Normal file
8
src/php/Errors/WhoopsHandler.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Whoops\Run;
|
||||
|
||||
final class WhoopsHandler
|
||||
{
|
||||
}
|
47
src/php/Login/DiscordLoginProvider.php
Normal file
47
src/php/Login/DiscordLoginProvider.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Login;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GamesShop\Api\DiscordAPI;
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\Entities\Account\User;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class DiscordLoginProvider implements LoginProvider
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManager $entityManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUser(ServerRequestInterface $request): User
|
||||
{
|
||||
$discordApiHandler = ContainerHandler::get(DiscordAPI::class);
|
||||
$result = $discordApiHandler->getUserFromCode($request->getQueryParams()['code'], (string)$request->getUri()->withQuery(''));
|
||||
|
||||
$repo = $this->entityManager->getRepository(User::class);
|
||||
$users = $repo->findBy(['loginMethod' => LoginMethod::DISCORD, 'foreignLoginId' => $result['id']]);
|
||||
|
||||
$profilePictureUrl = $discordApiHandler->getAvatarURL($result['id'], $result['avatar'] ?? $result['discriminator'] % 5);
|
||||
if (!empty($users)) {
|
||||
$user = $users[0];
|
||||
$user->setName($result['global_name']);
|
||||
$user->setProfilePictureUrl($profilePictureUrl);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
$newUser = new User(
|
||||
LoginMethod::DISCORD,
|
||||
$result['id'],
|
||||
$result['global_name'],
|
||||
$profilePictureUrl,
|
||||
UserPermission::VIEWER
|
||||
);
|
||||
|
||||
return $newUser;
|
||||
}
|
||||
}
|
|
@ -3,15 +3,74 @@ declare(strict_types=1);
|
|||
|
||||
namespace GamesShop\Login;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Exception;
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\Entities\Account\User;
|
||||
|
||||
final class LoginHandler
|
||||
{
|
||||
/**
|
||||
* @return class-string[]
|
||||
*/
|
||||
private static array $providers;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManager $entityManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function isLoggedIn(): bool {
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
$this->ensureSession();
|
||||
|
||||
return isset($_SESSION['accountid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getLoginProvider(string $method): LoginProvider {
|
||||
$providers = self::getProviders();
|
||||
if (!array_key_exists($method, $providers)) {
|
||||
throw new Exception("Couldn't find method for login '{$method}'");
|
||||
}
|
||||
|
||||
return ContainerHandler::get($providers[$method]);
|
||||
}
|
||||
|
||||
public function setCurrentUser(User $user) {
|
||||
$this->ensureSession();
|
||||
|
||||
$_SESSION['accountid'] = $user->getId();
|
||||
}
|
||||
|
||||
public function getCurrentUser(): User {
|
||||
$this->ensureSession();
|
||||
|
||||
$userid = $_SESSION['accountid'];
|
||||
return $this->entityManager->getRepository(User::class)->find($userid);
|
||||
}
|
||||
|
||||
public function deleteSession() {
|
||||
$this->ensureSession();
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
private function ensureSession()
|
||||
{
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string[]
|
||||
*/
|
||||
private static function getProviders(): array
|
||||
{
|
||||
return self::$providers ??= [
|
||||
'discord' => DiscordLoginProvider::class
|
||||
];
|
||||
}
|
||||
}
|
16
src/php/Login/LoginMethod.php
Normal file
16
src/php/Login/LoginMethod.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Login;
|
||||
|
||||
enum LoginMethod: int
|
||||
{
|
||||
case DISCORD = 1;
|
||||
|
||||
public function getIconClass(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DISCORD => 'fa-discord',
|
||||
};
|
||||
}
|
||||
}
|
12
src/php/Login/LoginProvider.php
Normal file
12
src/php/Login/LoginProvider.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Login;
|
||||
|
||||
use GamesShop\Entities\Account\User;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
interface LoginProvider
|
||||
{
|
||||
public function getUser(ServerRequestInterface $request): User;
|
||||
}
|
19
src/php/Login/UserPermission.php
Normal file
19
src/php/Login/UserPermission.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Login;
|
||||
|
||||
enum UserPermission : int
|
||||
{
|
||||
case VIEWER = 1;
|
||||
case PROVIDER = 10;
|
||||
case ADMIN = 100;
|
||||
|
||||
public function getHumanReadableName() {
|
||||
return match ($this) {
|
||||
self::VIEWER => "Claimer",
|
||||
self::PROVIDER => "Provider",
|
||||
self::ADMIN => "Admin",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -8,4 +8,6 @@ final class Paths
|
|||
public const string ROOT_PATH = __DIR__ . '/../..';
|
||||
public const string PUBLIC_PATH = self::ROOT_PATH . '/public';
|
||||
public const string SOURCE_PATH = self::ROOT_PATH . '/src';
|
||||
|
||||
public const string PHP_SOURCE_PATH = self::SOURCE_PATH . '/php';
|
||||
}
|
|
@ -3,14 +3,25 @@ declare(strict_types=1);
|
|||
|
||||
namespace GamesShop\Routing;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\Environment\EnvironmentHandler;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Templates\TemplateEngine;
|
||||
use Laminas\Diactoros\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class LoginRoutes
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManager $entityManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function login(ServerRequestInterface $request) {
|
||||
$discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment();
|
||||
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage(
|
||||
|
@ -25,9 +36,42 @@ final class LoginRoutes
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function loginCallback(ServerRequestInterface $request, array $args): ResponseInterface {
|
||||
if (array_key_exists('error', $request->getQueryParams())) {
|
||||
return new Response\RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$method = $args['method'];
|
||||
$loginHandler = ContainerHandler::get(LoginHandler::class);
|
||||
$loginProvider = $loginHandler->getLoginProvider($method);
|
||||
|
||||
$user = $loginProvider->getUser($request);
|
||||
if ($user->getId() === null) {
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$loginHandler->setCurrentUser($user);
|
||||
return new Response\RedirectResponse('/');
|
||||
}
|
||||
|
||||
public function logout(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$loginHandler = ContainerHandler::get(LoginHandler::class);
|
||||
$loginHandler->deleteSession();
|
||||
|
||||
return new Response\RedirectResponse('/login');
|
||||
}
|
||||
|
||||
public static function addRoutes(\League\Route\Router $router): void {
|
||||
$routes = ContainerHandler::get(LoginRoutes::class);
|
||||
|
||||
$router->get('/login', $routes->login(...));
|
||||
$router->get('/login-callback/{method:word}', $routes->loginCallback(...));
|
||||
$router->get('/logout', $routes->logout(...));
|
||||
}
|
||||
}
|
|
@ -9,13 +9,14 @@ use GamesShop\Templates\TemplateEngine;
|
|||
use Laminas\Diactoros\Response;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use League\Container\Container;
|
||||
use League\Route\Http\Exception\NotFoundException;
|
||||
use League\Route\Strategy\ApplicationStrategy;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class Router
|
||||
{
|
||||
public function route()
|
||||
public function route(): ResponseInterface
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals(
|
||||
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
|
||||
|
@ -26,11 +27,12 @@ final class Router
|
|||
|
||||
IndexRoute::applyRoutes($router);
|
||||
LoginRoutes::addRoutes($router);
|
||||
SetupRoute::applyRoutes($router);
|
||||
ResourceRoute::addRouteEntry($router);
|
||||
|
||||
try {
|
||||
return $router->dispatch($request);
|
||||
} catch (\League\Route\Http\Exception\NotFoundException $e) {
|
||||
} catch (NotFoundException $e) {
|
||||
return (new ErrorRoute())->renderErrorPage(404);
|
||||
}
|
||||
}
|
||||
|
|
53
src/php/Routing/SetupRoute.php
Normal file
53
src/php/Routing/SetupRoute.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Routing;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GamesShop\Entities\SystemAttribute;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response\RedirectResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class SetupRoute
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManager $entityManager,
|
||||
private readonly LoginHandler $loginHandler
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface {
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$repo = $this->entityManager->getRepository(SystemAttribute::class);
|
||||
|
||||
$attribute = $repo->find('ADMIN_SETUP_COMPLETED');
|
||||
if ($attribute) {
|
||||
|
||||
return new RedirectResponse('/');
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
$user->setPermission(UserPermission::ADMIN);
|
||||
|
||||
$attribute = new SystemAttribute(
|
||||
'ADMIN_SETUP_COMPLETED',
|
||||
'true'
|
||||
);
|
||||
$this->entityManager->persist($attribute);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new RedirectResponse('/');
|
||||
}
|
||||
|
||||
public static function applyRoutes(\League\Route\Router $router) {
|
||||
$router->get('/setup-admin', self::class);
|
||||
}
|
||||
|
||||
}
|
13
src/php/SetupHandler.php
Normal file
13
src/php/SetupHandler.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop;
|
||||
|
||||
final class SetupHandler
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
15
src/php/bin/doctrine.php
Normal file
15
src/php/bin/doctrine.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
|
||||
use GamesShop\ContainerHandler;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
$entityManager = ContainerHandler::get(EntityManager::class);
|
||||
ConsoleRunner::run(
|
||||
new SingleManagerProvider($entityManager)
|
||||
);
|
11
src/php/bootstrap.php
Normal file
11
src/php/bootstrap.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\DoctrineManager;
|
||||
use GamesShop\Environment\EnvironmentHandler;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
ContainerHandler::get(EnvironmentHandler::class)->load();
|
||||
ContainerHandler::get(DoctrineManager::class)->setup();
|
|
@ -2,20 +2,23 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\DoctrineManager;
|
||||
use GamesShop\Environment\EnvironmentHandler;
|
||||
use GamesShop\Routing\Router;
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
use Whoops\Handler\HandlerInterface;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
ContainerHandler::get(EnvironmentHandler::class)->load();
|
||||
require_once __DIR__ . '/../src/php/bootstrap.php';
|
||||
|
||||
$whoops = new Run();
|
||||
$whoops->pushHandler(new PrettyPageHandler);
|
||||
$prettyPageHandler = new PrettyPageHandler();
|
||||
$whoops->pushHandler($prettyPageHandler);
|
||||
$whoops->register();
|
||||
|
||||
ContainerHandler::getInstance()->addShared(HandlerInterface::class, $prettyPageHandler);
|
||||
|
||||
$router = ContainerHandler::getInstance()->get(Router::class);
|
||||
$result = $router->route();
|
||||
|
||||
|
|
37
src/templates/layout/accountDisplay.php
Normal file
37
src/templates/layout/accountDisplay.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
|
||||
$loginHandler = ContainerHandler::get(LoginHandler::class);
|
||||
?>
|
||||
|
||||
<?php if ($loginHandler->isLoggedIn()):
|
||||
$user = $loginHandler->getCurrentUser();
|
||||
?>
|
||||
<div class="d-flex avatar justify-content-center">
|
||||
|
||||
<div class="avatar-icon h-100 position-relative me-2 ratio-1">
|
||||
<img src="<?= $user->getProfilePictureUrl(); ?>" class="rounded-circle h-100" alt="User Profile Picture" />
|
||||
<div class="position-absolute bottom-0 end-0 ratio-1 d-flex align-items-center z-1 avatar-login-method">
|
||||
<i class="fa-brands <?= $user->getLoginMethod()->getIconClass() ?>"></i>
|
||||
<span class="position-absolute w-100 h-100 bottom-0 end-0 ratio-1 bg-body rounded-circle z-n1 avatar-login-method-icon"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<span class="me-2 h-3">
|
||||
<?= $user->getName() ?>
|
||||
</span>
|
||||
<small class="text-muted">
|
||||
<?= $user->getPermission()->getHumanReadableName() ?>
|
||||
</small>
|
||||
</div>
|
||||
<div class="h-100 d-flex align-items-center ms-2">
|
||||
<a href="/logout">
|
||||
<i class="fa-solid fa-arrow-right-to-bracket fa-xl text-danger"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif ?>
|
|
@ -33,30 +33,7 @@ $resource = $resources->getResource($resourceEntry);
|
|||
<?php endforeach; ?>
|
||||
</head>
|
||||
<body class="vh-100 d-flex flex-column">
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">Game Shop</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-content" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar-content">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
|
||||
|
||||
<div class="d-flex mode-switch">
|
||||
<button title="Use dark mode" id="dark" class="btn btn-sm btn-default text-secondary">
|
||||
<i class="fa-regular fa-moon"></i>
|
||||
</button>
|
||||
<button title="Use light mode" id="light" class="btn btn-sm btn-default text-secondary">
|
||||
<i class="fa-regular fa-sun"></i>
|
||||
</button>
|
||||
<button title="Use system preferred mode" id="system" class="btn btn-sm btn-default text-secondary">
|
||||
<i class="fa-solid fa-display"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?= $this->insert('layout/navbar') ?>
|
||||
|
||||
<main class="mt-2 position-relative flex-grow-1">
|
||||
<?= $this->section('content'); ?>
|
||||
|
|
34
src/templates/layout/navbar.php
Normal file
34
src/templates/layout/navbar.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use GamesShop\ContainerHandler;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
|
||||
ContainerHandler::get(LoginHandler::class);
|
||||
|
||||
?>
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary main-navigation">
|
||||
<div class="container-fluid navigation-container">
|
||||
<a class="navbar-brand" href="/">Game Shop</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-content" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar-content">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
|
||||
|
||||
<?= $this->insert('layout/accountDisplay'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex mode-switch me-3">
|
||||
<button title="Use dark mode" id="dark" class="btn btn-sm btn-default text-secondary">
|
||||
<i class="fa-regular fa-moon"></i>
|
||||
</button>
|
||||
<button title="Use light mode" id="light" class="btn btn-sm btn-default text-secondary">
|
||||
<i class="fa-regular fa-sun"></i>
|
||||
</button>
|
||||
<button title="Use system preferred mode" id="system" class="btn btn-sm btn-default text-secondary">
|
||||
<i class="fa-solid fa-display"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
Loading…
Reference in a new issue