Create an insecure bank application
This commit is contained in:
commit
2133574cae
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/webroot/config.php
|
9
README.md
Normal file
9
README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Setup
|
||||
|
||||
- Use a typical Webserver + PHP + SQL setup
|
||||
- Initialize the database with the instructions from `db-init.sql`
|
||||
- Copy the `webroot` directory onto the webserver
|
||||
- Inside `webroot`, copy `config.sample.php` to `config.php` and enter the SQL credentials
|
||||
- Register on the webpage to get your own account
|
||||
- Make yourself an admin:
|
||||
Using an SQL management software, set the `admin` field to `1` in the entry of the table `user` that corresponds to your account.
|
33
db-init.sql
Normal file
33
db-init.sql
Normal file
@ -0,0 +1,33 @@
|
||||
CREATE TABLE `user` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`admin` bit(1) NOT NULL DEFAULT b'0',
|
||||
`balance` bigint(20) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
|
||||
CREATE TABLE `session` (
|
||||
`token` binary(32) NOT NULL,
|
||||
`user` int(10) unsigned NOT NULL,
|
||||
KEY `user` (`user`),
|
||||
CONSTRAINT `session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
|
||||
CREATE TABLE `booking` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`affected` int(10) unsigned NOT NULL,
|
||||
`time` bigint(20) NOT NULL,
|
||||
`type` tinyint(1) unsigned NOT NULL,
|
||||
`amount` bigint(20) NOT NULL,
|
||||
`related` int(10) unsigned DEFAULT NULL,
|
||||
`comment` varchar(100) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `affected` (`affected`),
|
||||
KEY `related` (`related`),
|
||||
CONSTRAINT `booking_ibfk_1` FOREIGN KEY (`affected`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `booking_ibfk_2` FOREIGN KEY (`related`) REFERENCES `user` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
7
webroot/autoload.php
Normal file
7
webroot/autoload.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
spl_autoload_register(function ($className) {
|
||||
$pathName = str_replace('\\', '/', $className);
|
||||
include __DIR__ . '/lib/' . $pathName . '.php';
|
||||
});
|
8
webroot/bookings.php
Normal file
8
webroot/bookings.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\BookingOverviewController;
|
||||
|
||||
(new BookingOverviewController())->run()->send();
|
6
webroot/config.sample.php
Normal file
6
webroot/config.sample.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
const CONFIG_SQL_DSN = 'mysql:host=localhost;dbname=bank';
|
||||
const CONFIG_SQL_USER = 'www-data';
|
||||
const CONFIG_SQL_PASSWORD = '';
|
8
webroot/deposit.php
Normal file
8
webroot/deposit.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\CashTransactionController;
|
||||
|
||||
(new CashTransactionController('/deposit.php'))->run()->send();
|
9
webroot/index.php
Normal file
9
webroot/index.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Model\Context;
|
||||
use View\FrontPage;
|
||||
|
||||
(new FrontPage(Context::init('/')))->send();
|
41
webroot/lib/Controller/BookingOverviewController.php
Normal file
41
webroot/lib/Controller/BookingOverviewController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Session;
|
||||
use View\BookingOverviewPage;
|
||||
use View\Sendable;
|
||||
|
||||
class BookingOverviewController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/bookings.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$userId = $this->context->session->user->id;
|
||||
$page = new BookingOverviewPage($this->context);
|
||||
|
||||
$sql = Sql::connection();
|
||||
$sql->query('START TRANSACTION');
|
||||
$stmt = $sql->prepare(
|
||||
'SELECT time, type, amount, comment, name as relatedName FROM booking
|
||||
LEFT JOIN user ON booking.related = user.id
|
||||
WHERE affected = ?
|
||||
ORDER BY time, booking.id'
|
||||
);
|
||||
$stmt->execute([$userId]);
|
||||
$page->bookings = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt = $sql->prepare('SELECT balance FROM user WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$page->finalBalance = $stmt->fetch(\PDO::FETCH_ASSOC)['balance'];
|
||||
|
||||
$sql->query('COMMIT');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
78
webroot/lib/Controller/CashTransactionController.php
Normal file
78
webroot/lib/Controller/CashTransactionController.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\User;
|
||||
use View\CashTransactionPage;
|
||||
use View\MoneyFormatter;
|
||||
use View\Sendable;
|
||||
|
||||
class CashTransactionController extends RestrictedPageController
|
||||
{
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$cashTransactionPage = new CashTransactionPage($this->context);
|
||||
if ($cashTransactionPage->formWasSent) {
|
||||
$cashTransactionPage->fieldCustomer = trim($cashTransactionPage->fieldCustomer);
|
||||
$error = false;
|
||||
|
||||
// find customer
|
||||
$user = User::byName($cashTransactionPage->fieldCustomer);
|
||||
if (empty($user)) {
|
||||
$cashTransactionPage->errorCustomerNotFound = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check amount
|
||||
$amount = MoneyFormatter::parseAmount($cashTransactionPage->fieldAmount);
|
||||
if ($amount === null) {
|
||||
$cashTransactionPage->errorAmountInvalid = true;
|
||||
$error = true;
|
||||
} elseif ($amount === 0) {
|
||||
$cashTransactionPage->errorAmountZero = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check text
|
||||
$text = $cashTransactionPage->fieldText;
|
||||
if (iconv_strlen($text) > 100) {
|
||||
$cashTransactionPage->errorTextTooLong = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// create the actual booking
|
||||
if (!$error) {
|
||||
switch ($this->context->currentPage) {
|
||||
case '/deposit.php':
|
||||
Transaction::run($this->context, function ($transaction) use ($user, $amount, $text) {
|
||||
$transaction->createBooking($user, 1, $amount, null, $text);
|
||||
});
|
||||
break;
|
||||
case '/withdraw.php':
|
||||
$success = Transaction::run($this->context, function ($transaction) use ($user, $amount, $text) {
|
||||
return $transaction->createBooking($user, 2, -$amount, null, $text);
|
||||
});
|
||||
if (!$success) {
|
||||
$cashTransactionPage->errorInsufficientFunds = true;
|
||||
$error = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('unknown page url');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$cashTransactionPage->fieldCustomer = '';
|
||||
$cashTransactionPage->fieldAmount = '';
|
||||
$cashTransactionPage->fieldText = '';
|
||||
|
||||
$cashTransactionPage->success = true;
|
||||
$cashTransactionPage->successCustomer = $user->name;
|
||||
$cashTransactionPage->successAmount = $amount;
|
||||
}
|
||||
}
|
||||
return $cashTransactionPage;
|
||||
}
|
||||
}
|
43
webroot/lib/Controller/LoginController.php
Normal file
43
webroot/lib/Controller/LoginController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Session;
|
||||
use Model\User;
|
||||
use View\LoginRedirection;
|
||||
use View\LoginPage;
|
||||
use View\Sendable;
|
||||
|
||||
class LoginController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/login.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$loginPage = new LoginPage($this->context);
|
||||
if ($loginPage->formWasSent) {
|
||||
$loginPage->fieldUsername = trim($loginPage->fieldUsername);
|
||||
|
||||
// find user
|
||||
$user = User::byName($loginPage->fieldUsername);
|
||||
|
||||
// check password
|
||||
// (use a dummy hash if no user was found, to make timing attacks harder)
|
||||
$pwHash = $user?->pwHash ?? '$argon2id$v=19$m=65536,t=4,p=1$WmxPVmd5aGdkandaNWZTcA$6hcqXkBJIGgWkcGLdqZeHhkV83JKtn5Ke7jXRS31X2s';
|
||||
|
||||
$pwValid = password_verify($loginPage->fieldPassword, $pwHash);
|
||||
|
||||
if ($pwValid && !empty($user)) {
|
||||
$this->context->session = Session::create($user);
|
||||
return new LoginRedirection($this->context);
|
||||
} else {
|
||||
$loginPage->errorLoginDataInvalid = true;
|
||||
}
|
||||
}
|
||||
return $loginPage;
|
||||
}
|
||||
}
|
21
webroot/lib/Controller/LogoutController.php
Normal file
21
webroot/lib/Controller/LogoutController.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use View\LogoutRedirection;
|
||||
use View\Sendable;
|
||||
|
||||
class LogoutController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/logout.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$this->context->session->destroy();
|
||||
return new LogoutRedirection();
|
||||
}
|
||||
}
|
61
webroot/lib/Controller/RegisterController.php
Normal file
61
webroot/lib/Controller/RegisterController.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Session;
|
||||
use Model\User;
|
||||
use View\LoginRedirection;
|
||||
use View\RegisterPage;
|
||||
use View\Sendable;
|
||||
|
||||
class RegisterController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/register.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$registerPage = new RegisterPage($this->context);
|
||||
if ($registerPage->formWasSent) {
|
||||
$registerPage->fieldUsername = trim($registerPage->fieldUsername);
|
||||
$error = false;
|
||||
|
||||
// check username
|
||||
if (empty($registerPage->fieldUsername)) {
|
||||
$registerPage->errorUsernameEmpty = true;
|
||||
$error = true;
|
||||
} elseif (iconv_strlen($registerPage->fieldUsername) > 20) {
|
||||
$registerPage->errorUsernameTooLong = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check password
|
||||
if ($registerPage->fieldPassword != $registerPage->fieldRepeatPassword) {
|
||||
$registerPage->errorPasswordsMismatch = true;
|
||||
$error = true;
|
||||
} else if (empty($registerPage->fieldPassword)) {
|
||||
$registerPage->errorPasswordEmpty = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// create account
|
||||
if (!$error) {
|
||||
$pwHash = password_hash($registerPage->fieldPassword, PASSWORD_ARGON2ID);
|
||||
$user = User::create($registerPage->fieldUsername, $pwHash);
|
||||
if (!empty($user)) {
|
||||
$this->context->session = Session::create($user);
|
||||
return new LoginRedirection($this->context);
|
||||
} else {
|
||||
$registerPage->errorUsernameInUse = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $registerPage;
|
||||
} else {
|
||||
return $registerPage;
|
||||
}
|
||||
}
|
||||
}
|
38
webroot/lib/Controller/RestrictedPageController.php
Normal file
38
webroot/lib/Controller/RestrictedPageController.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Context;
|
||||
use View\AccessDeniedPage;
|
||||
use View\Sendable;
|
||||
|
||||
abstract class RestrictedPageController
|
||||
{
|
||||
protected ?Context $context = null;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$this->context = Context::init($url);
|
||||
}
|
||||
|
||||
protected function isCurrentPageAllowed(): bool
|
||||
{
|
||||
foreach ($this->context->navigation as $navigationEntry) {
|
||||
if ($navigationEntry->url === $this->context->currentPage) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public final function run(): Sendable
|
||||
{
|
||||
if (!$this->isCurrentPageAllowed()) {
|
||||
return new AccessDeniedPage($this->context);
|
||||
}
|
||||
return $this->runLogic();
|
||||
}
|
||||
|
||||
abstract protected function runLogic(): Sendable;
|
||||
}
|
14
webroot/lib/Controller/Sql.php
Normal file
14
webroot/lib/Controller/Sql.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
|
||||
class Sql
|
||||
{
|
||||
public static function connection(): \PDO
|
||||
{
|
||||
return new \PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASSWORD);
|
||||
}
|
||||
}
|
51
webroot/lib/Controller/Transaction.php
Normal file
51
webroot/lib/Controller/Transaction.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Context;
|
||||
use Model\User;
|
||||
|
||||
class Transaction
|
||||
{
|
||||
protected function __construct(
|
||||
protected Context $context,
|
||||
protected \PDO $sql,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function run(Context $context, callable $f): mixed
|
||||
{
|
||||
$sql = Sql::connection();
|
||||
$sql->query('START TRANSACTION');
|
||||
$result = $f(new self($context, $sql));
|
||||
$sql->query('COMMIT');
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function createBooking(User $user, int $type, int $amount, ?User $related, string $text): bool
|
||||
{
|
||||
$stmt = $this->sql->prepare('SELECT balance FROM user WHERE id = ?');
|
||||
$stmt->execute([$user->id]);
|
||||
$currentAmount = (int) $stmt->fetch(\PDO::FETCH_ASSOC)['balance'];
|
||||
$newAmount = (int) ($currentAmount + $amount);
|
||||
if ($newAmount < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// create booking entry
|
||||
$stmt = $this->sql->prepare(
|
||||
'INSERT INTO booking
|
||||
(affected, time, type, amount, related, comment)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$user->id, $this->context->requestTime, $type, $amount, $related?->id, $text]);
|
||||
|
||||
// change amount
|
||||
$stmt = $this->sql->prepare('UPDATE user SET balance = ? WHERE id = ?');
|
||||
$stmt->execute([$newAmount, $user->id]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
76
webroot/lib/Controller/TransferController.php
Normal file
76
webroot/lib/Controller/TransferController.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\User;
|
||||
use View\TransferPage;
|
||||
use View\MoneyFormatter;
|
||||
use View\Sendable;
|
||||
|
||||
class TransferController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/transfer.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$transferPage = new TransferPage($this->context);
|
||||
if ($transferPage->formWasSent) {
|
||||
$transferPage->fieldTarget = trim($transferPage->fieldTarget);
|
||||
$error = false;
|
||||
|
||||
// find target user
|
||||
$targetUser = User::byName($transferPage->fieldTarget);
|
||||
if (empty($targetUser)) {
|
||||
$transferPage->errorTargetNotFound = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check amount
|
||||
$amount = MoneyFormatter::parseAmount($transferPage->fieldAmount);
|
||||
if ($amount === null) {
|
||||
$transferPage->errorAmountInvalid = true;
|
||||
$error = true;
|
||||
} elseif ($amount === 0) {
|
||||
$transferPage->errorAmountZero = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check text
|
||||
$text = $transferPage->fieldText;
|
||||
if (iconv_strlen($text) > 100) {
|
||||
$transferPage->errorTextTooLong = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// create the actual bookings
|
||||
if (!$error) {
|
||||
$success = Transaction::run($this->context, function ($transaction) use ($targetUser, $amount, $text) {
|
||||
$success = $transaction->createBooking($this->context->session->user, 3, -$amount, $targetUser, $text);
|
||||
if ($success) {
|
||||
$transaction->createBooking($targetUser, 4, $amount, $this->context->session->user, $text);
|
||||
}
|
||||
return $success;
|
||||
});
|
||||
if (!$success) {
|
||||
$transferPage->errorInsufficientFunds = true;
|
||||
$error = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$transferPage->fieldTarget = '';
|
||||
$transferPage->fieldAmount = '';
|
||||
$transferPage->fieldText = '';
|
||||
|
||||
$transferPage->success = true;
|
||||
$transferPage->successTarget = $targetUser->name;
|
||||
$transferPage->successAmount = $amount;
|
||||
}
|
||||
}
|
||||
return $transferPage;
|
||||
}
|
||||
}
|
40
webroot/lib/Model/Context.php
Normal file
40
webroot/lib/Model/Context.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Model;
|
||||
|
||||
class Context
|
||||
{
|
||||
public ?int $requestTime = null;
|
||||
public ?Session $session = null;
|
||||
public ?string $currentPage = null;
|
||||
public ?array $navigation = null;
|
||||
|
||||
protected function initNavigation()
|
||||
{
|
||||
$this->navigation = [];
|
||||
$this->navigation[] = new NavigationEntry('Startseite', '/');
|
||||
if ($this->session === null) {
|
||||
$this->navigation[] = new NavigationEntry('Registrieren', '/register.php');
|
||||
$this->navigation[] = new NavigationEntry('Einloggen', '/login.php');
|
||||
} else {
|
||||
$this->navigation[] = new NavigationEntry('Umsatzübersicht', '/bookings.php');
|
||||
if ($this->session->user->isAdmin) {
|
||||
$this->navigation[] = new NavigationEntry('Einzahlen', '/deposit.php');
|
||||
$this->navigation[] = new NavigationEntry('Auszahlen', '/withdraw.php');
|
||||
}
|
||||
$this->navigation[] = new NavigationEntry('Überweisen', '/transfer.php');
|
||||
$this->navigation[] = new NavigationEntry('Ausloggen', '/logout.php');
|
||||
}
|
||||
}
|
||||
|
||||
public static function init(string $url): self
|
||||
{
|
||||
$context = new self();
|
||||
$context->requestTime = time();
|
||||
$context->currentPage = $url;
|
||||
$context->session = Session::load();
|
||||
$context->initNavigation();
|
||||
return $context;
|
||||
}
|
||||
}
|
24
webroot/lib/Model/NavigationEntry.php
Normal file
24
webroot/lib/Model/NavigationEntry.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Model;
|
||||
|
||||
class NavigationEntry
|
||||
{
|
||||
public function __construct(
|
||||
public string $displayText,
|
||||
public string $url,
|
||||
) {
|
||||
}
|
||||
|
||||
public function send(Context $context): void
|
||||
{
|
||||
$displayText = htmlspecialchars($this->displayText);
|
||||
$url = htmlspecialchars($this->url);
|
||||
if ($this->url === $context->currentPage) {
|
||||
echo "<li aria-current=\"page\"><span>{$displayText}</span></li>";
|
||||
} else {
|
||||
echo "<li><a href=\"{$url}\">{$displayText}</a></li>";
|
||||
}
|
||||
}
|
||||
}
|
59
webroot/lib/Model/Session.php
Normal file
59
webroot/lib/Model/Session.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Model;
|
||||
|
||||
use Controller\Sql;
|
||||
|
||||
class Session
|
||||
{
|
||||
public ?string $tokenHash = null;
|
||||
public ?string $newSessid = null;
|
||||
public ?User $user = null;
|
||||
|
||||
public static function create(User $user): Session
|
||||
{
|
||||
$sessid = bin2hex(random_bytes(32));
|
||||
$sessidHash = hash('sha256', $sessid);
|
||||
|
||||
$sql = Sql::connection();
|
||||
$stmt = $sql->prepare('INSERT INTO session (token, user) VALUES (UNHEX(?), ?)');
|
||||
$stmt->execute([$sessidHash, $user->id]);
|
||||
|
||||
$session = new self();
|
||||
$session->newSessid = $sessid;
|
||||
$session->user = $user;
|
||||
return $session;
|
||||
}
|
||||
|
||||
public static function load(): ?self
|
||||
{
|
||||
if (!isset($_COOKIE['sessid'])) {
|
||||
return null;
|
||||
}
|
||||
$sessidHash = hash('sha256', $_COOKIE['sessid']);
|
||||
|
||||
$sql = Sql::connection();
|
||||
$stmt = $sql->prepare(
|
||||
'SELECT user.id, user.name, user.admin FROM session
|
||||
JOIN user ON session.user = user.id
|
||||
WHERE token = UNHEX(?)'
|
||||
);
|
||||
$stmt->execute([$sessidHash]);
|
||||
if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$session = new Session();
|
||||
$session->tokenHash = $sessidHash;
|
||||
$session->user = new User($row['id'], $row['name'], null, (bool) $row['admin']);
|
||||
return $session;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function destroy(): void
|
||||
{
|
||||
$sql = Sql::connection();
|
||||
$stmt = $sql->prepare('DELETE FROM session WHERE token = UNHEX(?)');
|
||||
$stmt->execute([$this->tokenHash]);
|
||||
}
|
||||
}
|
46
webroot/lib/Model/User.php
Normal file
46
webroot/lib/Model/User.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Model;
|
||||
|
||||
use Controller\Sql;
|
||||
|
||||
class User
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public ?string $pwHash,
|
||||
public bool $isAdmin,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(string $name, string $pwHash): ?self
|
||||
{
|
||||
$sql = Sql::connection();
|
||||
$stmt = $sql->prepare('INSERT INTO user (name, password) VALUES (?, ?)');
|
||||
try {
|
||||
$stmt->execute([$name, $pwHash]);
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == 23000) {
|
||||
// duplicate entry, so the username is already in use
|
||||
return null;
|
||||
} else {
|
||||
// unknown error, throw out
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
return new self((int) $sql->lastInsertId(), $name, $pwHash, false);
|
||||
}
|
||||
|
||||
public static function byName(string $name): ?self
|
||||
{
|
||||
$sql = Sql::connection();
|
||||
$stmt = $sql->prepare('SELECT id, name, password, admin FROM user WHERE name = ?');
|
||||
$stmt->execute([$name]);
|
||||
if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
return new self($row['id'], $row['name'], $row['password'], (bool) $row['admin']);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
23
webroot/lib/View/AccessDeniedPage.php
Normal file
23
webroot/lib/View/AccessDeniedPage.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
class AccessDeniedPage extends BankingPage
|
||||
{
|
||||
public function sendHeader(): void
|
||||
{
|
||||
http_response_code(403);
|
||||
}
|
||||
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo 'Zugriff Verweigert';
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
echo '<h1>Zugriff Verweigert</h1>';
|
||||
echo '<p>Bitte wähle eine andere Seite in der Navigation aus.</p>';
|
||||
}
|
||||
}
|
32
webroot/lib/View/BankingPage.php
Normal file
32
webroot/lib/View/BankingPage.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
use Model\Context;
|
||||
|
||||
abstract class BankingPage extends Html
|
||||
{
|
||||
public function __construct(protected Context $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function sendHead(): void
|
||||
{
|
||||
parent::sendHead();
|
||||
echo '<link href="/style.css" rel="stylesheet">';
|
||||
}
|
||||
|
||||
public function sendBody(): void
|
||||
{
|
||||
echo '<nav><ul>';
|
||||
foreach ($this->context->navigation as $entry) {
|
||||
$entry->send($this->context);
|
||||
}
|
||||
echo '</ul></nav><main>';
|
||||
$this->sendMainContent();
|
||||
echo '</main>';
|
||||
}
|
||||
|
||||
abstract public function sendMainContent(): void;
|
||||
}
|
60
webroot/lib/View/BookingOverviewPage.php
Normal file
60
webroot/lib/View/BookingOverviewPage.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
class BookingOverviewPage extends BankingPage
|
||||
{
|
||||
public array $bookings = [];
|
||||
public int $finalBalance = 0;
|
||||
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo 'Umsatz-Übersicht';
|
||||
}
|
||||
|
||||
protected static function bookingText(int $type, ?string $relatedName)
|
||||
{
|
||||
switch ($type) {
|
||||
case 1:
|
||||
return "Einzahlung";
|
||||
case 2:
|
||||
return "Auszahlung";
|
||||
case 3:
|
||||
if ($relatedName === null) {
|
||||
return "Überweisung ins Leere";
|
||||
} else {
|
||||
return "Überweisung an {$relatedName}";
|
||||
}
|
||||
case 4:
|
||||
if ($relatedName === null) {
|
||||
return "Überweisung aus dem Nichts";
|
||||
} else {
|
||||
return "Überweisung von {$relatedName}";
|
||||
}
|
||||
default:
|
||||
return "Unbekannter Vorgang";
|
||||
}
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
$userName = htmlspecialchars($this->context->session->user->name);
|
||||
|
||||
echo "<h1>Umsatz-Übersicht für {$userName}</h1>";
|
||||
if (empty($this->bookings)) {
|
||||
echo '<p><i>Keine Buchungen</i></p>';
|
||||
}
|
||||
echo '<table>';
|
||||
foreach ($this->bookings as $booking) {
|
||||
$timeInfo = date('d.m.Y H:i', $booking['time']);
|
||||
$text = htmlspecialchars(static::bookingText($booking['type'], $booking['relatedName']));
|
||||
$comment = htmlspecialchars($booking['comment']);
|
||||
$amount = MoneyFormatter::formatAmount($booking['amount']);
|
||||
echo "<tr><td>{$timeInfo}</td><td>{$text}</td><td>{$comment}</td><td>{$amount}</td></tr>";
|
||||
}
|
||||
$final = MoneyFormatter::formatAmount($this->finalBalance);
|
||||
echo "<tr><td></td><td>Kontostand:</td><td></td><td>{$final}</td></tr>";
|
||||
echo '</table>';
|
||||
}
|
||||
}
|
106
webroot/lib/View/CashTransactionPage.php
Normal file
106
webroot/lib/View/CashTransactionPage.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
use Model\Context;
|
||||
|
||||
class CashTransactionPage extends BankingPage
|
||||
{
|
||||
protected string $title;
|
||||
|
||||
public string $fieldCustomer = '';
|
||||
public string $fieldAmount = '';
|
||||
public string $fieldText = '';
|
||||
public bool $formWasSent = false;
|
||||
|
||||
public bool $errorCustomerNotFound = false;
|
||||
public bool $errorAmountInvalid = false;
|
||||
public bool $errorAmountZero = false;
|
||||
public bool $errorInsufficientFunds = false;
|
||||
public bool $errorTextTooLong = false;
|
||||
|
||||
public bool $success = false;
|
||||
public ?string $successCustomer = null;
|
||||
public ?int $successAmount = null;
|
||||
|
||||
public function __construct(protected Context $context)
|
||||
{
|
||||
parent::__construct($context);
|
||||
switch ($context->currentPage) {
|
||||
case '/deposit.php':
|
||||
$this->title = 'Einzahlen';
|
||||
break;
|
||||
case '/withdraw.php':
|
||||
$this->title = 'Auszahlen';
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($_POST['customer'], $_POST['amount'], $_POST['text'])) {
|
||||
$this->formWasSent = true;
|
||||
$this->fieldCustomer = (string) $_POST['customer'];
|
||||
$this->fieldAmount = (string) $_POST['amount'];
|
||||
$this->fieldText = (string) $_POST['text'];
|
||||
}
|
||||
}
|
||||
|
||||
public function renderErrors(): string
|
||||
{
|
||||
$errors = [];
|
||||
if ($this->errorCustomerNotFound) {
|
||||
$errors[] = '[!] Der angegebene Kunde konnte nicht gefunden werden.';
|
||||
}
|
||||
if ($this->errorAmountInvalid) {
|
||||
$errors[] = '[!] Der eingegebene Betrag entspricht nicht dem vorgesehenen Format.';
|
||||
}
|
||||
if ($this->errorAmountZero) {
|
||||
$errors[] = '[!] Der Betrag muss größer als 0,00 € sein.';
|
||||
}
|
||||
if ($this->errorInsufficientFunds) {
|
||||
$errors[] = '[!] Das Konto des Kunden ist nicht ausreichend gedeckt.';
|
||||
}
|
||||
if ($this->errorTextTooLong) {
|
||||
$errors[] = '[!] Der Buchungstext darf nicht länger als 100 Zeichen sein.';
|
||||
}
|
||||
return implode('<br>', $errors);
|
||||
}
|
||||
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo $this->title;
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
$customer = htmlspecialchars($this->fieldCustomer);
|
||||
$amount = htmlspecialchars($this->fieldAmount);
|
||||
$text = htmlspecialchars($this->fieldText);
|
||||
|
||||
echo "<h1>{$this->title}</h1>";
|
||||
|
||||
$errors = $this->renderErrors();
|
||||
if (!empty($errors)) {
|
||||
echo "<p class=\"error\">{$errors}</p>";
|
||||
}
|
||||
|
||||
if ($this->success) {
|
||||
$successCustomer = htmlspecialchars($this->successCustomer);
|
||||
$successAmount = MoneyFormatter::formatAmount($this->successAmount);
|
||||
switch ($this->context->currentPage) {
|
||||
case '/deposit.php':
|
||||
echo "<p class=\"success\">Es wurden {$successAmount} auf das Konto von {$successCustomer} eingezahlt.</p>";
|
||||
break;
|
||||
case '/withdraw.php':
|
||||
echo "<p class=\"success\">Es wurden {$successAmount} aus dem Konto von {$successCustomer} ausgezahlt.</p>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
echo "<form class=\"cash-transaction\" action=\"{$this->context->currentPage}\" method=\"post\">";
|
||||
echo "<label for=\"customer\">Kundenname:</label><input type=\"text\" name=\"customer\" id=\"customer\" maxlength=\"20\" value=\"{$customer}\"><br>";
|
||||
echo "<label for=\"amount\">Betrag:</label><input type=\"text\" name=\"amount\" id=\"amount\" value=\"{$amount}\"><br>";
|
||||
echo "<label for=\"text\">Buchungstext:</label><input type=\"text\" name=\"text\" id=\"text\" value=\"{$text}\"><br>";
|
||||
echo "<input type=\"submit\" value=\"{$this->title}\">";
|
||||
echo '</form>';
|
||||
}
|
||||
}
|
28
webroot/lib/View/FrontPage.php
Normal file
28
webroot/lib/View/FrontPage.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
class FrontPage extends BankingPage
|
||||
{
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo 'Online-Banking';
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
echo '<h1>Online-Banking Instanz mit Sicherheitslücken</h1>';
|
||||
echo '<p>In dieser Applikation gibt es mindestens zwei Sicherheitslücken. Kannst du sie ausnutzen?</p>';
|
||||
echo '<p>Ablauf:</p><ol>';
|
||||
echo '<li>Registriere dich über das entsprechende Formular.</li>';
|
||||
echo '<li>Nenne mir deinen Nutzernamen.</li>';
|
||||
echo '<li>Du erhältst 1000,00 € Startkapital.</li>';
|
||||
echo '<li>Nutze mindestens eine Sicherheitslücke aus und erschleiche 1 Million € oder mehr.</li>';
|
||||
echo '</ol>';
|
||||
echo '<p class="pre">Die beiden beabsichtigten Lücken wurden jeweils in einem kurzen Text-Dokument beschrieben. Die SHA256-Hashwerte dieser Dokumente lauten:<br><br>';
|
||||
echo 'bb86334c2c4ecb4c3f35c35392a3867824216f0e23bee5c7b60953e41d7b7590 sicherheitsluecke-1.txt<br>';
|
||||
echo '75dde975461d9d20c81aa10a42ae886820bead9201bc032b16ce00dba873d89a sicherheitsluecke-2.txt<br><br>';
|
||||
echo 'Damit kann später zweifelsfrei überprüft werden, ob eine gefundene Sicherheitslücke tatsächlich beabsichtigt war oder ob es sich um eine weitere, unbeabsichtigte Lücke handelt.</p>';
|
||||
}
|
||||
}
|
30
webroot/lib/View/Html.php
Normal file
30
webroot/lib/View/Html.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
abstract class Html implements Sendable
|
||||
{
|
||||
public function send(): void
|
||||
{
|
||||
$this->sendHeader();
|
||||
echo '<!doctype html>';
|
||||
$this->sendHead();
|
||||
$this->sendBody();
|
||||
}
|
||||
|
||||
public function sendHeader(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function sendHead(): void
|
||||
{
|
||||
echo '<meta charset="UTF-8"><title>';
|
||||
$this->sendTitle();
|
||||
echo '</title>';
|
||||
}
|
||||
|
||||
abstract public function sendTitle(): void;
|
||||
|
||||
abstract public function sendBody(): void;
|
||||
}
|
44
webroot/lib/View/LoginPage.php
Normal file
44
webroot/lib/View/LoginPage.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
class LoginPage extends BankingPage
|
||||
{
|
||||
public string $fieldUsername = '';
|
||||
public string $fieldPassword = '';
|
||||
public bool $formWasSent = false;
|
||||
|
||||
public bool $errorLoginDataInvalid = false;
|
||||
|
||||
public function __construct(...$args)
|
||||
{
|
||||
parent::__construct(...$args);
|
||||
if (isset($_POST['username'], $_POST['password'])) {
|
||||
$this->formWasSent = true;
|
||||
$this->fieldUsername = (string) $_POST['username'];
|
||||
$this->fieldPassword = (string) $_POST['password'];
|
||||
}
|
||||
}
|
||||
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo 'Einloggen';
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
$username = htmlspecialchars($this->fieldUsername);
|
||||
$password = htmlspecialchars($this->fieldPassword);
|
||||
|
||||
echo '<h1>Einloggen</h1>';
|
||||
if ($this->errorLoginDataInvalid) {
|
||||
echo '<p class="error">[!] Der Login war nicht erfolgreich.</p>';
|
||||
}
|
||||
echo '<form class="login" action="/login.php" method="post">';
|
||||
echo "<label for=\"username\">Nutzername:</label><input type=\"text\" name=\"username\" id=\"username\" maxlength=\"20\" value=\"{$username}\"><br>";
|
||||
echo "<label for=\"password\">Passwort:</label><input type=\"password\" name=\"password\" id=\"password\" value=\"{$password}\"><br>";
|
||||
echo '<input type="submit" value="Einloggen">';
|
||||
echo '</form>';
|
||||
}
|
||||
}
|
21
webroot/lib/View/LoginRedirection.php
Normal file
21
webroot/lib/View/LoginRedirection.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
use Model\Context;
|
||||
|
||||
class LoginRedirection implements Sendable
|
||||
{
|
||||
public function __construct(protected Context $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function send(): void
|
||||
{
|
||||
$sessid = $this->context->session->newSessid;
|
||||
http_response_code(303); // "see other" redirection
|
||||
setcookie('sessid', $sessid, time() + 86400 * 365); // session creation
|
||||
header('Location: /bookings.php'); // login redirection
|
||||
}
|
||||
}
|
16
webroot/lib/View/LogoutRedirection.php
Normal file
16
webroot/lib/View/LogoutRedirection.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
use Model\Context;
|
||||
|
||||
class LogoutRedirection implements Sendable
|
||||
{
|
||||
public function send(): void
|
||||
{
|
||||
http_response_code(303); // "see other" redirection
|
||||
setcookie('sessid', '', 1); // delete cookie
|
||||
header('Location: /'); // redirection to home
|
||||
}
|
||||
}
|
30
webroot/lib/View/MoneyFormatter.php
Normal file
30
webroot/lib/View/MoneyFormatter.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
class MoneyFormatter
|
||||
{
|
||||
public static function formatAmount(int $amount): string
|
||||
{
|
||||
if ($amount < 0) {
|
||||
$inner = static::formatAmount(-$amount);
|
||||
return "<span class=\"negative\">- {$inner}</span>";
|
||||
}
|
||||
$euro = intdiv($amount, 100);
|
||||
$cent = sprintf("%02d", $amount % 100);
|
||||
return "{$euro},{$cent} €";
|
||||
}
|
||||
|
||||
public static function parseAmount(string $amount): ?int
|
||||
{
|
||||
$pattern = '/^([0-9]+)(,([0-9]{2}))?$/';
|
||||
if (preg_match($pattern, $amount, $matches)) {
|
||||
$euro = (int) $matches[1];
|
||||
$cent = (int) ($matches[3] ?? 0);
|
||||
$amount = 100 * $euro + $cent;
|
||||
return (int) $amount;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
77
webroot/lib/View/RegisterPage.php
Normal file
77
webroot/lib/View/RegisterPage.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
use Model\Context;
|
||||
|
||||
class RegisterPage extends BankingPage
|
||||
{
|
||||
public string $fieldUsername = '';
|
||||
public string $fieldPassword = '';
|
||||
public string $fieldRepeatPassword = '';
|
||||
public bool $formWasSent = false;
|
||||
|
||||
public bool $errorUsernameEmpty = false;
|
||||
public bool $errorUsernameTooLong = false;
|
||||
public bool $errorUsernameInUse = false;
|
||||
public bool $errorPasswordEmpty = false;
|
||||
public bool $errorPasswordsMismatch = false;
|
||||
|
||||
public function __construct(...$args)
|
||||
{
|
||||
parent::__construct(...$args);
|
||||
if (isset($_POST['username'], $_POST['password'], $_POST['repeat_password'])) {
|
||||
$this->formWasSent = true;
|
||||
$this->fieldUsername = (string) $_POST['username'];
|
||||
$this->fieldPassword = (string) $_POST['password'];
|
||||
$this->fieldRepeatPassword = (string) $_POST['repeat_password'];
|
||||
}
|
||||
}
|
||||
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo 'Registrieren';
|
||||
}
|
||||
|
||||
public function renderErrors(): string
|
||||
{
|
||||
$errors = [];
|
||||
if ($this->errorUsernameEmpty) {
|
||||
$errors[] = '[!] Bitte wähle einen Nutzernamen.';
|
||||
}
|
||||
if ($this->errorUsernameTooLong) {
|
||||
$errors[] = '[!] Der Nutzername darf nicht länger als 20 Zeichen sein.';
|
||||
}
|
||||
if ($this->errorUsernameInUse) {
|
||||
$errors[] = '[!] Der Nutzername wird bereits von einem Account verwendet.';
|
||||
}
|
||||
if ($this->errorPasswordEmpty) {
|
||||
$errors[] = '[!] Bitte wähle ein Passwort.';
|
||||
}
|
||||
if ($this->errorPasswordsMismatch) {
|
||||
$errors[] = '[!] Die beiden Passwörter stimmen nicht überein.';
|
||||
}
|
||||
return implode('<br>', $errors);
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
$username = htmlspecialchars($this->fieldUsername);
|
||||
$password = htmlspecialchars($this->fieldPassword);
|
||||
$repeatPassword = htmlspecialchars($this->fieldRepeatPassword);
|
||||
|
||||
echo '<h1>Registrieren</h1>';
|
||||
echo '<p>Erstelle dir hier einen neuen Banking-Account.</p>';
|
||||
$errors = $this->renderErrors();
|
||||
if (!empty($errors)) {
|
||||
echo "<p class=\"error\">{$errors}</p>";
|
||||
}
|
||||
echo '<form class="register" action="/register.php" method="post">';
|
||||
echo "<label for=\"username\">Nutzername:</label><input type=\"text\" name=\"username\" id=\"username\" maxlength=\"20\" value=\"{$username}\"><br>";
|
||||
echo "<label for=\"password\">Passwort:</label><input type=\"password\" name=\"password\" id=\"password\" value=\"{$password}\"><br>";
|
||||
echo "<label for=\"repeat_password\">Passwort wiederholen:</label><input type=\"password\" name=\"repeat_password\" id=\"repeat_password\" value=\"{$repeatPassword}\"><br>";
|
||||
echo '<input type="submit" value="Jetzt registrieren">';
|
||||
echo '</form>';
|
||||
}
|
||||
}
|
9
webroot/lib/View/Sendable.php
Normal file
9
webroot/lib/View/Sendable.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
interface Sendable
|
||||
{
|
||||
public function send(): void;
|
||||
}
|
89
webroot/lib/View/TransferPage.php
Normal file
89
webroot/lib/View/TransferPage.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace View;
|
||||
|
||||
use Model\Context;
|
||||
|
||||
class TransferPage extends BankingPage
|
||||
{
|
||||
public string $fieldTarget = '';
|
||||
public string $fieldAmount = '';
|
||||
public string $fieldText = '';
|
||||
public bool $formWasSent = false;
|
||||
|
||||
public bool $errorTargetNotFound = false;
|
||||
public bool $errorAmountInvalid = false;
|
||||
public bool $errorAmountZero = false;
|
||||
public bool $errorInsufficientFunds = false;
|
||||
public bool $errorTextTooLong = false;
|
||||
|
||||
public bool $success = false;
|
||||
public ?string $successTarget = null;
|
||||
public ?int $successAmount = null;
|
||||
|
||||
public function __construct(protected Context $context)
|
||||
{
|
||||
parent::__construct($context);
|
||||
|
||||
if (isset($_POST['target'], $_POST['amount'], $_POST['text'])) {
|
||||
$this->formWasSent = true;
|
||||
$this->fieldTarget = (string) $_POST['target'];
|
||||
$this->fieldAmount = (string) $_POST['amount'];
|
||||
$this->fieldText = (string) $_POST['text'];
|
||||
}
|
||||
}
|
||||
|
||||
public function renderErrors(): string
|
||||
{
|
||||
$errors = [];
|
||||
if ($this->errorTargetNotFound) {
|
||||
$errors[] = '[!] Der angegebene Nutzername (Zielkonto) konnte nicht gefunden werden.';
|
||||
}
|
||||
if ($this->errorAmountInvalid) {
|
||||
$errors[] = '[!] Der eingegebene Betrag entspricht nicht dem vorgesehenen Format.';
|
||||
}
|
||||
if ($this->errorAmountZero) {
|
||||
$errors[] = '[!] Der Betrag muss größer als 0,00 € sein.';
|
||||
}
|
||||
if ($this->errorInsufficientFunds) {
|
||||
$errors[] = '[!] Dein Konto ist nicht ausreichend gedeckt.';
|
||||
}
|
||||
if ($this->errorTextTooLong) {
|
||||
$errors[] = '[!] Der Buchungstext darf nicht länger als 100 Zeichen sein.';
|
||||
}
|
||||
return implode('<br>', $errors);
|
||||
}
|
||||
|
||||
public function sendTitle(): void
|
||||
{
|
||||
echo 'Überweisen';
|
||||
}
|
||||
|
||||
public function sendMainContent(): void
|
||||
{
|
||||
$target = htmlspecialchars($this->fieldTarget);
|
||||
$amount = htmlspecialchars($this->fieldAmount);
|
||||
$text = htmlspecialchars($this->fieldText);
|
||||
|
||||
echo "<h1>Überweisen</h1>";
|
||||
|
||||
$errors = $this->renderErrors();
|
||||
if (!empty($errors)) {
|
||||
echo "<p class=\"error\">{$errors}</p>";
|
||||
}
|
||||
|
||||
if ($this->success) {
|
||||
$successTarget = htmlspecialchars($this->successTarget);
|
||||
$successAmount = MoneyFormatter::formatAmount($this->successAmount);
|
||||
echo "<p class=\"success\">Es wurden {$successAmount} an {$successTarget} überwiesen.</p>";
|
||||
}
|
||||
|
||||
echo "<form class=\"transfer\" action=\"{$this->context->currentPage}\" method=\"post\">";
|
||||
echo "<label for=\"target\">Zielkonto (Nutzername):</label><input type=\"text\" name=\"target\" id=\"target\" maxlength=\"20\" value=\"{$target}\"><br>";
|
||||
echo "<label for=\"amount\">Betrag:</label><input type=\"text\" name=\"amount\" id=\"amount\" value=\"{$amount}\"><br>";
|
||||
echo "<label for=\"text\">Buchungstext:</label><input type=\"text\" name=\"text\" id=\"text\" value=\"{$text}\"><br>";
|
||||
echo "<input type=\"submit\" value=\"Überweisen\">";
|
||||
echo '</form>';
|
||||
}
|
||||
}
|
8
webroot/login.php
Normal file
8
webroot/login.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\LoginController;
|
||||
|
||||
(new LoginController())->run()->send();
|
8
webroot/logout.php
Normal file
8
webroot/logout.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\LogoutController;
|
||||
|
||||
(new LogoutController())->run()->send();
|
8
webroot/register.php
Normal file
8
webroot/register.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\RegisterController;
|
||||
|
||||
(new RegisterController())->run()->send();
|
92
webroot/style.css
Normal file
92
webroot/style.css
Normal file
@ -0,0 +1,92 @@
|
||||
html, body {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
main {
|
||||
margin: 2rem;
|
||||
}
|
||||
nav ul {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
nav li {
|
||||
flex: 1 1 0;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
nav a, nav span {
|
||||
border-right: 2px solid #aaa;
|
||||
border-bottom: 2px solid #aaa;
|
||||
display: block;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav a {
|
||||
color: #000;
|
||||
}
|
||||
nav a:hover, nav li[aria-current="page"] {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
nav li:last-child, nav li:last-child a {
|
||||
border-right: none;
|
||||
}
|
||||
input {
|
||||
background-color: #fff;
|
||||
border: 1px solid #000;
|
||||
box-sizing: border-box;
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0;
|
||||
padding: 0.5rem;
|
||||
width: 30rem;
|
||||
}
|
||||
.register input[type="submit"], .transfer input[type="submit"] {
|
||||
width: 50rem;
|
||||
}
|
||||
.login input[type="submit"], .cash-transaction input[type="submit"] {
|
||||
width: 45rem;
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
.register label, .transfer label {
|
||||
width: 20rem;
|
||||
}
|
||||
.login label, .cash-transaction label {
|
||||
width: 15rem;
|
||||
}
|
||||
p.error {
|
||||
color: #a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
p.success {
|
||||
color: #080;
|
||||
font-weight: bold;
|
||||
}
|
||||
.negative {
|
||||
color: #800;
|
||||
}
|
||||
table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
td {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
tr:nth-child(2n) {
|
||||
background-color: #eee;
|
||||
}
|
||||
tr:last-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
.pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
8
webroot/transfer.php
Normal file
8
webroot/transfer.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\TransferController;
|
||||
|
||||
(new TransferController())->run()->send();
|
8
webroot/withdraw.php
Normal file
8
webroot/withdraw.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/autoload.php';
|
||||
|
||||
use Controller\CashTransactionController;
|
||||
|
||||
(new CashTransactionController('/withdraw.php'))->run()->send();
|
Loading…
x
Reference in New Issue
Block a user