diff --git a/.env.example b/.env.example index cff20cf..cdc2cd4 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,6 @@ GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles STATIC_ROOT=/static +MAIL_FROM=mapguesser@mapguesser-dev.ch +MAIL_HOST=mail +MAIL_PORT=2500 diff --git a/composer.json b/composer.json index 0b434e7..4b32718 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "license": "GNU GPL 3.0", "require": { "vlucas/phpdotenv": "^4.1", - "symfony/console": "^5.1" + "symfony/console": "^5.1", + "phpmailer/phpmailer": "^6.1" }, "require-dev": {}, "autoload": { diff --git a/composer.lock b/composer.lock index ad2a23a..65cf8b5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,76 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "13a0eaff2786786caff2be86ac704fc7", + "content-hash": "67a75c3149ef859545476427e7f2f686", "packages": [ + { + "name": "phpmailer/phpmailer", + "version": "v6.1.6", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3", + "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "doctrine/annotations": "^1.2", + "friendsofphp/php-cs-fixer": "^2.2", + "phpunit/phpunit": "^4.8 || ^5.7" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "funding": [ + { + "url": "https://github.com/synchro", + "type": "github" + } + ], + "time": "2020-05-27T12:24:03+00:00" + }, { "name": "phpoption/phpoption", "version": "1.7.3", diff --git a/database/migrations/data/20200612_2124_map_area.php b/database/migrations/data/20200612_2124_map_area.php index d6460ef..dae5e11 100644 --- a/database/migrations/data/20200612_2124_map_area.php +++ b/database/migrations/data/20200612_2124_map_area.php @@ -10,8 +10,6 @@ $select->columns(['id', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', $result = $select->execute(); -\Container::$dbConnection->startTransaction(); - while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { $bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']); @@ -20,5 +18,3 @@ while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { $modify->set('area', $bounds->calculateApproximateArea()); $modify->save(); } - -\Container::$dbConnection->commit(); diff --git a/database/migrations/data/20200614_1328_user_confirmation.php b/database/migrations/data/20200614_1328_user_confirmation.php new file mode 100644 index 0000000..fd7aa0b --- /dev/null +++ b/database/migrations/data/20200614_1328_user_confirmation.php @@ -0,0 +1,17 @@ +columns(['id']); + +$result = $select->execute(); + +while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->setId($map['id']); + $modify->set('active', true); + $modify->save(); +} diff --git a/database/migrations/structure/20200614_1328_user_confirmation.sql b/database/migrations/structure/20200614_1328_user_confirmation.sql new file mode 100644 index 0000000..92ad62c --- /dev/null +++ b/database/migrations/structure/20200614_1328_user_confirmation.sql @@ -0,0 +1,14 @@ +CREATE TABLE `user_confirmations` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `token` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `token` (`token`), + CONSTRAINT `user_confirmations_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +ALTER TABLE + `users` +ADD + `active` tinyint(1) NOT NULL DEFAULT 0; diff --git a/docker-compose.yml b/docker-compose.yml index 3effcfe..b506eb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: - .:/var/www/mapguesser links: - 'mariadb' + - 'mail' mariadb: image: mariadb:10.1 volumes: @@ -17,5 +18,10 @@ services: MYSQL_DATABASE: 'mapguesser' MYSQL_USER: 'mapguesser' MYSQL_PASSWORD: 'mapguesser' + mail: + image: marcopas/docker-mailslurper:latest + ports: + - 8080:8080 + - 8085:8085 volumes: mysql: diff --git a/mail/signup.tpl b/mail/signup.tpl new file mode 100644 index 0000000..c93b404 --- /dev/null +++ b/mail/signup.tpl @@ -0,0 +1,13 @@ +Hi, + +You recently signed up on MapGuesser with this email address ({{EMAIL}}). To activate your account, please click on the following link: +{{ACTIVATE_LINK}} + +If you did not sign up on MapGuesser or changed your mind, no further action is required, your email address will be deleted soon. +However if you want to immediately delete it, please click on the following link: +{{CANCEL_LINK}} + +Have fun on MapGuesser! + +Regards, +MapGuesser diff --git a/main.php b/main.php index aeab442..c283f94 100644 --- a/main.php +++ b/main.php @@ -15,6 +15,7 @@ class Container static MapGuesser\Interfaces\Database\IConnection $dbConnection; static MapGuesser\Routing\RouteCollection $routeCollection; static \SessionHandlerInterface $sessionHandler; + static MapGuesser\Interfaces\Request\IRequest $request; } Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); diff --git a/public/index.php b/public/index.php index e4b5722..ad425a0 100644 --- a/public/index.php +++ b/public/index.php @@ -2,7 +2,6 @@ require '../web.php'; -$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; $method = strtolower($_SERVER['REQUEST_METHOD']); $url = substr($_SERVER['REQUEST_URI'], strlen('/')); if (($pos = strpos($url, '?')) !== false) { @@ -15,10 +14,10 @@ $match = Container::$routeCollection->match($method, explode('/', $url)); if ($match !== null) { list($route, $params) = $match; - $request = new MapGuesser\Request\Request($_GET, $params, $_POST, $_SESSION); + Container::$request->setParsedRouteParams($params); $handler = $route->getHandler(); - $controller = new $handler[0]($request); + $controller = new $handler[0](Container::$request); if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) { $authorized = $controller->authorize(); @@ -26,7 +25,7 @@ if ($match !== null) { $authorized = true; } - if ($method === 'post' && $request->post('anti_csrf_token') !== $request->session()->get('anti_csrf_token')) { + if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) { header('Content-Type: text/html; charset=UTF-8', true, 403); echo json_encode(['error' => 'no_valid_anti_csrf_token']); return; @@ -41,7 +40,7 @@ if ($match !== null) { return; } elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) { - header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode()); + header('Location: ' . Container::$request->getBase() . '/' . $response->getUrl(), true, $response->getHttpCode()); return; } diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index 1fec7b8..c2c24df 100644 --- a/public/static/css/mapguesser.css +++ b/public/static/css/mapguesser.css @@ -69,8 +69,9 @@ sub { bottom: -0.4em; } -.mono { - font-family: 'Roboto Mono', monospace; +hr { + border: solid #bbbbbb 1px; + margin: 10px 0; } .bold { @@ -261,13 +262,18 @@ div.modal { visibility: hidden; } -p.formError { +p.error, p.formError { color: #7f2929; font-weight: 500; +} + +p.formError { display: none; } div.header { + display: grid; + grid-template-columns: auto auto; background-color: #333333; height: 50px; line-height: 50px; @@ -275,30 +281,25 @@ div.header { color: white; } -div.header>div.grid { - display: grid; - grid-template-columns: auto auto; -} - div.header.small { height: 40px; line-height: 40px; } -div.header>div.grid>:nth-child(2) { +div.header>p.header { line-height: inherit; text-align: right; } -div.header>div.grid>:nth-child(2)>span { +div.header>p.header>span { padding-left: 6px; } -div.header>div.grid>:nth-child(2)>span>a:link, div.header>div.grid>:nth-child(2)>span>a:visited { +div.header>p.header>span>a:link, div.header>p.header>span>a:visited { color: inherit; } -div.header>div.grid>:nth-child(2)>span:not(:last-child) { +div.header>p.header>span:not(:last-child) { border-right: solid white 1px; padding-right: 6px; } @@ -337,7 +338,7 @@ div.box { } @media screen and (max-width: 599px) { - div.header.small h1 span { + div.header h1 span { display: none; } button, a.button { diff --git a/public/static/js/login.js b/public/static/js/login.js index 809b49e..22f306c 100644 --- a/public/static/js/login.js +++ b/public/static/js/login.js @@ -13,7 +13,10 @@ var errorText; switch (this.response.error) { case 'user_not_found': - errorText = 'No user found with the given email address.'; + errorText = 'No user found with the given email address. You can sign up here!'; + break; + case 'user_not_active': + errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!'; break; case 'password_not_match': errorText = 'The given password is wrong.' diff --git a/public/static/js/mapguesser.js b/public/static/js/mapguesser.js index f870be9..1b9c2f7 100644 --- a/public/static/js/mapguesser.js +++ b/public/static/js/mapguesser.js @@ -72,7 +72,7 @@ var MapGuesser = { closeButton.classList.add('gray'); closeButton.classList.add('marginTop'); - closeButton.textContent = 'Cancel'; + closeButton.textContent = 'Close'; closeButton.onclick = function () { MapGuesser.hideModal(); }; @@ -88,6 +88,14 @@ var MapGuesser = { } document.getElementById('cover').style.visibility = 'hidden'; + }, + + toggleDisableOnChange: function (input, button) { + if (input.defaultValue !== input.value) { + button.disabled = false; + } else { + button.disabled = true; + } } }; diff --git a/public/static/js/profile.js b/public/static/js/profile.js new file mode 100644 index 0000000..744f99b --- /dev/null +++ b/public/static/js/profile.js @@ -0,0 +1,51 @@ +(function () { + var form = document.getElementById('profileForm'); + + form.elements.password_new.onkeyup = function () { + MapGuesser.toggleDisableOnChange(this, form.elements.save); + }; + + form.elements.password_new_confirm.onkeyup = function () { + MapGuesser.toggleDisableOnChange(this, form.elements.save); + }; + + form.onsubmit = function (e) { + document.getElementById('loading').style.visibility = 'visible'; + + e.preventDefault(); + + var formData = new FormData(form); + + MapGuesser.httpRequest('POST', form.action, function () { + document.getElementById('loading').style.visibility = 'hidden'; + + if (this.response.error) { + var errorText; + switch (this.response.error) { + case 'password_not_match': + errorText = 'The given current password is wrong.' + break; + case 'passwords_too_short': + errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!' + break; + case 'passwords_not_match': + errorText = 'The given new passwords do not match.' + break; + } + + var profileFormError = document.getElementById('profileFormError'); + profileFormError.style.display = 'block'; + profileFormError.innerHTML = errorText; + + form.elements.password_new.select(); + + return; + } + + document.getElementById('profileFormError').style.display = 'none'; + form.reset(); + form.elements.save.disabled = true; + form.elements.password_new.focus(); + }, formData); + }; +})(); diff --git a/public/static/js/signup.js b/public/static/js/signup.js new file mode 100644 index 0000000..4031b32 --- /dev/null +++ b/public/static/js/signup.js @@ -0,0 +1,47 @@ +(function () { + var form = document.getElementById('signupForm'); + + form.onsubmit = function (e) { + document.getElementById('loading').style.visibility = 'visible'; + + e.preventDefault(); + + var formData = new FormData(form); + + MapGuesser.httpRequest('POST', form.action, function () { + document.getElementById('loading').style.visibility = 'hidden'; + + if (this.response.error) { + var errorText; + switch (this.response.error) { + case 'passwords_too_short': + errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!' + break; + case 'passwords_not_match': + errorText = 'The given passwords do not match.' + break; + case 'user_found': + errorText = 'There is a user already registered with the given email address. Please login here!'; + break; + case 'not_active_user_found': + errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!'; + break; + } + + var signupFormError = document.getElementById('signupFormError'); + signupFormError.style.display = 'block'; + signupFormError.innerHTML = errorText; + + form.elements.email.select(); + + return; + } + + document.getElementById('signupFormError').style.display = 'none'; + form.reset(); + form.elements.email.focus(); + + MapGuesser.showModalWithContent('Sign up successful', 'Sign up was successful. Please check your email and click on the activation link to activate your account!'); + }, formData); + }; +})(); diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index 5f124d4..a8227af 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -53,6 +53,11 @@ class LoginController $user = new User($userData); + if (!$user->getActive()) { + $data = ['error' => 'user_not_active']; + return new JsonContent($data); + } + if (!$user->checkPassword($this->request->post('password'))) { $data = ['error' => 'password_not_match']; return new JsonContent($data); @@ -68,6 +73,6 @@ class LoginController { $this->request->session()->delete('user'); - return new Redirect([\Container::$routeCollection->getRoute('login'), []], IRedirect::TEMPORARY); + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); } } diff --git a/src/Controller/SignupController.php b/src/Controller/SignupController.php new file mode 100644 index 0000000..ea049bc --- /dev/null +++ b/src/Controller/SignupController.php @@ -0,0 +1,192 @@ +request = $request; + } + + public function getSignupForm() + { + $session = $this->request->session(); + + if ($session->get('user')) { + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + $data = []; + return new HtmlContent('signup/signup', $data); + } + + public function signup(): IContent + { + $session = $this->request->session(); + + if ($session->get('user')) { + //TODO: return with some error + $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) { + $user = new User($userData); + + if ($user->getActive()) { + $data = ['error' => 'user_found']; + } else { + $data = ['error' => 'not_active_user_found']; + } + return new JsonContent($data); + } + + if (strlen($this->request->post('password')) < 6) { + $data = ['error' => 'passwords_too_short']; + return new JsonContent($data); + } + + if ($this->request->post('password') !== $this->request->post('password_confirm')) { + $data = ['error' => 'passwords_not_match']; + return new JsonContent($data); + } + + $user = new User([ + 'email' => $this->request->post('email'), + ]); + + $user->setPlainPassword($this->request->post('password')); + + \Container::$dbConnection->startTransaction(); + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->fill($user->toArray()); + $modify->save(); + $userId = $modify->getId(); + + $token = hash('sha256', serialize($user) . random_bytes(10) . microtime()); + + $modify = new Modify(\Container::$dbConnection, 'user_confirmations'); + $modify->set('user_id', $userId); + $modify->set('token', $token); + $modify->save(); + + \Container::$dbConnection->commit(); + + $this->sendConfirmationEmail($user->getEmail(), $token); + + $data = ['success' => true]; + return new JsonContent($data); + } + + public function activate() + { + $session = $this->request->session(); + + if ($session->get('user')) { + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + $select = new Select(\Container::$dbConnection, 'user_confirmations'); + $select->columns(['id', 'user_id']); + $select->where('token', '=', $this->request->query('token')); + + $confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($confirmation === null) { + $data = []; + return new HtmlContent('signup/activate', $data); + } + + \Container::$dbConnection->startTransaction(); + + $modify = new Modify(\Container::$dbConnection, 'user_confirmations'); + $modify->setId($confirmation['id']); + $modify->delete(); + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->setId($confirmation['user_id']); + $modify->set('active', true); + $modify->save(); + + \Container::$dbConnection->commit(); + + $select = new Select(\Container::$dbConnection, 'users'); + $select->columns(User::getFields()); + $select->whereId($confirmation['user_id']); + + $userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + $user = new User($userData); + + $session->set('user', $user); + + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + public function cancel() + { + $session = $this->request->session(); + + if ($session->get('user')) { + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + $select = new Select(\Container::$dbConnection, 'user_confirmations'); + $select->columns(['id', 'user_id']); + $select->where('token', '=', $this->request->query('token')); + + $confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($confirmation === null) { + $data = ['success' => false]; + return new HtmlContent('signup/cancel', $data); + } + + \Container::$dbConnection->startTransaction(); + + $modify = new Modify(\Container::$dbConnection, 'user_confirmations'); + $modify->setId($confirmation['id']); + $modify->delete(); + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->setId($confirmation['user_id']); + $modify->delete(); + + \Container::$dbConnection->commit(); + + $data = ['success' => true]; + return new HtmlContent('signup/cancel', $data); + } + + private function sendConfirmationEmail($email, $token): void + { + $mail = new Mail(); + $mail->addRecipient($email); + $mail->setSubject('Welcome to MapGuesser - Activate your account'); + $mail->setBodyFromTemplate('signup', [ + 'EMAIL' => $email, + 'ACTIVATE_LINK' => $this->request->getBase() . '/signup/activate/' . $token, + 'CANCEL_LINK' => $this->request->getBase() . '/signup/cancel/' . $token, + ]); + $mail->send(); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..b9541fa --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,56 @@ +request = $request; + } + + public function getProfile(): IContent + { + $user = $this->request->user(); + + $data = ['user' => $user->toArray()]; + return new HtmlContent('profile', $data); + } + + public function saveProfile(): IContent + { + $user = $this->request->user(); + + if (!$user->checkPassword($this->request->post('password'))) { + $data = ['error' => 'password_not_match']; + return new JsonContent($data); + } + + if (strlen($this->request->post('password_new')) > 0) { + if (strlen($this->request->post('password_new')) < 6) { + $data = ['error' => 'passwords_too_short']; + return new JsonContent($data); + } + + if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) { + $data = ['error' => 'passwords_not_match']; + return new JsonContent($data); + } + + $user->setPlainPassword($this->request->post('password_new')); + } + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->fill($user->toArray()); + $modify->save(); + + $data = ['success' => true]; + return new JsonContent($data); + } +} diff --git a/src/Interfaces/Authentication/IUser.php b/src/Interfaces/Authentication/IUser.php index 363cfe8..f95824c 100644 --- a/src/Interfaces/Authentication/IUser.php +++ b/src/Interfaces/Authentication/IUser.php @@ -7,4 +7,6 @@ interface IUser const PERMISSION_ADMIN = 1; public function hasPermission(int $permission): bool; + + public function getDisplayName(): string; } diff --git a/src/Interfaces/Request/IRequest.php b/src/Interfaces/Request/IRequest.php index bb9c402..351550f 100644 --- a/src/Interfaces/Request/IRequest.php +++ b/src/Interfaces/Request/IRequest.php @@ -4,6 +4,10 @@ use MapGuesser\Interfaces\Authentication\IUser; interface IRequest { + public function setParsedRouteParams(array &$routeParams); + + public function getBase(): string; + public function query(string $key); public function post(string $key); diff --git a/src/Mailing/Mail.php b/src/Mailing/Mail.php new file mode 100644 index 0000000..4c75dcb --- /dev/null +++ b/src/Mailing/Mail.php @@ -0,0 +1,81 @@ +recipients[] = [$mail, $name]; + } + + public function setSubject(string $subject): void + { + $this->subject = $subject; + } + + public function setBody(string $body): void + { + $this->body = $body; + } + + public function setBodyFromTemplate(string $template, array $params = []): void + { + $this->body = file_get_contents(ROOT . '/mail/' . $template . '.tpl'); + + foreach ($params as $key => $param) { + $this->body = str_replace('{{' . $key . '}}', $param, $this->body); + } + } + + public function send(): void + { + $mailer = new PHPMailer(true); + + $mailer->CharSet = 'utf-8'; + $mailer->Hostname = substr($_ENV['MAIL_FROM'], strpos($_ENV['MAIL_FROM'], '@') + 1); + + if (!empty($_ENV['MAIL_HOST'])) { + $mailer->Mailer = 'smtp'; + $mailer->Host = $_ENV['MAIL_HOST']; + $mailer->Port = !empty($_ENV['MAIL_PORT']) ? $_ENV['MAIL_PORT'] : 25; + $mailer->SMTPSecure = !empty($_ENV['MAIL_SECURE']) ? $_ENV['MAIL_SECURE'] : ''; + + if (!empty($_ENV['MAIL_USER'])) { + $mailer->SMTPAuth = true; + $mailer->Username = $_ENV['MAIL_USER']; + $mailer->Password = $_ENV['MAIL_PASSWORD']; + } else { + $mailer->SMTPAuth = false; + } + } else { + $mailer->Mailer = 'mail'; + } + + $mailer->setFrom($_ENV['MAIL_FROM'], 'MapGuesser'); + $mailer->addReplyTo($_ENV['MAIL_FROM'], 'MapGuesser'); + + $mailer->Sender = !empty($_ENV['MAIL_BOUNCE']) ? $_ENV['MAIL_BOUNCE'] : $_ENV['MAIL_FROM']; + + $mailer->Subject = $this->subject; + $mailer->Body = $this->body; + + foreach ($this->recipients as $recipient) { + $this->sendMail($mailer, $recipient); + } + } + + private function sendMail(PHPMailer $mailer, array $recipient) + { + $mailer->clearAddresses(); + $mailer->addAddress($recipient[0], $recipient[1]); + + $mailer->send(); + } +} diff --git a/src/Model/User.php b/src/Model/User.php index cdc34dd..0814a78 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -6,7 +6,7 @@ class User extends BaseModel implements IUser { private static array $types = ['user', 'admin']; - protected static array $fields = ['email', 'password', 'type']; + protected static array $fields = ['email', 'password', 'type', 'active']; private string $email; @@ -14,6 +14,8 @@ class User extends BaseModel implements IUser private string $type = 'user'; + private bool $active = false; + public function setEmail(string $email): void { $this->email = $email; @@ -36,6 +38,11 @@ class User extends BaseModel implements IUser } } + public function setActive($active): void + { + $this->active = (bool) $active; + } + public function getEmail(): string { return $this->email; @@ -51,6 +58,11 @@ class User extends BaseModel implements IUser return $this->type; } + public function getActive(): bool + { + return $this->active; + } + public function hasPermission(int $permission): bool { switch ($permission) { @@ -63,6 +75,11 @@ class User extends BaseModel implements IUser } } + public function getDisplayName(): string + { + return $this->email; + } + public function checkPassword(string $password): bool { return password_verify($password, $this->password); diff --git a/src/Request/Request.php b/src/Request/Request.php index 2c10f16..fb95646 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -3,26 +3,37 @@ use MapGuesser\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\ISession; -use MapGuesser\Model\User; class Request implements IRequest { + private string $base; + private array $get; - private array $routeParams; + private array $routeParams = []; private array $post; private Session $session; - public function __construct(array &$get, array &$routeParams, array &$post, array &$session) + public function __construct(string $base, array &$get, array &$post, array &$session) { + $this->base = $base; $this->get = &$get; - $this->routeParams = &$routeParams; $this->post = &$post; $this->session = new Session($session); } + public function setParsedRouteParams(array &$routeParams) + { + $this->routeParams = &$routeParams; + } + + public function getBase(): string + { + return $this->base; + } + public function query($key) { if (isset($this->get[$key])) { diff --git a/views/admin/map_editor.php b/views/admin/map_editor.php index fcb92c1..a3e2360 100644 --- a/views/admin/map_editor.php +++ b/views/admin/map_editor.php @@ -14,41 +14,39 @@ $jsFiles = [ ?>
-
-

- - - MapGuesser - -

-

- - - - - - 0 - - - 0 - - - - 0 -

-
+

+ + + MapGuesser + +

+

+ + + + + + 0 + + + 0 + + + + 0 +

\ No newline at end of file diff --git a/views/templates/main_header.php b/views/templates/main_header.php index 89b5a87..bf8404d 100644 --- a/views/templates/main_header.php +++ b/views/templates/main_header.php @@ -28,6 +28,6 @@
\ No newline at end of file diff --git a/web.php b/web.php index e00c293..9d1ff68 100644 --- a/web.php +++ b/web.php @@ -15,7 +15,13 @@ Container::$routeCollection = new MapGuesser\Routing\RouteCollection(); 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('signup', 'signup', [MapGuesser\Controller\SignupController::class, 'getSignupForm']); +Container::$routeCollection->post('signup-action', 'signup', [MapGuesser\Controller\SignupController::class, 'signup']); +Container::$routeCollection->get('signup.activate', 'signup/activate/{token}', [MapGuesser\Controller\SignupController::class, 'activate']); +Container::$routeCollection->get('signup.cancel', 'signup/cancel/{token}', [MapGuesser\Controller\SignupController::class, 'cancel']); Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']); +Container::$routeCollection->get('profile', 'profile', [MapGuesser\Controller\UserController::class, 'getProfile']); +Container::$routeCollection->post('profile-action', 'profile', [MapGuesser\Controller\UserController::class, 'saveProfile']); 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']); @@ -40,6 +46,8 @@ session_start([ 'cookie_samesite' => 'Lax' ]); -if (!isset($_SESSION['anti_csrf_token'])) { - $_SESSION['anti_csrf_token'] = hash('sha256', random_bytes(10) . microtime()); +Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION); + +if (!Container::$request->session()->has('anti_csrf_token')) { + Container::$request->session()->set('anti_csrf_token', hash('sha256', random_bytes(10) . microtime())); }