diff --git a/database/migrations/structure/20200606_2352_users.sql b/database/migrations/structure/20200606_2352_users.sql
new file mode 100644
index 0000000..c3078a9
--- /dev/null
+++ b/database/migrations/structure/20200606_2352_users.sql
@@ -0,0 +1,8 @@
+CREATE TABLE `users` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `email` varchar(100) NOT NULL,
+ `password` varchar(60) NOT NULL,
+ `type` enum('user', 'admin') NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `email` (`email`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
diff --git a/mapg b/mapg
index ab89fe9..6409a95 100755
--- a/mapg
+++ b/mapg
@@ -6,5 +6,6 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\DatabaseMigration());
+$app->add(new MapGuesser\Cli\AddUserCommand());
$app->run();
diff --git a/public/index.php b/public/index.php
index 829da88..d90278f 100644
--- a/public/index.php
+++ b/public/index.php
@@ -12,6 +12,9 @@ if (($pos = strpos($url, '?')) !== false) {
$url = rawurldecode($url);
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
+Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
+Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']);
+Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
@@ -30,15 +33,32 @@ $match = Container::$routeCollection->match($method, explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
- $response = $route->callController($params);
+ $request = new MapGuesser\Request\Request($_GET, $params, $_POST, $_SESSION);
- if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
- header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
- echo $response->render();
- } elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
- header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode());
+ $handler = $route->getHandler();
+ $controller = new $handler[0]($request);
+
+ if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) {
+ $authorized = $controller->authorize();
+ } else {
+ $authorized = true;
+ }
+
+ if ($authorized) {
+ $response = call_user_func([$controller, $handler[1]]);
+
+ if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
+ header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
+ echo $response->render();
+
+ return;
+ } elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
+ header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode());
+
+ return;
+ }
}
-} else {
- header('Content-Type: text/html; charset=UTF-8', true, 404);
- require ROOT . '/views/error/404.php';
}
+
+header('Content-Type: text/html; charset=UTF-8', true, 404);
+require ROOT . '/views/error/404.php';
diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css
index d96bc8a..e95792f 100644
--- a/public/static/css/mapguesser.css
+++ b/public/static/css/mapguesser.css
@@ -17,7 +17,7 @@ button::-moz-focus-inner, input::-moz-focus-inner {
border: 0;
}
-p, h1, h2, button, a {
+p, h1, h2, input, textarea, select, button, a {
font-family: 'Roboto', sans-serif;
}
@@ -93,6 +93,10 @@ sub {
margin-bottom: 10px;
}
+.right {
+ text-align: right;
+}
+
svg.inline, img.inline {
display: inline-block;
width: 1em;
@@ -158,6 +162,54 @@ button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
background-color: #7f2929;
}
+input, select, textarea {
+ background-color: #f9fafb;
+ border: solid #c8d2e1 1px;
+ border-radius: 2px;
+ padding: 4px;
+ box-sizing: border-box;
+ font-size: 15px;
+ font-weight: 300;
+}
+
+textarea {
+ font-size: 13px;
+ resize: none;
+}
+
+input.big, select.big, textarea.big {
+ padding: 5px;
+ font-size: 18px;
+}
+
+input.fullWidth, select.fullWidth, textarea.fullWidth {
+ display: block;
+ width: 100%;
+}
+
+input:disabled, select:disabled, textarea:disabled {
+ background-color: #dfdfdf;
+ border: solid #dfdfdf 1px;
+ color: #000000;
+}
+
+input:focus, select:focus, textarea:focus {
+ background-color: #ffffff;
+ border: solid #29457f 2px;
+ padding: 3px;
+ outline: none;
+}
+
+input.big:focus, select.big:focus, textarea.big:focus {
+ padding: 4px;
+}
+
+p.formError {
+ color: #7f2929;
+ font-weight: 500;
+ display: none;
+}
+
div.header {
background-color: #333333;
height: 50px;
@@ -200,6 +252,15 @@ div.buttonContainer>button {
visibility: hidden;
}
+div.box {
+ width: 576px;
+ background-color: #eeeeee;
+ border-radius: 3px;
+ margin: 10px auto;
+ padding: 10px;
+ box-sizing: border-box;
+}
+
@media screen and (max-width: 599px) {
div.header.small h1 span {
display: none;
@@ -208,4 +269,7 @@ div.buttonContainer>button {
padding: 0;
width: 100%;
}
+ div.box {
+ width: initial;
+ }
}
diff --git a/public/static/js/login.js b/public/static/js/login.js
new file mode 100644
index 0000000..0297b14
--- /dev/null
+++ b/public/static/js/login.js
@@ -0,0 +1,42 @@
+(function () {
+ var form = document.getElementById('loginForm');
+
+ form.onsubmit = function (e) {
+ document.getElementById('loading').style.visibility = 'visible';
+
+ e.preventDefault();
+
+ var formData = new FormData(form);
+
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = 'json';
+ xhr.onload = function () {
+ document.getElementById('loading').style.visibility = 'hidden';
+
+ if (this.response.error) {
+ var errorText;
+ switch (this.response.error) {
+ case 'user_not_found':
+ errorText = 'No user found with the given email address.';
+ break;
+ case 'password_not_match':
+ errorText = 'The given password is wrong.'
+ break;
+ }
+
+ var loginFormError = document.getElementById('loginFormError');
+ loginFormError.style.display = 'block';
+ loginFormError.innerHTML = errorText;
+
+ form.elements.email.select();
+
+ return;
+ }
+
+ window.location.replace('/');
+ };
+
+ xhr.open('POST', form.action, true);
+ xhr.send(formData);
+ };
+})();
diff --git a/src/Cli/AddUserCommand.php b/src/Cli/AddUserCommand.php
new file mode 100644
index 0000000..45fb57d
--- /dev/null
+++ b/src/Cli/AddUserCommand.php
@@ -0,0 +1,51 @@
+setName('user:add')
+ ->setDescription('Adding of user.')
+ ->addArgument('email', InputArgument::REQUIRED, 'Email of user')
+ ->addArgument('password', InputArgument::REQUIRED, 'Password of user')
+ ->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $user = new User([
+ 'email' => $input->getArgument('email'),
+ ]);
+
+ $user->setPlainPassword($input->getArgument('password'));
+
+ if ($input->hasArgument('type')) {
+ $user->setType($input->getArgument('type'));
+ }
+
+ try {
+ $modify = new Modify(\Container::$dbConnection, 'users');
+ $modify->fill($user->toArray());
+ $modify->save();
+ } catch (\Exception $e) {
+ $output->writeln('Adding user failed!');
+ $output->writeln('');
+
+ $output->writeln((string) $e);
+ $output->writeln('');
+
+ return 1;
+ }
+
+ $output->writeln('User was successfully added!');
+
+ return 0;
+ }
+}
diff --git a/src/Controller/GameController.php b/src/Controller/GameController.php
index 246f9b8..1664623 100644
--- a/src/Controller/GameController.php
+++ b/src/Controller/GameController.php
@@ -2,6 +2,7 @@
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
+use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
@@ -9,16 +10,23 @@ use MapGuesser\Interfaces\Response\IContent;
class GameController
{
- public function getGame(array $parameters): IContent
+ private IRequest $request;
+
+ public function __construct(IRequest $request)
{
- $mapId = (int) $parameters['mapId'];
+ $this->request = $request;
+ }
+
+ public function getGame(): IContent
+ {
+ $mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new HtmlContent('game', $data);
}
- public function getGameJson(array $parameters): IContent
+ public function getGameJson(): IContent
{
- $mapId = (int) $parameters['mapId'];
+ $mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new JsonContent($data);
}
@@ -27,12 +35,14 @@ class GameController
{
$bounds = $this->getMapBounds($mapId);
- if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
- $_SESSION['state'] = [
+ $session = $this->request->session();
+
+ if (($state = $session->get('state')) && $state['mapId'] !== $mapId) {
+ $session->set('state', [
'mapId' => $mapId,
'area' => $bounds->calculateApproximateArea(),
'rounds' => []
- ];
+ ]);
}
return ['mapId' => $mapId, 'bounds' => $bounds->toArray()];
diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php
new file mode 100644
index 0000000..5f124d4
--- /dev/null
+++ b/src/Controller/LoginController.php
@@ -0,0 +1,73 @@
+request = $request;
+ }
+
+ public function getLoginForm()
+ {
+ $session = $this->request->session();
+
+ if ($session->get('user')) {
+ return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
+ }
+
+ $data = [];
+ return new HtmlContent('login', $data);
+ }
+
+ public function login(): IContent
+ {
+ $session = $this->request->session();
+
+ if ($session->get('user')) {
+ $data = ['success' => true];
+ return new JsonContent($data);
+ }
+
+ $select = new Select(\Container::$dbConnection, 'users');
+ $select->columns(User::getFields());
+ $select->where('email', '=', $this->request->post('email'));
+
+ $userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
+
+ if ($userData === null) {
+ $data = ['error' => 'user_not_found'];
+ return new JsonContent($data);
+ }
+
+ $user = new User($userData);
+
+ if (!$user->checkPassword($this->request->post('password'))) {
+ $data = ['error' => 'password_not_match'];
+ return new JsonContent($data);
+ }
+
+ $session->set('user', $user);
+
+ $data = ['success' => true];
+ return new JsonContent($data);
+ }
+
+ public function logout(): IRedirect
+ {
+ $this->request->session()->delete('user');
+
+ return new Redirect([\Container::$routeCollection->getRoute('login'), []], IRedirect::TEMPORARY);
+ }
+}
diff --git a/src/Controller/MapAdminController.php b/src/Controller/MapAdminController.php
index a0bd507..d96f8dc 100644
--- a/src/Controller/MapAdminController.php
+++ b/src/Controller/MapAdminController.php
@@ -1,22 +1,35 @@
request = $request;
$this->placeRepository = new PlaceRepository();
}
+ public function authorize(): bool
+ {
+ $user = $this->request->user();
+
+ return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
+ }
+
public function getMaps(): IContent
{
//TODO
@@ -24,9 +37,9 @@ class MapAdminController
return new HtmlContent('admin/maps');
}
- public function getMapEditor(array $parameters): IContent
+ public function getMapEditor(): IContent
{
- $mapId = (int) $parameters['mapId'];
+ $mapId = (int) $this->request->query('mapId');
$bounds = $this->getMapBounds($mapId);
@@ -36,9 +49,9 @@ class MapAdminController
return new HtmlContent('admin/map_editor', $data);
}
- public function getPlace(array $parameters)
+ public function getPlace()
{
- $placeId = (int) $parameters['placeId'];
+ $placeId = (int) $this->request->query('placeId');
$placeData = $this->placeRepository->getById($placeId);
diff --git a/src/Controller/PositionController.php b/src/Controller/PositionController.php
index 1564730..b804797 100644
--- a/src/Controller/PositionController.php
+++ b/src/Controller/PositionController.php
@@ -1,5 +1,6 @@
request = $request;
$this->placeRepository = new PlaceRepository();
}
- public function getPosition(array $parameters): IContent
+ public function getPosition(): IContent
{
- $mapId = (int) $parameters['mapId'];
+ $mapId = (int) $this->request->query('mapId');
- if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
+ $session = $this->request->session();
+
+ if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
}
- if (count($_SESSION['state']['rounds']) === 0) {
+ if (count($state['rounds']) === 0) {
$newPosition = $this->placeRepository->getForMapWithValidPano($mapId);
- $_SESSION['state']['rounds'][] = $newPosition;
+ $state['rounds'][] = $newPosition;
+ $session->set('state', $state);
$data = ['panoId' => $newPosition['panoId']];
} else {
- $rounds = count($_SESSION['state']['rounds']);
- $last = $_SESSION['state']['rounds'][$rounds - 1];
+ $rounds = count($state['rounds']);
+ $last = $state['rounds'][$rounds - 1];
$history = [];
for ($i = 0; $i < $rounds - 1; ++$i) {
- $round = $_SESSION['state']['rounds'][$i];
+ $round = $state['rounds'][$i];
$history[] = [
'position' => $round['position']->toArray(),
'guessPosition' => $round['guessPosition']->toArray(),
@@ -55,41 +62,45 @@ class PositionController
return new JsonContent($data);
}
- public function evaluateGuess(array $parameters): IContent
+ public function evaluateGuess(): IContent
{
- $mapId = (int) $parameters['mapId'];
+ $mapId = (int) $this->request->query('mapId');
- if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
+ $session = $this->request->session();
+
+ if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
}
- $last = &$_SESSION['state']['rounds'][count($_SESSION['state']['rounds']) - 1];
+ $last = $state['rounds'][count($state['rounds']) - 1];
$position = $last['position'];
- $guessPosition = new Position((float) $_POST['lat'], (float) $_POST['lng']);
-
- $last['guessPosition'] = $guessPosition;
+ $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$distance = $this->calculateDistance($position, $guessPosition);
- $score = $this->calculateScore($distance, $_SESSION['state']['area']);
+ $score = $this->calculateScore($distance, $state['area']);
+ $last['guessPosition'] = $guessPosition;
$last['distance'] = $distance;
$last['score'] = $score;
+ $state['rounds'][count($state['rounds']) - 1] = $last;
- if (count($_SESSION['state']['rounds']) < static::NUMBER_OF_ROUNDS) {
+ if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
$exclude = [];
- foreach ($_SESSION['state']['rounds'] as $round) {
+ foreach ($state['rounds'] as $round) {
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
}
$newPosition = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
- $_SESSION['state']['rounds'][] = $newPosition;
+ $state['rounds'][] = $newPosition;
+ $session->set('state', $state);
$panoId = $newPosition['panoId'];
} else {
- $_SESSION['state']['rounds'] = [];
+ $state['rounds'] = [];
+ $session->set('state', $state);
$panoId = null;
}
diff --git a/src/Interfaces/Authentication/IUser.php b/src/Interfaces/Authentication/IUser.php
new file mode 100644
index 0000000..363cfe8
--- /dev/null
+++ b/src/Interfaces/Authentication/IUser.php
@@ -0,0 +1,10 @@
+ $value) {
+ $method = 'set' . str_replace('_', '', ucwords($key, '_'));
+
+ if (method_exists($this, $method)) {
+ $this->$method($value);
+ }
+ }
+ }
+
+ public function setId($id): void
+ {
+ $this->id = $id;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ function toArray(): array
+ {
+ $array = [];
+
+ foreach (self::getFields() as $key) {
+ $method = 'get' . str_replace('_', '', ucwords($key, '_'));
+
+ if (method_exists($this, $method)) {
+ $array[$key] = $this->$method();
+ }
+ }
+
+ return $array;
+ }
+}
diff --git a/src/Model/User.php b/src/Model/User.php
new file mode 100644
index 0000000..cdc34dd
--- /dev/null
+++ b/src/Model/User.php
@@ -0,0 +1,70 @@
+email = $email;
+ }
+
+ public function setPassword(string $hashedPassword): void
+ {
+ $this->password = $hashedPassword;
+ }
+
+ public function setPlainPassword(string $plainPassword): void
+ {
+ $this->password = password_hash($plainPassword, PASSWORD_BCRYPT);
+ }
+
+ public function setType(string $type): void
+ {
+ if (in_array($type, self::$types)) {
+ $this->type = $type;
+ }
+ }
+
+ public function getEmail(): string
+ {
+ return $this->email;
+ }
+
+ public function getPassword(): string
+ {
+ return $this->password;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function hasPermission(int $permission): bool
+ {
+ switch ($permission) {
+ case IUser::PERMISSION_NORMAL:
+ return true;
+ break;
+ case IUser::PERMISSION_ADMIN:
+ return $this->type === 'admin';
+ break;
+ }
+ }
+
+ public function checkPassword(string $password): bool
+ {
+ return password_verify($password, $this->password);
+ }
+}
diff --git a/src/Request/Request.php b/src/Request/Request.php
new file mode 100644
index 0000000..2c10f16
--- /dev/null
+++ b/src/Request/Request.php
@@ -0,0 +1,57 @@
+get = &$get;
+ $this->routeParams = &$routeParams;
+ $this->post = &$post;
+ $this->session = new Session($session);
+ }
+
+ public function query($key)
+ {
+ if (isset($this->get[$key])) {
+ return $this->get[$key];
+ }
+
+ if (isset($this->routeParams[$key])) {
+ return $this->routeParams[$key];
+ }
+
+ return null;
+ }
+
+ public function post($key)
+ {
+ if (isset($this->post[$key])) {
+ return $this->post[$key];
+ }
+
+ return null;
+ }
+
+ public function session(): ISession
+ {
+ return $this->session;
+ }
+
+ public function user(): ?IUser
+ {
+ return $this->session->get('user');
+ }
+}
diff --git a/src/Request/Session.php b/src/Request/Session.php
new file mode 100644
index 0000000..f1fedab
--- /dev/null
+++ b/src/Request/Session.php
@@ -0,0 +1,37 @@
+data = &$data;
+ }
+
+ public function has($key): bool
+ {
+ return isset($this->data[$key]);
+ }
+
+ public function get($key)
+ {
+ if (isset($this->data[$key])) {
+ return $this->data[$key];
+ }
+
+ return null;
+ }
+
+ public function set($key, $value): void
+ {
+ $this->data[$key] = $value;
+ }
+
+ public function delete($key): void
+ {
+ unset($this->data[$key]);
+ }
+}
diff --git a/src/Routing/Route.php b/src/Routing/Route.php
index af5f659..afe5491 100644
--- a/src/Routing/Route.php
+++ b/src/Routing/Route.php
@@ -20,6 +20,11 @@ class Route
return $this->id;
}
+ public function getHandler(): array
+ {
+ return $this->handler;
+ }
+
public function generateLink(array $parameters = []): string
{
$link = [];
@@ -51,14 +56,6 @@ class Route
return implode('/', $link) . $query;
}
- public function callController(array $parameters)
- {
- $controllerName = $this->handler[0];
- $controller = new $controllerName();
-
- return call_user_func([$controller, $this->handler[1]], $parameters);
- }
-
public function testAgainst(array $path): ?array
{
$parameters = [];
diff --git a/views/login.php b/views/login.php
new file mode 100644
index 0000000..565e7aa
--- /dev/null
+++ b/views/login.php
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file