diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d1502b08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/.htaccess b/.htaccess new file mode 100644 index 00000000..8330727e --- /dev/null +++ b/.htaccess @@ -0,0 +1,7 @@ + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule (.*)$ index.php?url=$1 [QSA,NC,L] + # RewriteRule api/(.*)$ api/api.php?url=$1 [QSA,NC,L] + diff --git a/_bootstrap.php b/_bootstrap.php new file mode 100644 index 00000000..4fffbac1 --- /dev/null +++ b/_bootstrap.php @@ -0,0 +1,33 @@ +isDot()) + continue; + + if($item->isDir()) + recursive_autoloader($class, $item->getPathname() . DIRECTORY_SEPARATOR); + } + + $class_file = $path . "{$class}.class.php"; + + if(file_exists($class_file)) { + require_once $class_file; + } +} +spl_autoload_register('recursive_autoloader'); diff --git a/class/DB.class.php b/class/DB.class.php new file mode 100644 index 00000000..a63a516f --- /dev/null +++ b/class/DB.class.php @@ -0,0 +1,142 @@ +setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + self::$Conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + return self::$Conn; + } + + public static function getAllFrom($table, array $fields) + { + $fields = implode(', ', $fields); + + $sql = "SELECT {$fields} FROM {$table}"; + + $rs = self::getInstance()->query($sql); + + $output = array(); + if($rs) { + $output = $rs->fetchAll(); + } + + return $output; + } + + public static function getOneByIdFrom($table, array $fields, $id) + { + $fields = implode(', ', $fields); + + $sql = "SELECT {$fields} FROM {$table} WHERE id = {$id}"; + + $rs = self::getInstance()->query($sql); + $item = $rs->fetch(); + + if(!$item) + return array(); + + return $item; + } + + public static function getOneByField($table, array $fields) + { + $clauses = array(); + $values = array(); + foreach($fields as $field => $value) { + $clauses[] = "{$field} = ?"; + $values[] = $value; + } + $clauses = implode(' AND ', $clauses); + + $sql = "SELECT * FROM {$table} WHERE {$clauses}"; + $st = self::getInstance()->prepare($sql); + $st->execute($values); + + if(!$st) + return false; + + return $st->fetch(); + } + + public static function saveAt($table, array $data) + { + if(!$data['id']) + return self::insert($table, $data); + + return self::update($table, $data); + } + + public function removeFrom($table, array $data) + { + $sql = "DELETE FROM {$table} WHERE id = ?"; + + $st = self::getInstance()->prepare($sql); + $st->execute(array($data['id'])); + + return $data; + } + + private function insert($table, array $data) + { + $fields = array(); + $values = array(); + + foreach($data as $field => $value) { + if(in_array($field, array('id'))) + continue; + + $fields[] = $field; + $values[] = $value; + + $placeholders[] = '?'; + } + + $fields = implode(', ', $fields); + $placeholders = implode(', ', $placeholders); + + $sql = "INSERT INTO {$table} ({$fields}) VALUES ({$placeholders})"; + $st = self::getInstance()->prepare($sql); + $st->execute($values); + + $data['id'] = self::getInstance()->lastInsertId(); + + return $data; + } + + private function update($table, array $data) + { + $clauses = array(); + $values = array(); + + foreach($data as $field => $value) { + if(in_array($field, array('id'))) + continue; + + $clauses[] = "{$field} = ?"; + $values[] = $value; + } + + $clauses = implode(', ', $clauses); + + $sql = "UPDATE {$table} SET {$clauses} WHERE id = {$data['id']}"; + $st = self::getInstance()->prepare($sql); + $st->execute($values); + + return $data; + } + + public function saveAuthTokenFor($table, $id, $auth_token) + { + $sql = "UPDATE {$table} SET auth_token = ? WHERE id = ?"; + $st = self::getInstance()->prepare($sql); + $st->execute(array($auth_token, $id)); + } +} diff --git a/class/Model.class.php b/class/Model.class.php new file mode 100644 index 00000000..f538f5d5 --- /dev/null +++ b/class/Model.class.php @@ -0,0 +1,98 @@ +getVerb()) { + case 'GET': + if($Request->getId()) { + if(!is_numeric($Request->getId())) { + $Request->sendResponse(400); + } + + $resource = $this->getById($Request->getId()); + + if(!$resource) + $Request->sendResponse(404); + + $Request->sendResponse(200, $resource); + } else { + $resources = $this->getAll(); + + $Request->sendResponse(200, $resources); + } + break; + + case 'POST': + if($Request->getId()) { + $Request->sendResponse(400); + } + + $this->createFrom($Request); + break; + + case 'PATCH': + if(!$Request->getId() || !is_numeric($Request->getId())) { + $Request->sendResponse(400); + } + + $this->updateFrom($Request); + break; + + case 'DELETE': + if(!$Request->getId() || !is_numeric($Request->getId())) { + $Request->sendResponse(400); + } + + $this->deleteFrom($Request); + break; + + default: + $Request->sendResponse(404); + } + } + + protected function getAll() + { + $rs = DB::getAllFrom(static::getTable(), static::getFields()); + + if(!$rs) + return array(); + + return $rs; + } + + protected function getById($id) + { + return DB::getOneByIdFrom(static::getTable(), static::getFields(), $id); + } + + protected function generateAuthToken($id, $name, $email) + { + // Token data + $token_data = array( + 'id' => $id, + 'name' => $name, + 'email' => $email + ); + $auth_token = JWT::encode($token_data, API_JWT_SECRET); + DB::saveAuthTokenFor(Users::getTable(), $id, $auth_token); + + return $auth_token; + } + + // GETTERS + public static function getTable() + { + return static::$table; + } + + public static function getFields() + { + return static::$fields; + } +} diff --git a/class/Request.class.php b/class/Request.class.php new file mode 100644 index 00000000..6bb58edc --- /dev/null +++ b/class/Request.class.php @@ -0,0 +1,110 @@ +verb = $_SERVER['REQUEST_METHOD']; + } + + public function handle() + { + + // Receives only JSON content + if(array_key_exists('CONTENT_TYPE', $_SERVER) && $_SERVER['CONTENT_TYPE'] !== 'application/json') { + $this->sendResponse(400); + } + + $this->resolveUrl(); + $this->validateAuthToken(); + + if(!in_array($this->getEndpoint(), self::$valid_endpoints)) + $this->sendResponse(400); + + // Create object and manipulate response + $class = ucwords($this->endpoint); + new $class($this); + } + + public function sendResponse($status_code, $response_body=null, $additional_headers=array()) + { + header("HTTP/1.0 {$status_code}", true, $status_code); + foreach ($additional_headers as $header) { + header($header); + } + + if($response_body) + echo json_encode($response_body); + + die; + } + + private function resolveUrl() + { + if(!array_key_exists('url', $_GET)) { + $this->sendResponse(404); + } + + $pieces = explode('/', $_GET['url']); + + if(count($pieces) > 2) { + $this->sendResponse(400); + } + + $this->endpoint = $pieces[0]; + if(array_key_exists(1, $pieces)) { + $this->id = $pieces[1]; + } + } + + private function validateAuthToken() + { + // Set bypass authentication + if(($this->getEndpoint() === 'users' && !$this->getId()) || $this->getEndpoint() === 'login') { + return; + } + + if(!array_key_exists('HTTP_X_AUTH', $_SERVER)) + $this->sendResponse(401); + + $auth_token = $_SERVER['HTTP_X_AUTH']; + try{ + $token_data = JWT::decode($auth_token, API_JWT_SECRET, array('HS256')); + } catch(Exception $e) { + $this->sendResponse(401); + } + + $fields = array( + 'name' => $token_data->name, + 'email' => $token_data->email, + 'auth_token' => $auth_token, + ); + $resource = DB::getOneByField(Users::getTable(), $fields); + + if(!$resource) + $this->sendResponse(401); + } + + + // GETTERS + public function getEndpoint() + { + return $this->endpoint; + } + + public function getId() + { + return $this->id; + } + + public function getVerb() + { + return $this->verb; + } +} diff --git a/class/model/Resources.class.php b/class/model/Resources.class.php new file mode 100644 index 00000000..ad3392bc --- /dev/null +++ b/class/model/Resources.class.php @@ -0,0 +1,196 @@ + $this->getId(), + 'name' => $this->getName(), + 'age'=> $this->getAge(), + 'email' => $this->getEmail(), + 'department' => $this->getDepartment(), + 'salary' => $this->getSalary(), + 'created_at' => $this->getCreatedAt(), + 'updated_at' => $this->getUpdatedAt() + ); + } + + protected function createFrom($Request) + { + $input_data = json_decode(file_get_contents('php://input'), true); + + $required_keys = array('name', 'age', 'email', 'department', 'salary'); + if(!$input_data || array_keys($input_data) !== $required_keys) { + $Request->sendResponse(400); + } + + $this->setName($input_data['name']); + $this->setAge($input_data['age']); + $this->setEmail($input_data['email']); + $this->setDepartment($input_data['department']); + $this->setSalary($input_data['salary']); + + $now = date('Y-m-d H:i:s'); + $this->setCreatedAt($now); + $this->setUpdatedAt($now); + + return $this->save(); + } + + protected function updateFrom($Request) + { + $input_data = json_decode(file_get_contents('php://input'), true); + + $valid_keys = array('name', 'age', 'email', 'department', 'salary'); + if(array_diff(array_keys($input_data), $valid_keys)) { + $Request->sendResponse(400); + } + + $resource = $this->getById($Request->getId()); + if(!$resource) { + $Request->sendResponse(404); + } + + $this->setId($resource['id']); + + $this->setName($resource['name']); + if(array_key_exists('name', $input_data)) + $this->setName($input_data['name']); + + $this->setAge($resource['age']); + if(array_key_exists('age', $input_data)) + $this->setAge($input_data['age']); + + $this->setEmail($resource['email']); + if(array_key_exists('email', $input_data)) + $this->setEmail($input_data['email']); + + $this->setDepartment($resource['department']); + if(array_key_exists('department', $input_data)) + $this->setDepartment($input_data['department']); + + $this->setSalary($resource['salary']); + if(array_key_exists('salary', $input_data)) + $this->setSalary($input_data['salary']); + + $this->setCreatedAt($resource['created_at']); + $this->setUpdatedAt(date('Y-m-d H:i:s')); + + return $this->save(); + } + + protected function deleteFrom($Request) + { + $resource = $this->getById($Request->getId()); + if(!$resource) { + $Request->sendResponse(404); + } + + return DB::removeFrom(self::getTable(), $resource); + } + + protected function save() + { + return DB::saveAt(self::getTable(), $this->toArray()); + } + + // SETTERS + public function setId($id) + { + $this->id = $id; + } + + public function setName($name) + { + $this->name = $name; + } + + public function setAge($age) + { + $this->age = $age; + } + + public function setEmail($email) + { + $this->email = $email; + } + + public function setDepartment($department) + { + $this->department = $department; + } + + public function setSalary($salary) + { + $this->salary = $salary; + } + + public function setCreatedAt($created_at) + { + $this->created_at = $created_at; + } + + public function setUpdatedAt($updated_at) + { + $this->updated_at = $updated_at; + } + + // GETTERS + public static function getTable() + { + return self::$table; + } + + public function getId() + { + return $this->id; + } + + public function getName() + { + return $this->name; + } + + public function getAge() + { + return $this->age; + } + + public function getEmail() + { + return $this->email; + } + + public function getDepartment() + { + return $this->department; + } + + public function getSalary() + { + return $this->salary; + } + + public function getCreatedAt() + { + return $this->created_at; + } + + public function getUpdatedAt() + { + return $this->updated_at; + } +} diff --git a/class/model/Users.class.php b/class/model/Users.class.php new file mode 100644 index 00000000..dd49324b --- /dev/null +++ b/class/model/Users.class.php @@ -0,0 +1,162 @@ + $this->getId(), + 'name' => $this->getName(), + 'email' => $this->getEmail(), + 'password' => $this->getPassword(), + 'auth_token' => $this->getAuthToken() + ); + } + + protected function createFrom($Request) + { + $input_data = json_decode(file_get_contents('php://input'), true); + + // Validate + $required_keys = array('name', 'email', 'password'); + if(!$input_data || array_keys($input_data) !== $required_keys) { + $Request->sendResponse(400); + } + if(!filter_var($input_data['email'], FILTER_VALIDATE_EMAIL)) { + echo "INVALID EMAIL"; + $Request->sendResponse(400); + } + if(DB::getOneByField(self::getTable(), array('email' => $input_data['email']))) { + $Request->sendResponse(409); + } + if(strlen($input_data['password']) < 6) { + $Request->sendResponse(400); + } + + $this->setName($input_data['name']); + $this->setEmail($input_data['email']); + $this->setPassword($input_data['password']); + + $resource = $this->save(); + + $auth_token = $this->generateAuthToken($resource['id'], $resource['name'], $resource['email']); + + $Request->sendResponse(200, $resource, array("x-auth: {$auth_token}")); + } + + protected function updateFrom($Request) + { + $input_data = json_decode(file_get_contents('php://input'), true); + + $valid_keys = array('name', 'email'); + if(array_diff(array_keys($input_data), $valid_keys)) { + $Request->sendResponse(400); + } + + $resource = DB::getOneByIdFrom(static::getTable(), array('*'), $Request->getId()); + if(!$resource) { + $Request->sendResponse(404); + } + + $this->setId($resource['id']); + + $this->setName($resource['name']); + if(array_key_exists('name', $input_data)) + $this->setName($input_data['name']); + + $this->setEmail($resource['email']); + if(array_key_exists('email', $input_data)) + $this->setEmail($input_data['email']); + + $this->setPassword($resource['password']); + $this->setAuthToken($resource['auth_token']); + + $resource = $this->save(); + + $Request->sendResponse(200, $resource); + } + + protected function deleteFrom($Request) + { + $resource = $this->getById($Request->getId()); + if(!$resource) { + $Request->sendResponse(404); + } + + $resource = DB::removeFrom(self::getTable(), $resource); + + $Request->sendResponse(200, $resource); + } + + protected function save() + { + $item = DB::saveAt(self::getTable(), $this->toArray()); + + foreach(array_diff(array_keys($item), self::getFields()) as $skip) { + unset($item[$skip]); + } + + return $item; + } + + // SETTERS + public function setId($id) + { + $this->id = $id; + } + + public function setName($name) + { + $this->name = $name; + } + + public function setEmail($email) + { + $this->email = $email; + } + + public function setPassword($password) + { + $this->password = sha1($password); + } + + public function setAuthToken($auth_token) + { + $this->auth_token = $auth_token; + } + + // GETTERS + public function getId() + { + return $this->id; + } + + public function getName() + { + return $this->name; + } + + public function getEmail() + { + return $this->email; + } + + public function getPassword() + { + return $this->password; + } + + public function getAuthToken() + { + return $this->auth_token; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..f9edafbc --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "firebase/php-jwt": "^2.2.0" + } +} diff --git a/db.sq3 b/db.sq3 new file mode 100644 index 00000000..0f23eec2 Binary files /dev/null and b/db.sq3 differ diff --git a/index.php b/index.php new file mode 100644 index 00000000..8d96ad01 --- /dev/null +++ b/index.php @@ -0,0 +1,11 @@ +handle(); +} catch(Exception $e) { + $req->sendResponse(500); +}