1
0
mirror of https://github.com/chylex/Lightning-Tracker.git synced 2025-04-10 02:15:43 +02:00

Add user registration and login pages

This commit is contained in:
chylex 2020-08-03 06:09:49 +02:00
parent 53a25994f2
commit 04f86b8b06
11 changed files with 404 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types = 1);
namespace Pages\Controllers\Handlers;
use Pages\Controllers\IControlHandler;
use Pages\IAction;
use Routing\Request;
use Session\Session;
use function Pages\Actions\redirect;
class RequireLoginState implements IControlHandler{
private bool $should_be_logged_in;
public function __construct(bool $should_be_logged_in){
$this->should_be_logged_in = $should_be_logged_in;
}
public function run(Request $req, Session $sess): ?IAction{
if ($this->should_be_logged_in !== $sess->isLoggedOn()){
return redirect([BASE_URL_ENC,
$req->getBasePath()->encoded(),
$this->should_be_logged_in ? 'login?return='.rawurlencode(ltrim($_SERVER['REQUEST_URI'], '/')) : '']);
}
return null;
}
}
?>

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace Pages\Controllers\Mixed;
use Generator;
use Pages\Controllers\AbstractHandlerController;
use Pages\Controllers\Handlers\RequireLoginState;
use Pages\IAction;
use Pages\Models\Mixed\LoginModel;
use Pages\Views\Mixed\LoginPage;
use Routing\Request;
use Session\Session;
use function Pages\Actions\redirect;
use function Pages\Actions\view;
class LoginController extends AbstractHandlerController{
protected function prerequisites(): Generator{
yield new RequireLoginState(false);
}
protected function finally(Request $req, Session $sess): IAction{
$model = new LoginModel($req);
$data = $req->getData();
if (!empty($data) && $model->loginUser($data, $sess)){
return redirect([BASE_URL_ENC,
$req->getBasePath()->encoded(),
isset($_GET['return']) ? ltrim($_GET['return'], '/') : '']);
}
return view(new LoginPage($model->load()));
}
}
?>

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types = 1);
namespace Pages\Controllers\Mixed;
use Database\Objects\TrackerInfo;
use Generator;
use Pages\Controllers\AbstractHandlerController;
use Pages\IAction;
use Pages\Models\BasicMixedPageModel;
use Pages\Models\ErrorModel;
use Pages\Models\Mixed\RegisterModel;
use Pages\Views\ErrorPage;
use Pages\Views\Mixed\RegisterPage;
use Routing\Request;
use Session\Session;
use function Pages\Actions\redirect;
use function Pages\Actions\view;
class RegisterController extends AbstractHandlerController{
protected function prerequisites(): Generator{
yield from [];
}
protected function finally(Request $req, Session $sess): IAction{
if (isset($_GET['success'])){
$model = new RegisterModel($req, true);
return view(new RegisterPage($model->load()));
}
if (!SYS_ENABLE_REGISTRATION){
$page_model = new BasicMixedPageModel($req);
$error_model = new ErrorModel($page_model, 'Registration Error', 'User registrations are disabled by the administrator.');
return view(new ErrorPage($error_model->load()));
}
if ($sess->isLoggedOn()){
return redirect([BASE_URL_ENC, $req->getBasePath()->encoded()]);
}
$model = new RegisterModel($req);
$data = $req->getData();
if (!empty($data) && $model->registerUser($data, $sess)){
return redirect([BASE_URL_ENC, $req->getBasePath()->encoded(), 'register?success']);
}
return view(new RegisterPage($model->load()));
}
}
?>

View File

@ -31,6 +31,16 @@ abstract class AbstractPageModel implements IModel{
$this->nav = $this->createNavigation();
$this->setupNavigation($this->nav, $logon_user);
if ($logon_user !== null){
}
else{
$this->nav->addRight(Text::withIcon('Login', 'enter'), '/login');
if (SYS_ENABLE_REGISTRATION){
$this->nav->addRight(Text::withIcon('Register', 'user'), '/register');
}
}
return $this;
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types = 1);
namespace Pages\Models;
use Routing\Request;
class BasicMixedPageModel extends AbstractWrapperModel{
public function __construct(Request $req){
parent::__construct(new BasicRootPageModel($req)); // TODO
}
}
?>

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types = 1);
namespace Pages\Models\Mixed;
use Database\DB;
use Database\Tables\UserTable;
use Exception;
use Pages\Components\Forms\FormComponent;
use Pages\Components\Text;
use Pages\Models\BasicMixedPageModel;
use Routing\Request;
use Session\Session;
class LoginModel extends BasicMixedPageModel{
private FormComponent $form;
public function __construct(Request $req){
parent::__construct($req);
$this->form = new FormComponent();
$this->form->addTextField('Name')
->label('Username')
->type('text')
->autocomplete('username');
$this->form->addTextField('Password')
->type('password')
->autocomplete('current-password');
$this->form->addButton('submit', 'Login')
->icon('enter');
}
public function getForm(): FormComponent{
return $this->form;
}
public function loginUser(array $data, Session $sess): bool{
if (!$this->form->accept($data)){
return false;
}
try{
$users = new UserTable(DB::get());
$login_info = $users->getLoginInfo($data['Name']);
if ($login_info === null || !$login_info->checkPassword($data['Password'])){
$this->form->addMessage(FormComponent::MESSAGE_ERROR, Text::warning('Invalid username or password.'));
return false;
}
return $sess->tryLoginWithId($login_info->getId());
}catch(Exception $e){
$this->form->onGeneralError($e);
return false;
}
}
}
?>

View File

@ -0,0 +1,117 @@
<?php
declare(strict_types = 1);
namespace Pages\Models\Mixed;
use Database\DB;
use Database\SQL;
use Database\Tables\UserTable;
use Exception;
use LogicException;
use Pages\Components\Forms\FormComponent;
use Pages\Models\BasicMixedPageModel;
use PDOException;
use Routing\Request;
use Session\Session;
use Validation\ValidationException;
use Validation\Validator;
class RegisterModel extends BasicMixedPageModel{
private FormComponent $form;
private bool $successful_login;
public function __construct(Request $req, bool $successful_login = false){
parent::__construct($req);
$this->form = new FormComponent();
$this->form->addTextField('Name')
->label('Username')
->type('text')
->autocomplete('username');
$this->form->addTextField('Password')
->type('password')
->autocomplete('new-password');
$this->form->addTextField('PasswordRepeated')
->label('Confirm Password')
->type('password')
->autocomplete('new-password');
$this->form->addTextField('Email')
->type('email')
->autocomplete('email');
$this->form->addButton('submit', 'Register')
->icon('pencil');
$this->successful_login = $successful_login;
}
public function getForm(): FormComponent{
return $this->form;
}
public function isSuccessfulLogin(): bool{
return $this->successful_login;
}
public function registerUser(array $data, Session $sess): bool{
if (!$this->form->accept($data)){
return false;
}
$name = $data['Name'];
$email = $data['Email'];
$password = $data['Password'];
$password_repeated = $data['PasswordRepeated'];
$validator = new Validator(); // TODO deduplicate validation of same fields in other places
$validator->str('Name', $name)->notEmpty();
$validator->str('Email', $email)->notEmpty()->contains('@', 'Email is not valid.');
$validator->str('Password', $password)->minLength(7)->maxLength(72);
$validator->str('PasswordRepeated', $password_repeated)->isTrue(fn($v): bool => $v === $password, 'Passwords do not match.');
try{
$validator->validate();
$users = new UserTable(DB::get());
$users->addUser($name, $email, $password);
if ($sess->tryLoginWithName($name)){
return true;
}
else{
throw new LogicException('Could not login a newly registered user.');
}
}catch(ValidationException $e){
$this->form->invalidateFields($e->getFields());
}catch(PDOException $e){
if ($e->getCode() === SQL::CONSTRAINT_VIOLATION){
try{
$users = new UserTable(DB::get());
if ($users->checkEmailExists($email)){
$this->form->invalidateField('Email', 'User with this email already exists.');
return false;
}
elseif ($users->checkNameExists($name)){
$this->form->invalidateField('Name', 'User with this name already exists.');
return false;
}
}catch(Exception $e){
$this->form->onGeneralError($e);
return false;
}
}
$this->form->onGeneralError($e);
}catch(Exception $e){
$this->form->onGeneralError($e);
}
return false;
}
}
?>

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace Pages\Views\Mixed;
use Pages\Components\Forms\FormComponent;
use Pages\Models\Mixed\LoginModel;
use Pages\Views\AbstractPage;
class LoginPage extends AbstractPage{
private LoginModel $model;
public function __construct(LoginModel $model){
parent::__construct($model);
$this->model = $model;
}
protected function getHeading(): string{
return 'Login';
}
protected function getLayout(): string{
return self::LAYOUT_COMPACT;
}
protected function echoPageHead(){
FormComponent::echoHead();
}
protected function echoPageBody(): void{
$this->model->getForm()->echoBody();
}
}
?>

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace Pages\Views\Mixed;
use Pages\Components\Forms\FormComponent;
use Pages\Models\Mixed\RegisterModel;
use Pages\Views\AbstractPage;
class RegisterPage extends AbstractPage{
private RegisterModel $model;
public function __construct(RegisterModel $model){
parent::__construct($model);
$this->model = $model;
}
protected function getHeading(): string{
return 'Register';
}
protected function getLayout(): string{
return self::LAYOUT_COMPACT;
}
protected function echoPageHead(){
FormComponent::echoHead();
}
protected function echoPageBody(): void{
if ($this->model->isSuccessfulLogin()){
echo <<<HTML
<p>Registration successful, you are now logged in!</p>
HTML;
}
else{
$this->model->getForm()->echoBody();
}
}
}
?>

View File

@ -1,6 +1,8 @@
<?php
define('BASE_URL', 'http://localhost');
define('SYS_ENABLE_REGISTRATION', true);
define('DB_DRIVER', 'mysql');
define('DB_NAME', 'tracker');
define('DB_HOST', 'localhost');

View File

@ -53,6 +53,9 @@ $router = new Router();
$router->add('&/about', 'Root/AboutController');
$router->add('&/login', 'Mixed/LoginController');
$router->add('&/register', 'Mixed/RegisterController');
function handle_error(int $code, string $title, string $message): void{
http_response_code($code);
$page_model = new BasicRootPageModel(new Request('', '', []));