Final
This commit is contained in:
commit
21bb9e3914
10 changed files with 359 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
tests
|
3
.htaccess
Normal file
3
.htaccess
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule . index.php
|
||||||
|
ErrorDocument 404 /error/not_found.php
|
57
api/APIHandler.php
Normal file
57
api/APIHandler.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class APIHandler {
|
||||||
|
public static function handle_request($url_parts) {
|
||||||
|
if (count($url_parts) < 2) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($url_parts[1]) {
|
||||||
|
case "tenis":
|
||||||
|
return APIHandler::handle_tenis_request(array_slice($url_parts, 1));
|
||||||
|
default:
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function handle_tenis_request($url_parts) {
|
||||||
|
if (count($url_parts) < 2) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = new PDO("sqlite:tenis.db");
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($url_parts[1]) {
|
||||||
|
case "reservation":
|
||||||
|
$reservation = new Reservation($pdo);
|
||||||
|
echo $reservation->process($_SERVER["REQUEST_METHOD"]);
|
||||||
|
break;
|
||||||
|
case "court":
|
||||||
|
$court = new Court($pdo);
|
||||||
|
echo $court->process($_SERVER["REQUEST_METHOD"]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidArgumentException $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode([
|
||||||
|
"message" => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
catch (Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
"message" => $e->getMessage(),
|
||||||
|
"code" => $e->getCode(),
|
||||||
|
"line" => $e->getLine()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
api/Court.php
Normal file
24
api/Court.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Court {
|
||||||
|
private PDO $pdo;
|
||||||
|
|
||||||
|
public function __construct(PDO $pdo) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(string $request) {
|
||||||
|
switch ($request) {
|
||||||
|
case "GET":
|
||||||
|
return $this->get_all();
|
||||||
|
default:
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_all() {
|
||||||
|
$query = $this->pdo->query("SELECT * FROM courts");
|
||||||
|
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
return json_encode($rows);
|
||||||
|
}
|
||||||
|
}
|
22
api/Pricing.php
Normal file
22
api/Pricing.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Pricing {
|
||||||
|
// should rather be in db
|
||||||
|
private static int $TENIS_PRICE = 500;
|
||||||
|
private static $TENIS_GAME = array(1, 1.5);
|
||||||
|
private static $SURFACE_PRICING = array(1, 1.5, 2);
|
||||||
|
|
||||||
|
public static function for_tenis(int $duration, int $game, int $surface) {
|
||||||
|
return intdiv($duration, 3600) * Pricing::$TENIS_PRICE
|
||||||
|
* Pricing::$TENIS_GAME[$game]
|
||||||
|
* Pricing::$SURFACE_PRICING[$surface];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_games_count() {
|
||||||
|
return count(Pricing::$TENIS_GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_surfaces_count() {
|
||||||
|
return count(Pricing::$SURFACE_PRICING);
|
||||||
|
}
|
||||||
|
}
|
214
api/Reservation.php
Normal file
214
api/Reservation.php
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Reservation {
|
||||||
|
private PDO $pdo;
|
||||||
|
private int $MAX_DURATION = 36000;
|
||||||
|
private int $MIN_DURATION = 3600;
|
||||||
|
|
||||||
|
public function __construct(PDO $pdo) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(string $request) {
|
||||||
|
// null will become empty array
|
||||||
|
$data = (array) json_decode(file_get_contents("php://input"), true);
|
||||||
|
switch ($request) {
|
||||||
|
case "GET":
|
||||||
|
return $this->get_data($data);
|
||||||
|
case "POST":
|
||||||
|
return $this->create_reservation($data);
|
||||||
|
case "DELETE":
|
||||||
|
$this->delete($data);
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_data($data) {
|
||||||
|
$stmt = null;
|
||||||
|
if (empty($data)) {
|
||||||
|
$stmt = $this->get_all();
|
||||||
|
}
|
||||||
|
elseif (isset($data["court"])) {
|
||||||
|
$stmt = $this->get_by_court($data["court"]);
|
||||||
|
}
|
||||||
|
elseif (isset($data["phone"])) {
|
||||||
|
$stmt = $this->get_by_phone($data["phone"]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new InvalidArgumentException("Expected court id or phone number got none");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
return json_encode($result ? $result : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_all() {
|
||||||
|
return $this->pdo->prepare("SELECT * FROM reservations");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_by_court(int $id) {
|
||||||
|
if (!is_int($id)) {
|
||||||
|
throw new InvalidArgumentException("id not an integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "SELECT * FROM reservations WHERE court_id=:id";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
$stmt->bindValue(":id", $id, PDO::PARAM_INT);
|
||||||
|
return $stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_by_phone(string $phone_number) {
|
||||||
|
if (!Security::is_phone_number($phone_number)) {
|
||||||
|
throw new InvalidArgumentException("phone number not in valid format");
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "SELECT * FROM reservations WHERE phone_number=:phone";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
$stmt->bindValue(":phone", $phone_number, PDO::PARAM_STR);
|
||||||
|
return $stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function create_reservation($data) {
|
||||||
|
$this->verify_create_data($data);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("SELECT surface FROM courts WHERE id=:id");
|
||||||
|
$stmt->bindValue(":id", $data["court_id"], PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
$surface = $stmt->fetchAll()[0][0];
|
||||||
|
$price = Pricing::for_tenis((int)$data["end"] - (int)$data["start"], $data["game"], $surface);
|
||||||
|
|
||||||
|
$password = Security::generate_password(6);
|
||||||
|
$password_hash = hash("sha256", $password);
|
||||||
|
|
||||||
|
$phone = preg_replace('/\s*/', '', $data["phone"]);
|
||||||
|
$phone_hash = hash("sha256", $phone);
|
||||||
|
|
||||||
|
$query = "INSERT INTO reservations VALUES (NULL, :court_id, :phone, :password, :game, :start, :end)";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
|
||||||
|
$stmt->bindValue(":court_id", $data["court_id"], PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":phone", $phone_hash, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(":password", $password_hash, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(":game", $data["game"], PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":start", $data["start"], PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":end", $data["end"], PDO::PARAM_INT);
|
||||||
|
|
||||||
|
$result = $stmt->execute();
|
||||||
|
if ($result) {
|
||||||
|
http_response_code(201);
|
||||||
|
echo json_encode([
|
||||||
|
"price" => $price,
|
||||||
|
"password" => $password
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(500);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function verify_create_data($data) {
|
||||||
|
if (!isset($data["court_id"], $data["phone"], $data["game"], $data["start"], $data["end"])) {
|
||||||
|
throw new InvalidArgumentException("some arguments are missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_int($data["court_id"])) {
|
||||||
|
throw new InvalidArgumentException("court id not in correct format");
|
||||||
|
}
|
||||||
|
|
||||||
|
$court_id = (int)$data["court_id"];
|
||||||
|
$court_count = $this->pdo->query("SELECT COUNT(*) FROM courts")->fetchAll()[0][0];
|
||||||
|
|
||||||
|
if ($court_id < 1 || $court_id > $court_count) {
|
||||||
|
throw new InvalidArgumentException("court id is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Security::is_phone_number($data["phone"])) {
|
||||||
|
throw new InvalidArgumentException("phone number not in correct format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_int($data["game"]) || $data["game"] < 0 || $data["game"] >= Pricing::get_games_count()) {
|
||||||
|
throw new InvalidArgumentException("game type is not in valid format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_int($data["start"]) || !is_int($data["end"]))
|
||||||
|
{
|
||||||
|
throw new InvalidArgumentException("start or end is not in valid format");
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = (int)$data["start"];
|
||||||
|
$end = (int)$data["end"];
|
||||||
|
|
||||||
|
$this->verify_interval($start, $end, $data["court_id"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function verify_interval(int $start, int $end, int $id) {
|
||||||
|
if ($start > $end || $end - $start > $this->MAX_DURATION || $end - $start < $this->MIN_DURATION) {
|
||||||
|
throw new InvalidArgumentException("reservation interval not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($start <= time()) {
|
||||||
|
throw new InvalidArgumentException("can't create reservation in the past");
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = 'SELECT * FROM reservations WHERE court_id=:court_id AND start>=:start AND end<=:end';
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
|
||||||
|
$stmt->bindValue(":start", $start, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":end", $end, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":court_id", $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if ($result) {
|
||||||
|
throw new InvalidArgumentException("reservation in this interval already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function delete($data) {
|
||||||
|
if (!isset($data["id"], $data["password"], $data["phone"])) {
|
||||||
|
throw new InvalidArgumentException("invalid arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_int($data["id"])) {
|
||||||
|
throw new InvalidArgumentException("id not in correct format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Security::is_phone_number($data["phone"])) {
|
||||||
|
throw new InvalidArgumentException("phone number not in correct format");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$phone = preg_replace('/\s*/', '', $data["phone"]);
|
||||||
|
$phone_hash = hash("sha256", $phone);
|
||||||
|
$password_hash = hash("sha256", $data["password"]);
|
||||||
|
|
||||||
|
$query = "SELECT COUNT(*) FROM reservations WHERE id=:id AND phone_number=:phone AND password=:password";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
|
||||||
|
$stmt->bindValue(":id", $data["id"], PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":password", $password_hash, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(":phone", $phone_hash, PDO::PARAM_STR);
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$count = $stmt->fetchAll()[0][0];
|
||||||
|
if ($count == 0) {
|
||||||
|
throw new InvalidArgumentException("invalid entry (does not exist or not correct information)");
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "DELETE FROM reservations WHERE id=:id AND phone_number=:phone AND password=:password";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
|
||||||
|
$stmt->bindValue(":id", $data["id"], PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(":password", $password_hash, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(":phone", $phone_hash, PDO::PARAM_STR);
|
||||||
|
|
||||||
|
$result = $stmt->execute();
|
||||||
|
if (!$result) {
|
||||||
|
throw new InvalidArgumentException("something went wrong");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
api/Security.php
Normal file
21
api/Security.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Security {
|
||||||
|
public static function generate_password(int $len) : string {
|
||||||
|
$password = "";
|
||||||
|
for ($i = 0; $i < $len; $i++) {
|
||||||
|
if (random_int(0, 1)) {
|
||||||
|
$password .= chr(random_int(65, 90));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$password .= random_int(0, 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function is_phone_number(string $phone) : ?string {
|
||||||
|
return !(is_null($phone) || preg_match("/\+[0-9\s]*$/", trim($phone)) == 0);
|
||||||
|
}
|
||||||
|
}
|
0
index.html
Normal file
0
index.html
Normal file
17
index.php
Normal file
17
index.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
spl_autoload_register(function ($class) {
|
||||||
|
include_once __DIR__ . "/api/$class.php";
|
||||||
|
});
|
||||||
|
|
||||||
|
header("Content-type: application/json; charset=UTF-8");
|
||||||
|
|
||||||
|
$url_parts = explode("/", $_SERVER["REQUEST_URI"]);
|
||||||
|
|
||||||
|
switch ($url_parts[2]) {
|
||||||
|
case "api":
|
||||||
|
APIHandler::handle_request(array_slice($url_parts, 2));
|
||||||
|
exit;
|
||||||
|
default:
|
||||||
|
http_response_code(404);
|
||||||
|
}
|
BIN
tenis.db
Normal file
BIN
tenis.db
Normal file
Binary file not shown.
Loading…
Reference in a new issue