├── .gitignore ├── .htaccess ├── .phalcon └── .gitkeep ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api-phalcon-micro.postman_collection.json ├── app.php ├── composer.json ├── config ├── acl.php ├── config.php ├── loader.php ├── server.development.php ├── server.production.php ├── server.staging.php └── services.php ├── controllers ├── CitiesController.php ├── ControllerBase.php ├── IndexController.php ├── ProfileController.php └── UsersController.php ├── deploy-production.sh ├── deploy-staging.sh ├── exclude-production.txt ├── exclude-staging.txt ├── index.html ├── library └── .gitkeep ├── middlewares └── AccessMiddleware.php ├── migrations └── .gitkeep ├── models ├── Cities.php ├── Logs.php ├── Users.php └── UsersAccess.php ├── phpunit.xml ├── public ├── .htaccess └── index.php ├── schemas ├── myproject.sql └── myproject_log.sql ├── tests ├── 0IndexTest.php ├── 1CitiesTest.php ├── 2UsersTest.php └── 3ProfileTest.php └── views ├── 404.phtml └── index.phtml /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Options -Indexes 3 | Options +FollowSymlinks 4 | RewriteEngine On 5 | 6 | #************************************************************* 7 | #* To force SECURE (https) server: remove the "#" symbol * 8 | #* from the following 2 lines * 9 | #************************************************************* 10 | 11 | #RewriteCond %{HTTPS} off 12 | #RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] 13 | 14 | RewriteRule ^$ public/ [L] 15 | RewriteRule (.*) public/$1 [L] 16 | 17 | 18 | # Compress output of JSON files # 19 | 20 | SetOutputFilter DEFLATE 21 | 22 | -------------------------------------------------------------------------------- /.phalcon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davellanedam/phalcon-micro-rest-api-skeleton/3c1a9bb4264d3438920e8779dbc7884bf5283a4d/.phalcon/.gitkeep -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.5 (Feb 4, 2019) 2 | 3 | - Refactor tests 4 | 5 | ## 1.0.4 (Feb 4, 2019) 6 | 7 | - Refactor tests 8 | 9 | ## 1.0.3 (Feb 4, 2019) 10 | 11 | - Changelog updated 12 | 13 | ## 1.0.2 (Feb 4, 2019) 14 | 15 | - Refactor tests 16 | 17 | ## 1.0.1 (Feb 2, 2019) 18 | 19 | - Postman collection updated 20 | 21 | ## 1.0.0 (Feb 2, 2019) 22 | 23 | - Big refactor 24 | - PHPUnit testing added 25 | 26 | ## 0.4.2 (Dec 12, 2018) 27 | 28 | - ACL fix 29 | 30 | ## 0.4.1 (Nov 20, 2017) 31 | 32 | - Formatting code 33 | 34 | ## 0.4.0 (Oct 22, 2017) 35 | 36 | - Make HTTPS redirect host and port agnostic 37 | 38 | ## 0.3.0 (Sep 12, 2017) 39 | 40 | - JWT service added 41 | 42 | ## 0.2.3 (Sep 12, 2017) 43 | 44 | - Fix index controller getting now datetime 45 | 46 | ## 0.2.2 (Sep 7, 2017) 47 | 48 | - Token encryption moved to ControllerBase function 49 | 50 | ## 0.2.1 (Sep 6, 2017) 51 | 52 | - README.md updated 53 | 54 | ## 0.2.0 (Sep 6, 2017) 55 | 56 | - Service to get token config 57 | - ENV config added issuer and audience 58 | - Call settings of token in encode/decode functions 59 | - When building token added issuer, audience, issued at time and expiration. 60 | 61 | ## 0.1.1 (Sep 5, 2017) 62 | 63 | - Added CHANGELOG.md 64 | - Release version on README.md 65 | 66 | ## 0.1.0 (Sep 5, 2017) 67 | 68 | - Initial release 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Avellaneda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phalcon Micro REST API Basic Project Skeleton 2 | 3 | [![Author](http://img.shields.io/badge/author-@davellanedam-blue.svg?style=flat-square)](https://twitter.com/davellanedam) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/davellanedam/phalcon-micro-rest-api-skeleton/blob/master/LICENSE) 5 | [![Release](https://img.shields.io/github/release/davellanedam/phalcon-micro-rest-api-skeleton.svg?style=flat-square)](https://github.com/davellanedam/phalcon-micro-rest-api-skeleton/releases) 6 | 7 | ## Getting started 8 | 9 | This is a basic API REST skeleton written on the **ultra hyper mega fastest framework for PHP [phalcon](https://github.com/phalcon/cphalcon)**. A full-stack PHP framework delivered as a C-extension. Its innovative architecture makes Phalcon the fastest PHP framework ever built! 10 | 11 | This project is created to help other developers create a **basic REST API in an easy way with phalcon**. This basic example shows how powerful and simple phalcon can be. Do you want to contribute? Pull requests are always welcome to show more features of phalcon. 12 | 13 | ## Features 14 | 15 | - JWT Tokens, provide login with `Authorization` header with value `Basic username:password` where `username:password` **MUST BE ENCODED** with `Base64`. 16 | - Make requests with a token after login with `Authorization` header with value `Bearer yourToken` where `yourToken` is the **signed and encrypted token** given in the response from the login process. 17 | - Use ACL so you can have roles for users. 18 | - Timezone ready: Work UTC time (GMT+0). Responses with iso8601 date/time format. 19 | - Pagination ready. 20 | - Filters (JSON). 21 | - Easy deploy to staging and production environments with rsync. 22 | - Internationalization ready. API responses use JSON format to make life easier at the frontend. 23 | - Separate database for logs. 24 | - User profile. 25 | - Users list. 26 | - Cities. (Example of use: call cities API, then send name of the city when creating or updating a user. 27 | - Integrated tests 28 | 29 | ## Requirements 30 | 31 | - Apache **2** 32 | - PHP **5.6+** 33 | - Phalcon **3.2+** 34 | - MySQL **5.5+** 35 | 36 | ## How to install 37 | 38 | ### Using Git (recommended) 39 | 40 | 1. First you need to [install composer](https://getcomposer.org/download/) if you haven´t already. 41 | 2. Clone the project from github. Change 'myproject' to you project name. 42 | 43 | ```bash 44 | git clone https://github.com/davellanedam/phalcon-micro-api.git ./myproject 45 | ``` 46 | 47 | ### Using manual download ZIP 48 | 49 | 1. Download repository 50 | 2. Uncompress to your desired directory 51 | 52 | ### Install composer dependencies after installing (Git or manual download) 53 | 54 | ```bash 55 | cd myproject 56 | composer install 57 | composer update 58 | ``` 59 | 60 | ### Database Configuration and Security 61 | 62 | There are 3 files in the `/myproject/config` directory, (development, staging and production) each one is meant to be used on different environments to make your life easier on deployment. 63 | 64 | 1. Create a MySQL database with your custom name and then import `myproject.sql` (in the `/schemas` directory) 65 | 2. Create a second MySQL database with your custom name and then import `myproject_log.sql` (in the `/schemas` directory). 66 | 3. Open `/myproject/config/server.development.php` and setup your DEVELOPMENT (local) database connection credentials 67 | 4. Open `/myproject/config/server.staging.php` and setup your STAGING (testing server) database connection credentials 68 | 5. Open `/myproject/config/server.production.php` and setup your PRODUCTION (production server) database connection credentials 69 | 70 | This is the structure of those 3 files, remember to change values for yours. 71 | 72 | ```php 73 | return [ 74 | 'database' => [ 75 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 76 | 'host' => 'your_ip_or_hostname', 77 | 'username' => 'your_db_username', 78 | 'password' => 'your_db_password', 79 | 'dbname' => 'your_database_schema', 80 | 'charset' => 'utf8', 81 | ], 82 | 'log_database' => [ 83 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 84 | 'host' => 'your_ip_or_hostname', 85 | 'username' => 'your_db_username', 86 | 'password' => 'your_db_password', 87 | 'dbname' => 'myproject_log', 88 | 'charset' => 'utf8', 89 | ], 90 | 'authentication' => [ 91 | 'secret' => 'your secret key to SIGN token', // This will sign the token. (still insecure) 92 | 'encryption_key' => 'Your ultra secret key to ENCRYPT the token', // Secure token with an ultra password 93 | 'expiration_time' => 86400 * 7, // One week till token expires 94 | 'iss' => 'myproject', // Token issuer eg. www.myproject.com 95 | 'aud' => 'myproject', // Token audience eg. www.myproject.com 96 | ] 97 | ]; 98 | ``` 99 | 100 | ### Setting up environments 101 | 102 | The ENV variable is set on an .htaccess file located at `/public/.htaccess` that you must upload **once** to each server you use. Change the environment variable on each server to what you need. To make your life easy this .htaccess file is on the **excluded files list to upload** when you make a deploy. Possible values are: `development`, `staging` and `production`. 103 | 104 | ```bash 105 | ############################################################ 106 | # Possible values: development, staging, production # 107 | # Change value and upload ONCE to your server # 108 | # AVOID re-uploading when deployment, things will go crazy # 109 | ############################################################ 110 | SetEnv APPLICATION_ENV "development" 111 | ``` 112 | 113 | ## Usage 114 | 115 | Once everything is set up to test API routes either use Postman or any other api testing application. Remember to change the URL of the **provided example Postman JSON file**. Default username/password combination for login is `admin/admin1234`. 116 | 117 | If you use Postman please go to `manage environments` and then create one for each of your servers. Create a new key `authToken` with `token` value (the token you got from the login process), each time you make a request to the API it will send `Authorization` header with the token value in the request, you can check this on the headers of users or cities endpoints in the Postman example. 118 | 119 | This is a REST API, so it works using the following HTTP methods: 120 | 121 | - GET (Read): Gets a list of items, or a single item 122 | - POST (Create): Creates an item 123 | - PATCH (Update): Updates an item 124 | - DELETE: Deletes an item 125 | 126 | ### Testing 127 | 128 | There are some tests included, to run tests you need to go to the command line and type: 129 | 130 | ```bash 131 | composer test 132 | ``` 133 | 134 | ### Creating new models 135 | 136 | If you need to add more models to the project there´s an easy way to do it with `phalcondevtools` (If you did `composer install`, you already have this). 137 | Step into a terminal window and open your project folder, then type the following and you are set! 138 | 139 | ```bash 140 | phalcon model --name=your_table_name --schema=your_database --mapcolumn 141 | ``` 142 | 143 | ### Creating new controllers 144 | 145 | If you need to add more controllers to the project there´s an easy way to do it with `phalcondevtools` (If you did `composer install`, you already have this). 146 | Step into a terminal window and open your project folder, then type the following. 147 | 148 | ```bash 149 | phalcon controller --name=your_controller_name_without_the_controller_word 150 | ``` 151 | 152 | When it´s done, it creates your new controller, but if you want to use `ControllerBase.php` functions in your newly created controller you must change the following line in the new controller: 153 | 154 | ```php 155 | class MyNewController extends \Phalcon\Mvc\Controller 156 | ``` 157 | 158 | to this: 159 | 160 | ```php 161 | class MyNewController extends ControllerBase 162 | ``` 163 | 164 | ### Creating new routes 165 | 166 | You can add more routes to your project by adding them into the `/app.php` file. This is an example of `/users` routes: 167 | 168 | ```php 169 | /** 170 | * Users 171 | */ 172 | $users = new MicroCollection(); 173 | $users->setHandler('UsersController', true); 174 | $users->setPrefix('/users'); 175 | // Gets all users 176 | $users->get('/', 'index'); 177 | // Creates a new user 178 | $users->post('/create', 'create'); 179 | // Gets user based on unique key 180 | $users->get('/get/{id}', 'get'); 181 | // Updates user based on unique key 182 | $users->patch('/update/{id}', 'update'); 183 | // Changes user password 184 | $users->patch('/change-password/{id}', 'changePassword'); 185 | // Adds users routes to $app 186 | $app->mount($users); 187 | ``` 188 | 189 | Remember to add the controller (without the controller word) and methods of endpoints to the `/config/acl.php`file. Otherwise you will get this response from the API: `'common.YOUR_USER_ROLE_DOES_NOT_HAVE_THIS_FEATURE',` 190 | 191 | ```php 192 | /* 193 | * RESOURCES 194 | * for each user, specify the 'controller' and 'method' they have access to ('user_type'=>['controller'=>['method','method','...']],...) 195 | * */ 196 | $arrResources = [ 197 | 'Guest'=>[ 198 | 'Users'=>['login'], 199 | ], 200 | 'User'=>[ 201 | 'Profile'=>['index','update','changePassword'], 202 | 'Users'=>['index','create','get','search','update','logout'], 203 | 'Cities'=>['index','create','get','ajax','update','delete'], 204 | ], 205 | 'Superuser'=>[ 206 | 'Users'=>['changePassword'], 207 | ] 208 | ]; 209 | ``` 210 | 211 | Always keep in mind the following: 212 | 213 | ```php 214 | /* 215 | * ROLES 216 | * Superuser - can do anything (Guest, User, and own things) 217 | * User - can do most things (Guest and own things) 218 | * Guest - Public 219 | * */ 220 | ``` 221 | 222 | ## Internationalization 223 | 224 | API is designed to response with a JSON, so at the FRONTEND you can use lang files like this: 225 | Put your language files in 'langs' directory at frontend: 226 | 227 | - `en.json` 228 | - `es.json` 229 | 230 | Example of language file `en.json` : 231 | 232 | ```json 233 | { 234 | "common": { 235 | "HEADER_AUTHORIZATION_NOT_SENT": "Header Authorization not sent", 236 | "EMPTY_TOKEN_OR_NOT_RECEIVED": "Empty token or not received", 237 | "YOUR_USER_ROLE_DOES_NOT_HAVE_THIS_FEATURE": "Your user role does not have this feature", 238 | "BAD_TOKEN_GET_A_NEW_ONE": "Bad token, get a new one", 239 | "SUCCESSFUL_REQUEST": "Succesfull request", 240 | "CREATED_SUCCESSFULLY": "Created successfully", 241 | "THERE_IS_ALREADY_A_RECORD_WITH_THAT_NAME": "There is already a record with that name", 242 | "UPDATED_SUCCESSFULLY": "Updated successfully", 243 | "DELETED_SUCCESSFULLY": "Deleted successfully", 244 | "THERE_HAS_BEEN_AN_ERROR": "There has been an error", 245 | "INCOMPLETE_DATA_RECEIVED": "Incomplete data received", 246 | "NO_RECORDS": "No records", 247 | "COULD_NOT_BE_CREATED": "Could not be created", 248 | "NOT_FOUND": "Not found", 249 | "COULD_NOT_BE_DELETED": "Could not be deleted", 250 | "COULD_NOT_BE_UPDATED": "Could not be updated" 251 | }, 252 | "login": { 253 | "USER_IS_NOT_REGISTERED": "User is not registered", 254 | "USER_BLOCKED": "User blocked", 255 | "USER_UNAUTHORIZED": "User unauthorized", 256 | "WRONG_USER_PASSWORD": "Wrong user password", 257 | "TOO_MANY_FAILED_LOGIN_ATTEMPTS": "Too many failed login attempts" 258 | }, 259 | "profile": { 260 | "PROFILE_NOT_FOUND": "Profile not found", 261 | "PROFILE_UPDATED": "Profile updated", 262 | "PROFILE_COULD_NOT_BE_UPDATED": "Profile could not be updated", 263 | "ANOTHER_USER_ALREADY_REGISTERED_WITH_THIS_USERNAME": "Another user already registered with this username" 264 | }, 265 | "change-password": { 266 | "PASSWORD_COULD_NOT_BE_UPDATED": "Password could not be updated", 267 | "PASSWORD_SUCCESSFULLY_UPDATED": "Password updated successfully", 268 | "WRONG_CURRENT_PASSWORD": "Wrong current password" 269 | } 270 | } 271 | ``` 272 | 273 | Example of language file `es.json` : 274 | 275 | ```json 276 | { 277 | "common": { 278 | "HEADER_AUTHORIZATION_NOT_SENT": "Cabezote Authorization no enviado", 279 | "EMPTY_TOKEN_OR_NOT_RECEIVED": "Token vacío o no recibido", 280 | "YOUR_USER_ROLE_DOES_NOT_HAVE_THIS_FEATURE": "Tu rol de usuario no tiene esta característica", 281 | "BAD_TOKEN_GET_A_NEW_ONE": "Token errado, solicita uno nuevo", 282 | "SUCCESSFUL_REQUEST": "Solicitud exitosa", 283 | "CREATED_SUCCESSFULLY": "Creado ecitosamente", 284 | "THERE_IS_ALREADY_A_RECORD_WITH_THAT_NAME": "Ya existe un registro con ese nombre", 285 | "UPDATED_SUCCESSFULLY": "Actualizado exitosamente", 286 | "DELETED_SUCCESSFULLY": "Eliminado exitosamente", 287 | "THERE_HAS_BEEN_AN_ERROR": "Ha ocurrido un error", 288 | "INCOMPLETE_DATA_RECEIVED": "Datos incompletos recibidos", 289 | "NO_RECORDS": "No hay registros", 290 | "COULD_NOT_BE_CREATED": "No se pudo crear", 291 | "NOT_FOUND": "No encontrado", 292 | "COULD_NOT_BE_DELETED": "No pudo ser eliminado", 293 | "COULD_NOT_BE_UPDATED": "No pudo ser actualizado" 294 | }, 295 | "login": { 296 | "USER_IS_NOT_REGISTERED": "Usuario no está registrado", 297 | "USER_BLOCKED": "Usuario bloqueado", 298 | "USER_UNAUTHORIZED": "Usuario no autorizado", 299 | "WRONG_USER_PASSWORD": "Contraseña de usuario errada", 300 | "TOO_MANY_FAILED_LOGIN_ATTEMPTS": "Demasiados intento de ingreso fallidos" 301 | }, 302 | "profile": { 303 | "PROFILE_NOT_FOUND": "Perfil no encontrado", 304 | "PROFILE_UPDATED": "Perfil actualizado", 305 | "PROFILE_COULD_NOT_BE_UPDATED": "Perfil no pudo ser actualizado", 306 | "ANOTHER_USER_ALREADY_REGISTERED_WITH_THIS_USERNAME": "Otro usuario ya se encuentra registrado con este nombre de usuario" 307 | }, 308 | "change-password": { 309 | "PASSWORD_COULD_NOT_BE_UPDATED": "Contraseña no pudo ser actualizada", 310 | "PASSWORD_SUCCESSFULLY_UPDATED": "Contraseña actualizada exitosamente", 311 | "WRONG_CURRENT_PASSWORD": "Contraseña actual errada" 312 | } 313 | } 314 | ``` 315 | 316 | ## Deployment 317 | 318 | You can make a deploy to staging or production servers. It will run a bash file with `rsync` command to sync your project directory to the servers project directory. Step into a terminal window and open your project folder, then type the following. 319 | 320 | ### Deploy to staging server 321 | 322 | ```bash 323 | deploy-staging.sh 324 | ``` 325 | 326 | ### Deploy to production server 327 | 328 | ```bash 329 | deploy-production.sh 330 | ``` 331 | 332 | ### Do not forget to change variables to yours on each .sh file 333 | 334 | ```bash 335 | PROJECT=myproject 336 | USER=server_username 337 | URL=my_staging_server_url 338 | ``` 339 | 340 | ### Excluding files on deploy 341 | 342 | You can exclude files on each deployment environment (`/exclude-production.txt` and `/exclude-staging.txt`), add files you want to be excluded on the corresponding .txt file. Each excluded file or directory must be on a new line. Staging exclusion list example: 343 | 344 | ```txt 345 | .git/ 346 | *.DS_Store 347 | .phalcon/ 348 | .php/ 349 | cache/ 350 | .htaccess 351 | exclude-staging.txt 352 | exclude-production.txt 353 | deploy-staging.sh 354 | deploy-production.sh 355 | config/server.development.php 356 | config/server.production.php 357 | public/.htaccess 358 | schemas/ 359 | ``` 360 | 361 | ## Bugs or improvements 362 | 363 | Feel free to report any bugs or improvements. Pull requests are always welcome. 364 | 365 | ## I love this! How can I help 366 | 367 | It´s amazing you feel like that! Send me a tweet , share this with others, make a pull request or if you feel really thankful you can always buy me a beer! Enjoy! 368 | 369 | ## License 370 | 371 | This project is open-sourced software licensed under the MIT License. See the LICENSE file for more information. 372 | -------------------------------------------------------------------------------- /api-phalcon-micro.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bd456712-1cdc-b9c5-4961-deef858101dc", 3 | "name": "api phalcon micro", 4 | "description": "", 5 | "auth": null, 6 | "events": null, 7 | "variables": [], 8 | "order": [], 9 | "folders_order": ["63ff5769-c1f5-dde5-89de-0913c849d29f", "b7a3d897-49ac-abd0-c890-9fdf0097b542", "d18dfd48-d3ae-0c30-29c5-ffa0e8e6c98f", "0468a31b-14b4-a0cd-f956-5b77bd88737b"], 10 | "folders": [{ 11 | "id": "63ff5769-c1f5-dde5-89de-0913c849d29f", 12 | "name": "Cities", 13 | "description": "", 14 | "auth": null, 15 | "events": null, 16 | "collection": "bd456712-1cdc-b9c5-4961-deef858101dc", 17 | "folder": null, 18 | "order": ["732b4a4c-62ed-d256-0a2c-9484ac355d9a", "2d2dc26e-eff1-770a-647c-261a896ad4b5", "d74ea29e-dc42-39d8-4b0c-13cc3af6d97d", "1c5737da-a0e5-e6ad-a8c7-fb413f6e40a8", "045aa7d8-7521-f81e-f3fc-1a2765f47d7b"], 19 | "folders_order": [], 20 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 21 | "folderId": "63ff5769-c1f5-dde5-89de-0913c849d29f" 22 | }, { 23 | "id": "b7a3d897-49ac-abd0-c890-9fdf0097b542", 24 | "name": "Index", 25 | "description": "", 26 | "auth": null, 27 | "events": null, 28 | "collection": "bd456712-1cdc-b9c5-4961-deef858101dc", 29 | "folder": null, 30 | "order": ["6ba0116e-3c18-cbe8-8449-cf548b950bfc"], 31 | "folders_order": [], 32 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 33 | "folderId": "b7a3d897-49ac-abd0-c890-9fdf0097b542" 34 | }, { 35 | "id": "d18dfd48-d3ae-0c30-29c5-ffa0e8e6c98f", 36 | "name": "Profile", 37 | "description": "", 38 | "auth": null, 39 | "events": null, 40 | "collection": "bd456712-1cdc-b9c5-4961-deef858101dc", 41 | "folder": null, 42 | "order": ["634b17b9-3c8f-ff74-1263-eb01f1a36960", "38b8d9dd-5b98-89e7-e99b-650ba637b54c", "65068fc0-8fd7-7824-99cb-059a147c6a9c"], 43 | "folders_order": [], 44 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 45 | "folderId": "d18dfd48-d3ae-0c30-29c5-ffa0e8e6c98f" 46 | }, { 47 | "id": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 48 | "name": "Users", 49 | "description": "", 50 | "auth": null, 51 | "events": null, 52 | "collection": "bd456712-1cdc-b9c5-4961-deef858101dc", 53 | "folder": null, 54 | "order": ["eb82e60a-952b-c51f-5954-d16db6fb5ca9", "b4ec44f8-a16f-9036-21e5-dadcfda27cd0", "bd55552c-2081-ffee-10a5-9b407b85ea20", "2923ffa1-ff09-5938-4c05-9efb56cb7f16", "666acbf2-52e2-2da0-10ff-204e2346fa10", "f3b827e3-ff06-4c39-afa1-025026eb7b4e"], 55 | "folders_order": [], 56 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 57 | "folderId": "0468a31b-14b4-a0cd-f956-5b77bd88737b" 58 | }], 59 | "requests": [{ 60 | "id": "045aa7d8-7521-f81e-f3fc-1a2765f47d7b", 61 | "name": "\/cities\/delete\/{id}", 62 | "url": "http:\/\/api.myproject.local\/cities\/delete\/8", 63 | "description": "Delete city", 64 | "data": [], 65 | "dataMode": "params", 66 | "headerData": [{ 67 | "key": "Authorization", 68 | "value": "Bearer {{authToken}}", 69 | "description": "", 70 | "enabled": true 71 | }], 72 | "method": "DELETE", 73 | "pathVariableData": [], 74 | "queryParams": [], 75 | "auth": null, 76 | "events": null, 77 | "folder": "63ff5769-c1f5-dde5-89de-0913c849d29f", 78 | "currentHelper": null, 79 | "helperAttributes": null, 80 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 81 | "headers": "Authorization: Bearer {{authToken}}\n", 82 | "pathVariables": [] 83 | }, { 84 | "id": "1c5737da-a0e5-e6ad-a8c7-fb413f6e40a8", 85 | "name": "\/cities\/update\/{id}", 86 | "url": "http:\/\/api.myproject.local\/cities\/update\/7", 87 | "description": "Update city info", 88 | "data": [{ 89 | "key": "name", 90 | "value": "Floridablanca", 91 | "type": "text", 92 | "enabled": true 93 | }, { 94 | "key": "country", 95 | "value": "Colombia", 96 | "description": "", 97 | "type": "text", 98 | "enabled": true 99 | }], 100 | "dataMode": "urlencoded", 101 | "headerData": [{ 102 | "key": "Authorization", 103 | "value": "Bearer {{authToken}}", 104 | "description": "", 105 | "enabled": true 106 | }], 107 | "method": "PATCH", 108 | "pathVariableData": [], 109 | "queryParams": [], 110 | "auth": null, 111 | "events": null, 112 | "folder": "63ff5769-c1f5-dde5-89de-0913c849d29f", 113 | "currentHelper": null, 114 | "helperAttributes": null, 115 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 116 | "headers": "Authorization: Bearer {{authToken}}\n", 117 | "pathVariables": [] 118 | }, { 119 | "id": "2923ffa1-ff09-5938-4c05-9efb56cb7f16", 120 | "name": "\/users\/update\/{id}", 121 | "url": "http:\/\/api.myproject.local\/users\/update\/1", 122 | "description": "Update user info", 123 | "data": [{ 124 | "key": "email", 125 | "value": "onemore@email.com", 126 | "description": "", 127 | "type": "text", 128 | "enabled": true 129 | }, { 130 | "key": "firstname", 131 | "value": "Other name", 132 | "type": "text", 133 | "enabled": true 134 | }, { 135 | "key": "lastname", 136 | "value": "Other lastname", 137 | "type": "text", 138 | "enabled": true 139 | }, { 140 | "key": "birthday", 141 | "value": "1979-01-01", 142 | "description": "", 143 | "type": "text", 144 | "enabled": true 145 | }, { 146 | "key": "level", 147 | "value": "Superuser", 148 | "type": "text", 149 | "enabled": true 150 | }, { 151 | "key": "phone", 152 | "value": "4534534", 153 | "type": "text", 154 | "enabled": true 155 | }, { 156 | "key": "mobile", 157 | "value": "2425235", 158 | "type": "text", 159 | "enabled": true 160 | }, { 161 | "key": "address", 162 | "value": "Calle 12", 163 | "type": "text", 164 | "enabled": true 165 | }, { 166 | "key": "city", 167 | "value": "Bogot\u00e1", 168 | "type": "text", 169 | "enabled": true 170 | }, { 171 | "key": "country", 172 | "value": "Colombia", 173 | "type": "text", 174 | "enabled": true 175 | }, { 176 | "key": "authorised", 177 | "value": "1", 178 | "type": "text", 179 | "enabled": true 180 | }], 181 | "dataMode": "urlencoded", 182 | "headerData": [{ 183 | "key": "Authorization", 184 | "value": "Bearer {{authToken}}", 185 | "description": "", 186 | "enabled": true 187 | }], 188 | "method": "PATCH", 189 | "pathVariableData": [], 190 | "queryParams": [], 191 | "auth": null, 192 | "events": null, 193 | "folder": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 194 | "currentHelper": null, 195 | "helperAttributes": null, 196 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 197 | "headers": "Authorization: Bearer {{authToken}}\n", 198 | "pathVariables": [] 199 | }, { 200 | "id": "2d2dc26e-eff1-770a-647c-261a896ad4b5", 201 | "name": "\/cities\/create", 202 | "url": "http:\/\/api.myproject.local\/cities\/create", 203 | "description": "Create city", 204 | "data": [{ 205 | "key": "name", 206 | "value": "Floridablanca35", 207 | "type": "text", 208 | "enabled": true 209 | }, { 210 | "key": "country", 211 | "value": "Colombia", 212 | "description": "", 213 | "type": "text", 214 | "enabled": true 215 | }], 216 | "dataMode": "urlencoded", 217 | "headerData": [{ 218 | "key": "Authorization", 219 | "value": "Bearer {{authToken}}", 220 | "description": "", 221 | "enabled": true 222 | }], 223 | "method": "POST", 224 | "pathVariableData": [], 225 | "queryParams": [], 226 | "auth": null, 227 | "events": null, 228 | "folder": "63ff5769-c1f5-dde5-89de-0913c849d29f", 229 | "currentHelper": null, 230 | "helperAttributes": null, 231 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 232 | "headers": "Authorization: Bearer {{authToken}}\n", 233 | "pathVariables": [] 234 | }, { 235 | "id": "38b8d9dd-5b98-89e7-e99b-650ba637b54c", 236 | "name": "\/profile\/update", 237 | "url": "http:\/\/api.myproject.local\/profile\/update", 238 | "description": "Updates profile", 239 | "data": [{ 240 | "key": "firstname", 241 | "value": "Other name", 242 | "type": "text", 243 | "enabled": true 244 | }, { 245 | "key": "lastname", 246 | "value": "Lastname", 247 | "type": "text", 248 | "enabled": true 249 | }, { 250 | "key": "birthday", 251 | "value": "1979-12-11", 252 | "type": "text", 253 | "enabled": true 254 | }, { 255 | "key": "phone", 256 | "value": "5345345", 257 | "type": "text", 258 | "enabled": true 259 | }, { 260 | "key": "mobile", 261 | "value": "34534534", 262 | "type": "text", 263 | "enabled": true 264 | }, { 265 | "key": "address", 266 | "value": "Calle15", 267 | "type": "text", 268 | "enabled": true 269 | }, { 270 | "key": "city", 271 | "value": "Bogot\u00e1", 272 | "type": "text", 273 | "enabled": true 274 | }, { 275 | "key": "email", 276 | "value": "asd@asd.com", 277 | "description": "", 278 | "type": "text", 279 | "enabled": true 280 | }], 281 | "dataMode": "urlencoded", 282 | "headerData": [{ 283 | "key": "Authorization", 284 | "value": "Bearer {{authToken}}", 285 | "description": "", 286 | "enabled": true 287 | }], 288 | "method": "PATCH", 289 | "pathVariableData": [], 290 | "queryParams": [], 291 | "auth": null, 292 | "events": null, 293 | "folder": "d18dfd48-d3ae-0c30-29c5-ffa0e8e6c98f", 294 | "currentHelper": null, 295 | "helperAttributes": null, 296 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 297 | "headers": "Authorization: Bearer {{authToken}}\n", 298 | "pathVariables": [] 299 | }, { 300 | "id": "634b17b9-3c8f-ff74-1263-eb01f1a36960", 301 | "name": "\/profile", 302 | "url": "http:\/\/api.myproject.local\/profile", 303 | "description": "User profile", 304 | "data": null, 305 | "dataMode": null, 306 | "headerData": [{ 307 | "key": "Authorization", 308 | "value": "Bearer {{authToken}}", 309 | "description": "", 310 | "enabled": true 311 | }], 312 | "method": "GET", 313 | "pathVariableData": [], 314 | "queryParams": [], 315 | "auth": null, 316 | "events": null, 317 | "folder": "d18dfd48-d3ae-0c30-29c5-ffa0e8e6c98f", 318 | "currentHelper": null, 319 | "helperAttributes": null, 320 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 321 | "headers": "Authorization: Bearer {{authToken}}\n", 322 | "pathVariables": [] 323 | }, { 324 | "id": "65068fc0-8fd7-7824-99cb-059a147c6a9c", 325 | "name": "\/profile\/change-password", 326 | "url": "http:\/\/api.myproject.local\/profile\/change-password", 327 | "description": "Updates password", 328 | "data": [{ 329 | "key": "current_password", 330 | "value": "admin1234", 331 | "type": "text", 332 | "enabled": true 333 | }, { 334 | "key": "new_password", 335 | "value": "admin1234", 336 | "type": "text", 337 | "enabled": true 338 | }], 339 | "dataMode": "urlencoded", 340 | "headerData": [{ 341 | "key": "Authorization", 342 | "value": "Bearer {{authToken}}", 343 | "description": "", 344 | "enabled": true 345 | }], 346 | "method": "PATCH", 347 | "pathVariableData": [], 348 | "queryParams": [], 349 | "auth": null, 350 | "events": null, 351 | "folder": "d18dfd48-d3ae-0c30-29c5-ffa0e8e6c98f", 352 | "currentHelper": null, 353 | "helperAttributes": null, 354 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 355 | "headers": "Authorization: Bearer {{authToken}}\n", 356 | "pathVariables": [] 357 | }, { 358 | "id": "666acbf2-52e2-2da0-10ff-204e2346fa10", 359 | "name": "\/users\/change-password\/{id}", 360 | "url": "http:\/\/api.myproject.local\/users\/change-password\/2", 361 | "description": "Update user password", 362 | "data": [{ 363 | "key": "new_password", 364 | "value": "admin1234", 365 | "type": "text", 366 | "enabled": true 367 | }], 368 | "dataMode": "urlencoded", 369 | "headerData": [{ 370 | "key": "Authorization", 371 | "value": "Bearer {{authToken}}", 372 | "description": "", 373 | "enabled": true 374 | }], 375 | "method": "PATCH", 376 | "pathVariableData": [], 377 | "queryParams": [], 378 | "auth": null, 379 | "events": null, 380 | "folder": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 381 | "currentHelper": null, 382 | "helperAttributes": null, 383 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 384 | "headers": "Authorization: Bearer {{authToken}}\n", 385 | "pathVariables": [] 386 | }, { 387 | "id": "6ba0116e-3c18-cbe8-8449-cf548b950bfc", 388 | "name": "\/authenticate\/", 389 | "url": "http:\/\/api.myproject.local\/authenticate", 390 | "description": "User authentication", 391 | "data": [], 392 | "dataMode": "raw", 393 | "headerData": [{ 394 | "key": "Authorization", 395 | "value": "Basic YWRtaW46YWRtaW4xMjM0", 396 | "description": "", 397 | "enabled": true 398 | }], 399 | "method": "POST", 400 | "pathVariableData": [], 401 | "queryParams": [], 402 | "auth": { 403 | "type": "basic", 404 | "basic": [{ 405 | "key": "password", 406 | "value": "admin1234" 407 | }, { 408 | "key": "username", 409 | "value": "admin" 410 | }, { 411 | "key": "saveHelperData", 412 | "type": "any" 413 | }, { 414 | "key": "showPassword", 415 | "value": false, 416 | "type": "boolean" 417 | }] 418 | }, 419 | "events": null, 420 | "folder": "b7a3d897-49ac-abd0-c890-9fdf0097b542", 421 | "currentHelper": "basicAuth", 422 | "helperAttributes": { 423 | "id": "basic", 424 | "username": "admin", 425 | "password": "admin1234" 426 | }, 427 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 428 | "rawModeData": "", 429 | "headers": "Authorization: Basic YWRtaW46YWRtaW4xMjM0\n", 430 | "pathVariables": [] 431 | }, { 432 | "id": "732b4a4c-62ed-d256-0a2c-9484ac355d9a", 433 | "name": "\/cities", 434 | "url": "http:\/\/api.myproject.local\/cities?limit=5&offset=0&sort=name&order=asc", 435 | "description": "Cities list", 436 | "data": null, 437 | "dataMode": null, 438 | "headerData": [{ 439 | "key": "Authorization", 440 | "value": "Bearer {{authToken}}", 441 | "description": "", 442 | "enabled": true 443 | }], 444 | "method": "GET", 445 | "pathVariableData": [], 446 | "queryParams": [{ 447 | "key": "limit", 448 | "value": "5", 449 | "equals": true, 450 | "description": "", 451 | "enabled": true 452 | }, { 453 | "key": "offset", 454 | "value": "0", 455 | "equals": true, 456 | "description": "", 457 | "enabled": true 458 | }, { 459 | "key": "sort", 460 | "value": "name", 461 | "equals": true, 462 | "description": "", 463 | "enabled": true 464 | }, { 465 | "key": "order", 466 | "value": "asc", 467 | "equals": true, 468 | "description": "", 469 | "enabled": true 470 | }], 471 | "auth": null, 472 | "events": null, 473 | "folder": "63ff5769-c1f5-dde5-89de-0913c849d29f", 474 | "currentHelper": null, 475 | "helperAttributes": null, 476 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 477 | "headers": "Authorization: Bearer {{authToken}}\n", 478 | "pathVariables": [] 479 | }, { 480 | "id": "b4ec44f8-a16f-9036-21e5-dadcfda27cd0", 481 | "name": "\/users\/create", 482 | "url": "http:\/\/api.myproject.local\/users\/create", 483 | "description": "Create user", 484 | "data": [{ 485 | "key": "email", 486 | "value": "another@email.com", 487 | "type": "text", 488 | "enabled": true 489 | }, { 490 | "key": "new_password", 491 | "value": "test123", 492 | "type": "text", 493 | "enabled": true 494 | }, { 495 | "key": "username", 496 | "value": "admintest", 497 | "type": "text", 498 | "enabled": true 499 | }, { 500 | "key": "firstname", 501 | "value": "My name", 502 | "type": "text", 503 | "enabled": true 504 | }, { 505 | "key": "lastname", 506 | "value": "My last name", 507 | "type": "text", 508 | "enabled": true 509 | }, { 510 | "key": "level", 511 | "value": "Superuser", 512 | "type": "text", 513 | "enabled": true 514 | }, { 515 | "key": "phone", 516 | "value": "12312312", 517 | "type": "text", 518 | "enabled": true 519 | }, { 520 | "key": "mobile", 521 | "value": "31312312", 522 | "type": "text", 523 | "enabled": true 524 | }, { 525 | "key": "address", 526 | "value": "Calle 10", 527 | "type": "text", 528 | "enabled": true 529 | }, { 530 | "key": "city", 531 | "value": "Bogot\u00e1", 532 | "type": "text", 533 | "enabled": true 534 | }, { 535 | "key": "country", 536 | "value": "Colombia", 537 | "description": "", 538 | "type": "text", 539 | "enabled": true 540 | }, { 541 | "key": "birthday", 542 | "value": "1979-01-01", 543 | "type": "text", 544 | "enabled": true 545 | }, { 546 | "key": "authorised", 547 | "value": "1", 548 | "type": "text", 549 | "enabled": true 550 | }], 551 | "dataMode": "urlencoded", 552 | "headerData": [{ 553 | "key": "Authorization", 554 | "value": "Bearer {{authToken}}", 555 | "description": "", 556 | "enabled": true 557 | }], 558 | "method": "POST", 559 | "pathVariableData": [], 560 | "queryParams": [], 561 | "auth": null, 562 | "events": null, 563 | "folder": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 564 | "currentHelper": null, 565 | "helperAttributes": null, 566 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 567 | "headers": "Authorization: Bearer {{authToken}}\n", 568 | "pathVariables": [] 569 | }, { 570 | "id": "bd55552c-2081-ffee-10a5-9b407b85ea20", 571 | "name": "\/users\/get\/{id}", 572 | "url": "http:\/\/api.myproject.local\/users\/get\/1", 573 | "description": "Get user info", 574 | "data": null, 575 | "dataMode": null, 576 | "headerData": [{ 577 | "key": "Authorization", 578 | "value": "Bearer {{authToken}}", 579 | "description": "", 580 | "enabled": true 581 | }], 582 | "method": "GET", 583 | "pathVariableData": [], 584 | "queryParams": [], 585 | "auth": null, 586 | "events": null, 587 | "folder": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 588 | "currentHelper": null, 589 | "helperAttributes": null, 590 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 591 | "headers": "Authorization: Bearer {{authToken}}\n", 592 | "pathVariables": [] 593 | }, { 594 | "id": "d74ea29e-dc42-39d8-4b0c-13cc3af6d97d", 595 | "name": "\/cities\/get\/{id}", 596 | "url": "http:\/\/api.myproject.local\/cities\/get\/1", 597 | "description": "Get city info", 598 | "data": null, 599 | "dataMode": null, 600 | "headerData": [{ 601 | "key": "Authorization", 602 | "value": "Bearer {{authToken}}", 603 | "description": "", 604 | "enabled": true 605 | }], 606 | "method": "GET", 607 | "pathVariableData": [], 608 | "queryParams": [], 609 | "auth": null, 610 | "events": null, 611 | "folder": "63ff5769-c1f5-dde5-89de-0913c849d29f", 612 | "currentHelper": null, 613 | "helperAttributes": null, 614 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 615 | "headers": "Authorization: Bearer {{authToken}}\n", 616 | "pathVariables": [] 617 | }, { 618 | "id": "eb82e60a-952b-c51f-5954-d16db6fb5ca9", 619 | "name": "\/users", 620 | "url": "http:\/\/api.myproject.local\/users", 621 | "description": "Users list", 622 | "data": null, 623 | "dataMode": null, 624 | "headerData": [{ 625 | "key": "Authorization", 626 | "value": "Bearer {{authToken}}", 627 | "description": "", 628 | "enabled": true 629 | }], 630 | "method": "GET", 631 | "pathVariableData": [], 632 | "queryParams": [{ 633 | "key": "limit", 634 | "value": "5", 635 | "equals": true, 636 | "description": "", 637 | "enabled": false 638 | }, { 639 | "key": "offset", 640 | "value": "0", 641 | "equals": true, 642 | "description": "", 643 | "enabled": false 644 | }, { 645 | "key": "sort", 646 | "value": "firstname, lastname", 647 | "equals": true, 648 | "description": "", 649 | "enabled": false 650 | }, { 651 | "key": "order", 652 | "value": "asc", 653 | "equals": true, 654 | "description": "", 655 | "enabled": false 656 | }, { 657 | "key": "filter", 658 | "value": "{\"user_phone\":60}", 659 | "description": "", 660 | "enabled": false 661 | }], 662 | "auth": null, 663 | "events": null, 664 | "folder": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 665 | "currentHelper": null, 666 | "helperAttributes": null, 667 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 668 | "headers": "Authorization: Bearer {{authToken}}\n", 669 | "pathVariables": [] 670 | }, { 671 | "id": "f3b827e3-ff06-4c39-afa1-025026eb7b4e", 672 | "name": "\/users\/delete\/{id}", 673 | "url": "http:\/\/api.myproject.local\/users\/delete\/2", 674 | "description": "Delete user", 675 | "data": [], 676 | "dataMode": "params", 677 | "headerData": [{ 678 | "description": "", 679 | "enabled": true, 680 | "key": "Authorization", 681 | "value": "Bearer {{authToken}}" 682 | }], 683 | "method": "DELETE", 684 | "pathVariableData": [], 685 | "queryParams": [], 686 | "auth": null, 687 | "events": null, 688 | "folder": "0468a31b-14b4-a0cd-f956-5b77bd88737b", 689 | "currentHelper": null, 690 | "helperAttributes": null, 691 | "collectionId": "bd456712-1cdc-b9c5-4961-deef858101dc", 692 | "headers": "Authorization: Bearer {{authToken}}\n", 693 | "pathVariables": [] 694 | }] 695 | } 696 | -------------------------------------------------------------------------------- /app.php: -------------------------------------------------------------------------------- 1 | before(new AccessMiddleware()); 14 | 15 | /** 16 | * Insert your Routes below 17 | */ 18 | 19 | /** 20 | * Index 21 | */ 22 | $index = new MicroCollection(); 23 | $index->setHandler('IndexController', true); 24 | // Gets index 25 | $index->get('/', 'index'); 26 | // Authenticates a user 27 | $index->post('/authenticate', 'login'); 28 | // logout 29 | $index->get('/logout', 'logout'); 30 | // Adds index routes to $app 31 | $app->mount($index); 32 | 33 | /** 34 | * Profile 35 | */ 36 | $profile = new MicroCollection(); 37 | $profile->setHandler('ProfileController', true); 38 | $profile->setPrefix('/profile'); 39 | // Gets profile 40 | $profile->get('/', 'index'); 41 | // // Updates user profile 42 | $profile->patch('/update', 'update'); 43 | // Changes user password 44 | $profile->patch('/change-password', 'changePassword'); 45 | // Adds profile routes to $app 46 | $app->mount($profile); 47 | 48 | /** 49 | * Users 50 | */ 51 | $users = new MicroCollection(); 52 | $users->setHandler('UsersController', true); 53 | $users->setPrefix('/users'); 54 | // Gets all users 55 | $users->get('/', 'index'); 56 | // Creates a new user 57 | $users->post('/create', 'create'); 58 | // Gets user based on unique key 59 | $users->get('/get/{id}', 'get'); 60 | // Updates user based on unique key 61 | $users->patch('/update/{id}', 'update'); 62 | // Changes user password 63 | $users->patch('/change-password/{id}', 'changePassword'); 64 | // Deletes user based on unique key 65 | $users->delete('/delete/{id}', 'delete'); 66 | // Adds users routes to $app 67 | $app->mount($users); 68 | 69 | /** 70 | * Cities 71 | */ 72 | $cities = new MicroCollection(); 73 | $cities->setHandler('CitiesController', true); 74 | $cities->setPrefix('/cities'); 75 | // Gets cities 76 | $cities->get('/', 'index'); 77 | // Creates a new city 78 | $cities->post('/create', 'create'); 79 | // Gets city based on unique key 80 | $cities->get('/get/{id}', 'get'); 81 | // Updates city based on unique key 82 | $cities->patch('/update/{id}', 'update'); 83 | // Deletes city based on unique key 84 | $cities->delete('/delete/{id}', 'delete'); 85 | // Adds cities routes to $app 86 | $app->mount($cities); 87 | 88 | /** 89 | * Not found handler 90 | */ 91 | $app->notFound(function () use ($app) { 92 | $app->response->setStatusCode(404, 'Not Found')->sendHeaders(); 93 | $app->response->setContentType('application/json', 'UTF-8'); 94 | $app->response->setJsonContent(array( 95 | 'status' => 'error', 96 | 'code' => '404', 97 | 'messages' => 'URL Not found', 98 | )); 99 | $app->response->send(); 100 | }); 101 | 102 | /** 103 | * Error handler 104 | */ 105 | $app->error( 106 | function ($exception) { 107 | print_r('An error has occurred'); 108 | } 109 | ); 110 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "./vendor/bin/phpunit --testdox tests" 4 | }, 5 | "require": { 6 | "phalcon/devtools": "^3.2", 7 | "firebase/php-jwt": "^5.0", 8 | "phpunit/phpunit": "^7.5", 9 | "guzzlehttp/guzzle": "^6.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/acl.php: -------------------------------------------------------------------------------- 1 | setDefaultAction(Phalcon\Acl::DENY); 7 | 8 | /* 9 | * ROLES 10 | * Superuser - can do anything (Guest, User, and own things) 11 | * User - can do most things (Guest and own things) 12 | * Guest - Public 13 | * */ 14 | $acl->addRole(new Phalcon\Acl\Role('Guest')); 15 | $acl->addRole(new Phalcon\Acl\Role('User')); 16 | $acl->addRole(new Phalcon\Acl\Role('Superuser')); 17 | 18 | // User can do everything a Guest can do 19 | $acl->addInherit('User', 'Guest'); 20 | // Admin can do everything a User can do 21 | $acl->addInherit('Superuser', 'Guest'); 22 | $acl->addInherit('Superuser', 'User'); 23 | 24 | /* 25 | * RESOURCES 26 | * for each user, specify the 'controller' and 'method' they have access to ('user_type'=>['controller'=>['method','method','...']],...) 27 | * this is created in an array as we later loop over this structure to assign users to resources 28 | * */ 29 | $arrResources = [ 30 | 'Guest' => [ 31 | 'Index' => ['login'], 32 | ], 33 | 'User' => [ 34 | 'Profile' => ['index', 'update', 'changePassword'], 35 | 'Users' => ['index', 'create', 'get', 'search', 'update', 'logout'], 36 | 'Cities' => ['index', 'create', 'get', 'ajax', 'update', 'delete'], 37 | ], 38 | 'Superuser' => [ 39 | 'Users' => ['changePassword', 'delete'], 40 | ], 41 | ]; 42 | 43 | foreach ($arrResources as $arrResource) { 44 | foreach ($arrResource as $controller => $arrMethods) { 45 | $acl->addResource(new Phalcon\Acl\Resource($controller), $arrMethods); 46 | } 47 | } 48 | 49 | /* 50 | * ACCESS 51 | * */ 52 | foreach ($acl->getRoles() as $objRole) { 53 | $roleName = $objRole->getName(); 54 | 55 | // everyone gets access to global resources 56 | foreach ($arrResources['Guest'] as $resource => $method) { 57 | $acl->allow($roleName, $resource, $method); 58 | } 59 | 60 | // users 61 | if ($roleName == 'User') { 62 | foreach ($arrResources['User'] as $resource => $method) { 63 | $acl->allow($roleName, $resource, $method); 64 | } 65 | } 66 | 67 | // admins 68 | if ($roleName == 'Superuser') { 69 | foreach ($arrResources['Superuser'] as $resource => $method) { 70 | $acl->allow($roleName, $resource, $method); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'title' => 'API REST', 9 | 'description' => 'API REST', 10 | 'controllersDir' => APP_PATH . '/controllers/', 11 | 'libraryDir' => APP_PATH . '/library/', 12 | 'modelsDir' => APP_PATH . '/models/', 13 | 'migrationsDir' => APP_PATH . '/migrations/', 14 | 'viewsDir' => APP_PATH . '/views/', 15 | 'middlewaresDir' => APP_PATH . '/middlewares/', 16 | 'baseUri' => '/', 17 | ], 18 | ]); 19 | 20 | $configOverride = new \Phalcon\Config(include_once __DIR__ . '/../config/server.' . APPLICATION_ENV . '.php'); 21 | 22 | $config = $config->merge($configOverride); 23 | 24 | return $config; 25 | -------------------------------------------------------------------------------- /config/loader.php: -------------------------------------------------------------------------------- 1 | registerDirs( 9 | array( 10 | $config->application->controllersDir, 11 | $config->application->middlewaresDir, 12 | $config->application->modelsDir, 13 | $config->application->libraryDir, 14 | ) 15 | )->register(); 16 | -------------------------------------------------------------------------------- /config/server.development.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 6 | 'host' => '127.0.0.1', 7 | 'username' => 'root', 8 | 'password' => 'mysql', 9 | 'dbname' => 'myproject', 10 | 'charset' => 'utf8', 11 | ], 12 | 'log_database' => [ 13 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 14 | 'host' => '127.0.0.1', 15 | 'username' => 'root', 16 | 'password' => 'mysql', 17 | 'dbname' => 'myproject_log', 18 | 'charset' => 'utf8', 19 | ], 20 | 'authentication' => [ 21 | 'secret' => 'your secret key to SIGN token', // This will sign the token. (still insecure) 22 | 'encryption_key' => 'Your ultra secret key to ENCRYPT the token', // Secure token with an ultra password 23 | 'expiration_time' => 86400 * 7, // One week till token expires 24 | 'iss' => 'myproject', // Token issuer eg. www.myproject.com 25 | 'aud' => 'myproject', // Token audience eg. www.myproject.com 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /config/server.production.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 6 | 'host' => 'your_ip_or_hostname', 7 | 'username' => 'your_db_username', 8 | 'password' => 'your_db_password', 9 | 'dbname' => 'your_database_schema', 10 | 'charset' => 'utf8', 11 | ], 12 | 'log_database' => [ 13 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 14 | 'host' => 'your_ip_or_hostname', 15 | 'username' => 'your_db_username', 16 | 'password' => 'your_db_password', 17 | 'dbname' => 'your_database_schema', 18 | 'charset' => 'utf8', 19 | ], 20 | 'authentication' => [ 21 | 'secret' => 'your secret key to SIGN token', // This will sign the token. (still insecure) 22 | 'encryption_key' => 'Your ultra secret key to ENCRYPT the token', // Secure token with an ultra password 23 | 'expiration_time' => 86400 * 7, // One week till token expires 24 | 'iss' => 'myproject', // Token issuer eg. www.myproject.com 25 | 'aud' => 'myproject', // Token audience eg. www.myproject.com 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /config/server.staging.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 6 | 'host' => 'your_ip_or_hostname', 7 | 'username' => 'your_db_username', 8 | 'password' => 'your_db_password', 9 | 'dbname' => 'your_database_schema', 10 | 'charset' => 'utf8', 11 | ], 12 | 'log_database' => [ 13 | 'adapter' => 'Mysql', /* Possible Values: Mysql, Postgres, Sqlite */ 14 | 'host' => 'your_ip_or_hostname', 15 | 'username' => 'your_db_username', 16 | 'password' => 'your_db_password', 17 | 'dbname' => 'your_database_schema', 18 | 'charset' => 'utf8', 19 | ], 20 | 'authentication' => [ 21 | 'secret' => 'your secret key to SIGN token', // This will sign the token. (still insecure) 22 | 'encryption_key' => 'Your ultra secret key to ENCRYPT the token', // Secure token with an ultra password 23 | 'expiration_time' => 86400 * 7, // One week till token expires 24 | 'iss' => 'myproject', // Token issuer eg. www.myproject.com 25 | 'aud' => 'myproject', // Token audience eg. www.myproject.com 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | set('modelsManager', function () { 21 | $modelsManager = new ModelsManager(); 22 | return $modelsManager; 23 | }); 24 | 25 | /** 26 | * Sets the view component 27 | */ 28 | $di->setShared('view', function () use ($config) { 29 | $view = new View(); 30 | $view->setViewsDir($config->application->viewsDir); 31 | return $view; 32 | }); 33 | 34 | /** 35 | * The URL component is used to generate all kind of urls in the application 36 | */ 37 | $di->setShared('url', function () use ($config) { 38 | $url = new UrlResolver(); 39 | $url->setBaseUri($config->application->baseUri); 40 | return $url; 41 | }); 42 | 43 | /** 44 | * Crypt service 45 | */ 46 | $di->set('mycrypt', function () use ($config) { 47 | $crypt = new Crypt(); 48 | $crypt->setKey($config->get('authentication')->encryption_key); 49 | return $crypt; 50 | }, true); 51 | 52 | /** 53 | * JWT service 54 | */ 55 | $di->setShared('jwt', function () { 56 | return new JWT(); 57 | }); 58 | 59 | /** 60 | * tokenConfig 61 | */ 62 | $di->setShared('tokenConfig', function () use ($config) { 63 | $tokenConfig = $config->authentication->toArray(); 64 | return $tokenConfig; 65 | }); 66 | 67 | /** 68 | * Database connection is created based in the parameters defined in the configuration file 69 | */ 70 | $di->setShared('db', function () use ($config) { 71 | $dbConfig = $config->database->toArray(); 72 | $adapter = $dbConfig['adapter']; 73 | unset($dbConfig['adapter']); 74 | 75 | $class = 'Phalcon\Db\Adapter\Pdo\\' . $adapter; 76 | 77 | $connection = new $class($dbConfig); 78 | $connection->setNestedTransactionsWithSavepoints(true); 79 | 80 | return $connection; 81 | }); 82 | 83 | /** 84 | * Another Database connection is created based in the parameters defined in the configuration file 85 | */ 86 | $di->setShared('db_log', function () use ($config) { 87 | $dbConfig = $config->log_database->toArray(); 88 | $adapter = $dbConfig['adapter']; 89 | unset($dbConfig['adapter']); 90 | 91 | $class = 'Phalcon\Db\Adapter\Pdo\\' . $adapter; 92 | 93 | $connection = new $class($dbConfig); 94 | $connection->setNestedTransactionsWithSavepoints(true); 95 | 96 | return $connection; 97 | }); 98 | -------------------------------------------------------------------------------- /controllers/CitiesController.php: -------------------------------------------------------------------------------- 1 | $name, 14 | ); 15 | $city = Cities::findFirst( 16 | array( 17 | $conditions, 18 | 'bind' => $parameters, 19 | ) 20 | ); 21 | if ($city) { 22 | $this->buildErrorResponse(409, 'common.THERE_IS_ALREADY_A_RECORD_WITH_THAT_NAME'); 23 | } 24 | } 25 | 26 | private function checksIfCityToUpdateAlreadyExists($name, $id) 27 | { 28 | $name = trim($name); 29 | $conditions = 'name = :name: AND id != :id:'; 30 | $parameters = array( 31 | 'name' => $name, 32 | 'id' => $id, 33 | ); 34 | $city = Cities::findFirst( 35 | array( 36 | $conditions, 37 | 'bind' => $parameters, 38 | ) 39 | ); 40 | if ($city) { 41 | $this->buildErrorResponse(409, 'common.THERE_IS_ALREADY_A_RECORD_WITH_THAT_NAME'); 42 | } 43 | } 44 | 45 | private function createCity($name, $country) 46 | { 47 | $city = new Cities(); 48 | $city->name = trim($name); 49 | $city->country = trim($country); 50 | $this->tryToSaveData($city, 'common.COULD_NOT_BE_CREATED'); 51 | return $city; 52 | } 53 | 54 | private function updateCity($city, $name, $country) 55 | { 56 | $city->name = trim($name); 57 | $city->country = trim($country); 58 | $this->tryToSaveData($city, 'common.COULD_NOT_BE_UPDATED'); 59 | return $city; 60 | } 61 | 62 | /** 63 | * Public functions 64 | */ 65 | public function index() 66 | { 67 | $this->initializeGet(); 68 | $options = $this->buildOptions('name asc', $this->request->get('sort'), $this->request->get('order'), $this->request->get('limit'), $this->request->get('offset')); 69 | $filters = $this->buildFilters($this->request->get('filter')); 70 | $cities = $this->findElements('Cities', $filters['conditions'], $filters['parameters'], 'id, name, country', $options['order_by'], $options['offset'], $options['limit']); 71 | $total = $this->calculateTotalElements('Cities', $filters['conditions'], $filters['parameters']); 72 | $data = $this->buildListingObject($cities, $options['rows'], $total); 73 | $this->buildSuccessResponse(200, 'common.SUCCESSFUL_REQUEST', $data); 74 | } 75 | 76 | public function create() 77 | { 78 | $this->initializePost(); 79 | $this->checkForEmptyData([$this->request->getPost('name'), $this->request->getPost('country')]); 80 | $this->checksIfCityAlreadyExists($this->request->getPost('name')); 81 | $city = $this->createCity($this->request->getPost('name'), $this->request->getPost('country')); 82 | $this->registerLog(); 83 | $this->buildSuccessResponse(201, 'common.CREATED_SUCCESSFULLY', $city->toArray()); 84 | } 85 | 86 | public function get($id) 87 | { 88 | $this->initializeGet(); 89 | $city = $this->findElementById('Cities', $id); 90 | $this->buildSuccessResponse(200, 'common.SUCCESSFUL_REQUEST', $city->toArray()); 91 | } 92 | 93 | public function update($id) 94 | { 95 | $this->initializePatch(); 96 | $this->checkForEmptyData([$this->request->getPut('name'), $this->request->getPut('country')]); 97 | $this->checksIfCityToUpdateAlreadyExists($this->request->getPut('name'), $id); 98 | $city = $this->updateCity($this->findElementById('Cities', $id), $this->request->getPut('name'), $this->request->getPut('country')); 99 | $this->registerLog(); 100 | $this->buildSuccessResponse(200, 'common.UPDATED_SUCCESSFULLY', $city->toArray()); 101 | } 102 | 103 | public function delete($id) 104 | { 105 | $this->initializeDelete(); 106 | if ($this->tryToDeleteData($this->findElementById('Cities', $id))) { 107 | $this->registerLog(); 108 | $this->buildSuccessResponse(200, 'common.DELETED_SUCCESSFULLY'); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /controllers/ControllerBase.php: -------------------------------------------------------------------------------- 1 | decodeToken($this->getToken()); 18 | 19 | // Gets URL route from request 20 | $url = $this->request->get(); 21 | 22 | // Initiates log db transaction 23 | $this->db_log->begin(); 24 | $newLog = new Logs(); 25 | $newLog->username = $token_decoded->username_username; // gets username 26 | $newLog->route = $url['_url']; // gets route 27 | $newLog->date = $this->getNowDateTime(); 28 | if (!$newLog->save()) { 29 | // rollback transaction 30 | $this->db_log->rollback(); 31 | // Send errors 32 | $errors = array(); 33 | foreach ($newLog->getMessages() as $message) { 34 | $errors[] = $message->getMessage(); 35 | } 36 | $this->buildErrorResponse(400, 'common.COULD_NOT_BE_CREATED', $errors); 37 | } 38 | // Commit the transaction 39 | $this->db_log->commit(); 40 | } 41 | 42 | /** 43 | * Try to save data in DB 44 | */ 45 | public function tryToSaveData($element, $customMessage = 'common.THERE_HAS_BEEN_AN_ERROR') 46 | { 47 | if (!$element->save()) { 48 | // Send errors 49 | $errors = array(); 50 | foreach ($element->getMessages() as $message) { 51 | $errors[] = $message->getMessage(); 52 | } 53 | $this->buildErrorResponse(400, $customMessage, $errors); 54 | } 55 | return true; 56 | } 57 | 58 | /** 59 | * Try to delete data in DB 60 | */ 61 | public function tryToDeleteData($element) 62 | { 63 | if (!$element->delete()) { 64 | // Send errors 65 | $errors = array(); 66 | foreach ($element->getMessages() as $message) { 67 | $errors[] = $message->getMessage(); 68 | } 69 | $this->buildErrorResponse(400, 'common.COULD_NOT_BE_DELETED', $errors); 70 | } 71 | return true; 72 | } 73 | 74 | /** 75 | * Build options for listings 76 | */ 77 | public function buildOptions($defaultSort, $sort, $order, $limit, $offset) 78 | { 79 | $options = []; 80 | $rows = 5; 81 | $order_by = $defaultSort; 82 | $offset = 0; 83 | $limit = $offset + $rows; 84 | 85 | // Handles Sort querystring (order_by) 86 | if ($sort != null && $order != null) { 87 | $order_by = $sort . ' ' . $order; 88 | } 89 | 90 | // Gets rows_per_page 91 | if ($this->request->get('limit') != null) { 92 | $rows = $this->getQueryLimit($this->request->get('limit')); 93 | $limit = $rows; 94 | } 95 | 96 | // Calculate the offset and limit 97 | if ($this->request->get('offset') != null) { 98 | $offset = $this->request->get('offset'); 99 | $limit = $rows; 100 | } 101 | $options = $this->array_push_assoc($options, 'rows', $rows); 102 | $options = $this->array_push_assoc($options, 'order_by', $order_by); 103 | $options = $this->array_push_assoc($options, 'offset', $offset); 104 | $options = $this->array_push_assoc($options, 'limit', $limit); 105 | return $options; 106 | } 107 | 108 | /** 109 | * Build filters for listings 110 | */ 111 | public function buildFilters($filter) 112 | { 113 | $filters = []; 114 | $conditions = []; 115 | $parameters = []; 116 | 117 | // Filters simple (no left joins needed) 118 | if ($filter != null) { 119 | $filter = json_decode($filter, true); 120 | foreach ($filter as $key => $value) { 121 | array_push($conditions, $key . ' LIKE :' . $key . ':'); 122 | $parameters = $this->array_push_assoc($parameters, $key, '%' . trim($value) . '%'); 123 | } 124 | $conditions = implode(' AND ', $conditions); 125 | } 126 | $filters = $this->array_push_assoc($filters, 'conditions', $conditions); 127 | $filters = $this->array_push_assoc($filters, 'parameters', $parameters); 128 | return $filters; 129 | } 130 | 131 | /** 132 | * Build listing object 133 | */ 134 | public function buildListingObject($elements, $rows, $total) 135 | { 136 | $data = []; 137 | $data = $this->array_push_assoc($data, 'rows_per_page', $rows); 138 | $data = $this->array_push_assoc($data, 'total_rows', $total); 139 | $data = $this->array_push_assoc($data, 'rows', $elements->toArray()); 140 | return $data; 141 | } 142 | 143 | /** 144 | * Calculates total rows for an specified model 145 | */ 146 | public function calculateTotalElements($model, $conditions, $parameters) 147 | { 148 | $total = $model::count( 149 | array( 150 | $conditions, 151 | 'bind' => $parameters, 152 | ) 153 | ); 154 | return $total; 155 | } 156 | 157 | /** 158 | * Find element by ID from an specified model 159 | */ 160 | public function findElementById($model, $id) 161 | { 162 | $conditions = 'id = :id:'; 163 | $parameters = array( 164 | 'id' => $id, 165 | ); 166 | $element = $model::findFirst( 167 | array( 168 | $conditions, 169 | 'bind' => $parameters, 170 | ) 171 | ); 172 | if (!$element) { 173 | $this->buildErrorResponse(404, 'common.NOT_FOUND'); 174 | } 175 | return $element; 176 | } 177 | 178 | /** 179 | * Find elements from an specified model 180 | */ 181 | public function findElements($model, $conditions, $parameters, $columns, $order_by, $offset, $limit) 182 | { 183 | $elements = $model::find( 184 | array( 185 | $conditions, 186 | 'bind' => $parameters, 187 | 'columns' => $columns, 188 | 'order' => $order_by, 189 | 'offset' => $offset, 190 | 'limit' => $limit, 191 | ) 192 | ); 193 | if (!$elements) { 194 | $this->buildErrorResponse(404, 'common.NO_RECORDS'); 195 | } 196 | return $elements; 197 | } 198 | 199 | /** 200 | * Check if there is missing data from the request 201 | */ 202 | public function checkForEmptyData($array) 203 | { 204 | foreach ($array as $value) { 205 | if (empty($value)) { 206 | $this->buildErrorResponse(400, 'common.INCOMPLETE_DATA_RECEIVED'); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * uset a properties from an array 213 | */ 214 | public function unsetPropertyFromArray($array, $remove) 215 | { 216 | foreach ($remove as $value) { 217 | unset($array[$value]); 218 | } 219 | return $array; 220 | } 221 | 222 | /** 223 | * Generated NOW datetime based on a timezone 224 | */ 225 | public function getNowDateTime() 226 | { 227 | $now = new DateTime(); 228 | $now->setTimezone(new DateTimeZone('UTC')); 229 | $now = $now->format('Y-m-d H:i:s'); 230 | return $now; 231 | } 232 | 233 | /** 234 | * Generated NOW datetime based on a timezone and added XX minutes 235 | */ 236 | public function getNowDateTimePlusMinutes($minutes_to_add) 237 | { 238 | $now = new DateTime(); 239 | $now->setTimezone(new DateTimeZone('UTC')); 240 | $now->add(new DateInterval('PT' . $minutes_to_add . 'M')); 241 | $now = $now->format('Y-m-d H:i:s'); 242 | return $now; 243 | } 244 | 245 | /** 246 | * Converts ISO8601 date to DateTime UTC 247 | */ 248 | public function iso8601_to_utc($date) 249 | { 250 | return date('Y-m-d H:i:s', strtotime($date)); 251 | } 252 | 253 | /** 254 | * Converts DateTime UTC date to ISO8601 255 | */ 256 | public function utc_to_iso8601($date) 257 | { 258 | if (!empty($date) && ($date != '0000-00-00') && ($date != '0000-00-00 00:00') && ($date != '0000-00-00 00:00:00')) { 259 | $datetime = new DateTime($date); 260 | return $datetime->format('Y-m-d\TH:i:s\Z'); 261 | } 262 | return null; 263 | } 264 | 265 | /** 266 | * Array push associative. 267 | */ 268 | public function array_push_assoc($array, $key, $value) 269 | { 270 | $array[$key] = $value; 271 | return $array; 272 | } 273 | 274 | /** 275 | * Generates limits for queries. 276 | */ 277 | public function getQueryLimit($limit) 278 | { 279 | $setLimit = 5; 280 | if ($limit != '') { 281 | if ($limit > 150) { 282 | $setLimit = 150; 283 | } 284 | if ($limit <= 0) { 285 | $setLimit = 1; 286 | } 287 | if (($limit >= 1) && ($limit <= 150)) { 288 | $setLimit = $limit; 289 | } 290 | } 291 | return $setLimit; 292 | } 293 | 294 | /** 295 | * Verifies if is get request 296 | */ 297 | public function initializeGet() 298 | { 299 | if (!$this->request->isGet()) { 300 | die(); 301 | } 302 | } 303 | 304 | /** 305 | * Verifies if is post request 306 | */ 307 | public function initializePost() 308 | { 309 | if (!$this->request->isPost()) { 310 | die(); 311 | } 312 | } 313 | 314 | /** 315 | * Verifies if is patch request 316 | */ 317 | public function initializePatch() 318 | { 319 | if (!$this->request->isPatch()) { 320 | die(); 321 | } 322 | } 323 | 324 | /** 325 | * Verifies if is patch request 326 | */ 327 | public function initializeDelete() 328 | { 329 | if (!$this->request->isDelete()) { 330 | die(); 331 | } 332 | } 333 | 334 | /** 335 | * Encode token. 336 | */ 337 | public function encodeToken($data) 338 | { 339 | // Encode token 340 | $token_encoded = $this->jwt->encode($data, $this->tokenConfig['secret']); 341 | $token_encoded = $this->mycrypt->encryptBase64($token_encoded); 342 | return $token_encoded; 343 | } 344 | 345 | /** 346 | * Decode token. 347 | */ 348 | public function decodeToken($token) 349 | { 350 | // Decode token 351 | $token = $this->mycrypt->decryptBase64($token); 352 | $token = $this->jwt->decode($token, $this->tokenConfig['secret'], array('HS256')); 353 | return $token; 354 | } 355 | 356 | /** 357 | * Returns token from the request. 358 | * Uses token URL query field, or Authorization header 359 | */ 360 | public function getToken() 361 | { 362 | $authHeader = $this->request->getHeader('Authorization'); 363 | $authQuery = $this->request->getQuery('token'); 364 | return $authQuery ? $authQuery : $this->parseBearerValue($authHeader); 365 | } 366 | 367 | protected function parseBearerValue($string) 368 | { 369 | if (strpos(trim($string), 'Bearer') !== 0) { 370 | return null; 371 | } 372 | return preg_replace('/.*\s/', '', $string); 373 | } 374 | 375 | /** 376 | * Builds success responses. 377 | */ 378 | public function buildSuccessResponse($code, $messages, $data = '') 379 | { 380 | switch ($code) { 381 | case 200: 382 | $status = 'OK'; 383 | break; 384 | case 201: 385 | $status = 'Created'; 386 | break; 387 | case 202: 388 | break; 389 | } 390 | $generated = array( 391 | 'status' => $status, 392 | 'code' => $code, 393 | 'messages' => $messages, 394 | 'data' => $data, 395 | ); 396 | $this->response->setStatusCode($code, $status)->sendHeaders(); 397 | $this->response->setContentType('application/json', 'UTF-8'); 398 | $this->response->setJsonContent($generated, JSON_NUMERIC_CHECK)->send(); 399 | die(); 400 | } 401 | 402 | /** 403 | * Builds error responses. 404 | */ 405 | public function buildErrorResponse($code, $messages, $data = '') 406 | { 407 | switch ($code) { 408 | case 400: 409 | $status = 'Bad Request'; 410 | break; 411 | case 401: 412 | $status = 'Unauthorized'; 413 | break; 414 | case 403: 415 | $status = 'Forbidden'; 416 | break; 417 | case 404: 418 | $status = 'Not Found'; 419 | break; 420 | case 409: 421 | $status = 'Conflict'; 422 | break; 423 | } 424 | $generated = array( 425 | 'status' => $status, 426 | 'code' => $code, 427 | 'messages' => $messages, 428 | 'data' => $data, 429 | ); 430 | $this->response->setStatusCode($code, $status)->sendHeaders(); 431 | $this->response->setContentType('application/json', 'UTF-8'); 432 | $this->response->setJsonContent($generated, JSON_NUMERIC_CHECK)->send(); 433 | die(); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | view->render('index'); 11 | } 12 | 13 | /** 14 | * Private functions 15 | */ 16 | private function checkIfHeadersExist($headers) 17 | { 18 | return (!isset($headers['Authorization']) || empty($headers['Authorization'])) ? $this->buildErrorResponse(403, 'common.HEADER_AUTHORIZATION_NOT_SENT') : true; 19 | } 20 | 21 | private function findUser($credentials) 22 | { 23 | $username = $credentials['username']; 24 | 25 | $conditions = 'username = :username:'; 26 | $parameters = array( 27 | 'username' => $username, 28 | ); 29 | $user = Users::findFirst( 30 | array( 31 | $conditions, 32 | 'bind' => $parameters, 33 | ) 34 | ); 35 | return (!$user) ? $this->buildErrorResponse(404, 'login.USER_IS_NOT_REGISTERED') : $user; 36 | } 37 | 38 | private function getUserPassword($credentials) 39 | { 40 | return $credentials['password']; 41 | } 42 | 43 | private function checkIfUserIsNotBlocked($user) 44 | { 45 | $block_expires = strtotime($user->block_expires); 46 | $now = strtotime($this->getNowDateTime()); 47 | return ($block_expires > $now) ? $this->buildErrorResponse(403, 'login.USER_BLOCKED') : true; 48 | } 49 | 50 | private function checkIfUserIsAuthorized($user) 51 | { 52 | return ($user->authorised == 0) ? $this->buildErrorResponse(403, 'login.USER_UNAUTHORIZED') : true; 53 | } 54 | 55 | private function addOneLoginAttempt($user) 56 | { 57 | $user->login_attempts = $user->login_attempts + 1; 58 | $this->tryToSaveData($user); 59 | return $user->login_attempts; 60 | } 61 | 62 | private function addXMinutesBlockToUser($minutes, $user) 63 | { 64 | $user->block_expires = $this->getNowDateTimePlusMinutes($minutes); 65 | if ($this->tryToSaveData($user)) { 66 | $this->buildErrorResponse(400, 'login.TOO_MANY_FAILED_LOGIN_ATTEMPTS'); 67 | } 68 | } 69 | 70 | private function checkPassword($password, $user) 71 | { 72 | if (!password_verify($password, $user->password)) { 73 | $login_attempts = $this->addOneLoginAttempt($user); 74 | ($login_attempts <= 4) ? $this->buildErrorResponse(400, 'login.WRONG_USER_PASSWORD') : $this->addXMinutesBlockToUser(120, $user); 75 | } 76 | } 77 | 78 | private function checkIfPasswordNeedsRehash($password, $user) 79 | { 80 | $options = [ 81 | 'cost' => 10, // the default cost is 10, max is 12. 82 | ]; 83 | if (password_needs_rehash($user->password, PASSWORD_DEFAULT, $options)) { 84 | $newHash = password_hash($password, PASSWORD_DEFAULT, $options); 85 | $user->password = $newHash; 86 | $this->tryToSaveData($user); 87 | } 88 | } 89 | 90 | private function buildUserData($user) 91 | { 92 | $user_data = array( 93 | 'id' => $user->id, 94 | 'username' => $user->username, 95 | 'email' => $user->email, 96 | 'firstname' => $user->firstname, 97 | 'lastname' => $user->lastname, 98 | ); 99 | return $user_data; 100 | } 101 | 102 | private function buildTokenData($user) 103 | { 104 | // issue at time and expires (token) 105 | $iat = strtotime($this->getNowDateTime()); 106 | $exp = strtotime('+' . $this->tokenConfig['expiration_time'] . ' seconds', $iat); 107 | 108 | $token_data = array( 109 | 'iss' => $this->tokenConfig['iss'], 110 | 'aud' => $this->tokenConfig['aud'], 111 | 'iat' => $iat, 112 | 'exp' => $exp, 113 | 'username_username' => $user->username, 114 | 'username_firstname' => $user->firstname, 115 | 'username_lastname' => $user->lastname, 116 | 'username_level' => $user->level, 117 | 'rand' => rand() . microtime(), 118 | ); 119 | return $token_data; 120 | } 121 | 122 | private function resetLoginAttempts($user) 123 | { 124 | $user->login_attempts = 0; 125 | $this->tryToSaveData($user); 126 | } 127 | 128 | private function registerNewUserAccess($user) 129 | { 130 | $headers = $this->request->getHeaders(); 131 | $newAccess = new UsersAccess(); 132 | $newAccess->username = $user->username; 133 | $newAccess->ip = (isset($headers['Http-Client-Ip']) || !empty($headers['Http-Client-Ip'])) ? $headers['Http-Client-Ip'] : $this->request->getClientAddress(); 134 | $newAccess->domain = (isset($headers['Http-Client-Domain']) || !empty($headers['Http-Client-Domain'])) ? $headers['Http-Client-Domain'] : gethostbyaddr($this->request->getClientAddress()); 135 | $newAccess->country = (isset($headers['Http-Client-Country']) || !empty($headers['Http-Client-Country'])) ? $headers['Http-Client-Country'] : ($this->request->getServer('HTTP_CF_IPCOUNTRY') !== null) ? $this->request->getServer('HTTP_CF_IPCOUNTRY') : 'XX'; 136 | $newAccess->browser = $this->request->getUserAgent(); 137 | $newAccess->date = $this->getNowDateTime(); 138 | $this->tryToSaveData($newAccess); 139 | } 140 | 141 | /** 142 | * Public functions 143 | */ 144 | public function login() 145 | { 146 | $this->initializePost($this->request->getHeaders()); 147 | if ($this->checkIfHeadersExist($this->request->getHeaders())) { 148 | $user = $this->findUser($this->request->getBasicAuth()); 149 | $password = $this->getUserPassword($this->request->getBasicAuth()); 150 | $this->checkIfUserIsNotBlocked($user); 151 | $this->checkIfUserIsAuthorized($user); 152 | $this->checkPassword($password, $user); 153 | 154 | // ALL OK, proceed to login 155 | $this->checkIfPasswordNeedsRehash($password, $user); 156 | $user_data = $this->buildUserData($user); 157 | $token = $this->encodeToken($this->buildTokenData($user)); 158 | 159 | $data = array( 160 | 'token' => $token, 161 | 'user' => $user_data, 162 | ); 163 | 164 | $this->resetLoginAttempts($user); 165 | $this->registerNewUserAccess($user); 166 | $this->buildSuccessResponse(200, 'common.SUCCESSFUL_REQUEST', $data); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | $token->username_username, 13 | ); 14 | $user = Users::findFirst( 15 | array( 16 | $conditions, 17 | 'bind' => $parameters, 18 | ) 19 | ); 20 | if (!$user) { 21 | $this->buildErrorResponse(404, 'profile.PROFILE_NOT_FOUND'); 22 | } 23 | return $user; 24 | } 25 | 26 | private function updateProfile($user, $firstname, $lastname, $email, $phone, $mobile, $address, $birthday) 27 | { 28 | $user->firstname = trim($firstname); 29 | $user->lastname = trim($lastname); 30 | $user->email = trim($email); 31 | $user->phone = trim($phone); 32 | $user->mobile = trim($mobile); 33 | $user->address = trim($address); 34 | $user->birthday = trim($birthday); 35 | $this->tryToSaveData($user, 'profile.PROFILE_COULD_NOT_BE_UPDATED'); 36 | return $user; 37 | } 38 | 39 | private function checkPassword($password, $user) 40 | { 41 | if (!password_verify($password, $user->password)) { 42 | $this->buildErrorResponse(400, 'change-password.WRONG_CURRENT_PASSWORD'); 43 | } 44 | } 45 | 46 | private function setNewPassword($new_password, $user) 47 | { 48 | $user->password = password_hash($new_password, PASSWORD_BCRYPT); 49 | $this->tryToSaveData($user, 'change-password.PASSWORD_COULD_NOT_BE_UPDATED'); 50 | } 51 | 52 | /** 53 | * Public functions 54 | */ 55 | public function index() 56 | { 57 | $this->initializeGet(); 58 | $user = $this->findUser($this->decodeToken($this->getToken()))->toArray(); 59 | $user = $this->unsetPropertyFromArray($user, ['password', 'level', 'authorised', 'block_expires', 'login_attempts']); 60 | $this->buildSuccessResponse(200, 'common.SUCCESSFUL_REQUEST', $user); 61 | } 62 | 63 | public function update() 64 | { 65 | $this->initializePatch(); 66 | $this->checkForEmptyData([$this->request->getPut('firstname'), $this->request->getPut('lastname')]); 67 | $user = $this->updateProfile($this->findUser($this->decodeToken($this->getToken())), $this->request->getPut('firstname'), $this->request->getPut('lastname'), $this->request->getPut('email'), $this->request->getPut('phone'), $this->request->getPut('mobile'), $this->request->getPut('address'), $this->request->getPut('birthday')); 68 | $user = $user->toArray(); 69 | $user = $this->unsetPropertyFromArray($user, ['password', 'level', 'authorised', 'block_expires', 'login_attempts']); 70 | $this->registerLog(); 71 | $this->buildSuccessResponse(200, 'profile.PROFILE_UPDATED', $user); 72 | } 73 | 74 | public function changePassword() 75 | { 76 | $this->initializePatch(); 77 | $this->checkForEmptyData([$this->request->getPut('current_password'), $this->request->getPut('new_password')]); 78 | $user = $this->findUser($this->decodeToken($this->getToken())); 79 | $this->checkPassword($this->request->getPut('current_password'), $user); 80 | $this->setNewPassword($this->request->getPut('new_password'), $user); 81 | $this->registerLog(); 82 | $this->buildSuccessResponse(200, 'change-password.PASSWORD_SUCCESSFULLY_UPDATED'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /controllers/UsersController.php: -------------------------------------------------------------------------------- 1 | buildErrorResponse(409, 'common.COULD_NOT_BE_CREATED'); 13 | } 14 | } 15 | 16 | private function checkIfUsernameAlreadyExists($username) 17 | { 18 | // checks if user already exists 19 | $conditions = 'username = :username:'; 20 | $parameters = array( 21 | 'username' => trim($username), 22 | ); 23 | $user = Users::findFirst( 24 | array( 25 | $conditions, 26 | 'bind' => $parameters, 27 | ) 28 | ); 29 | if ($user) { 30 | $this->buildErrorResponse(409, 'profile.ANOTHER_USER_ALREADY_REGISTERED_WITH_THIS_USERNAME'); 31 | } 32 | } 33 | 34 | private function createUser() 35 | { 36 | $user = new Users(); 37 | $user->email = trim($this->request->getPost('email')); 38 | $user->username = trim($this->request->getPost('username')); 39 | $user->firstname = trim($this->request->getPost('firstname')); 40 | $user->lastname = trim($this->request->getPost('lastname')); 41 | $user->level = trim($this->request->getPost('level')); 42 | $user->phone = trim($this->request->getPost('phone')); 43 | $user->mobile = trim($this->request->getPost('mobile')); 44 | $user->address = trim($this->request->getPost('address')); 45 | $user->city = trim($this->request->getPost('city')); 46 | $user->country = trim($this->request->getPost('country')); 47 | $user->birthday = trim($this->request->getPost('birthday')); 48 | $user->authorised = trim($this->request->getPost('authorised')) || 0; 49 | $user->password = password_hash($this->request->getPost('new_password'), PASSWORD_BCRYPT); 50 | $this->tryToSaveData($user, 'common.COULD_NOT_BE_CREATED'); 51 | return $user; 52 | } 53 | 54 | private function findUserLastAccess($user) 55 | { 56 | $conditions = 'username = :username:'; 57 | $parameters = array( 58 | 'username' => $user['username'], 59 | ); 60 | $last_access = UsersAccess::find( 61 | array( 62 | $conditions, 63 | 'bind' => $parameters, 64 | 'columns' => 'date, ip, domain, browser', 65 | 'order' => 'id DESC', 66 | 'limit' => 10, 67 | ) 68 | ); 69 | if ($last_access) { 70 | $array = array(); 71 | $user_last_access = $last_access->toArray(); 72 | foreach ($user_last_access as $value_last_access) { 73 | $_user_last_access = array( 74 | 'date' => $this->utc_to_iso8601($value_last_access['date']), 75 | 'ip' => $value_last_access['ip'], 76 | 'domain' => $value_last_access['domain'], 77 | 'browser' => $value_last_access['browser'], 78 | ); 79 | $array[] = $_user_last_access; 80 | } 81 | $user = empty($array) ? $this->array_push_assoc($user, 'last_access', '') : $this->array_push_assoc($user, 'last_access', $array); 82 | return $user; 83 | } 84 | } 85 | 86 | private function updateUser($user) 87 | { 88 | $user->email = trim($this->request->getPut('email')); 89 | $user->firstname = trim($this->request->getPut('firstname')); 90 | $user->lastname = trim($this->request->getPut('lastname')); 91 | $user->level = trim($this->request->getPut('level')); 92 | $user->phone = trim($this->request->getPut('phone')); 93 | $user->mobile = trim($this->request->getPut('mobile')); 94 | $user->address = trim($this->request->getPut('address')); 95 | $user->city = trim($this->request->getPut('city')); 96 | $user->country = trim($this->request->getPut('country')); 97 | $user->birthday = trim($this->request->getPut('birthday')); 98 | $user->authorised = trim($this->request->getPut('authorised')) || 0; 99 | $this->tryToSaveData($user, 'common.COULD_NOT_BE_UPDATED'); 100 | return $user; 101 | } 102 | 103 | private function setNewPassword($new_password, $user) 104 | { 105 | $user->password = password_hash($new_password, PASSWORD_BCRYPT); 106 | $this->tryToSaveData($user, 'common.COULD_NOT_BE_UPDATED'); 107 | } 108 | 109 | /** 110 | * Public functions 111 | */ 112 | public function index() 113 | { 114 | $this->initializeGet(); 115 | $options = $this->buildOptions('firstname asc, lastname asc', $this->request->get('sort'), $this->request->get('order'), $this->request->get('limit'), $this->request->get('offset')); 116 | $filters = $this->buildFilters($this->request->get('filter')); 117 | $cities = $this->findElements('Users', $filters['conditions'], $filters['parameters'], 'id, firstname, lastname, level, email, phone, mobile, address, country, city, birthday, authorised', $options['order_by'], $options['offset'], $options['limit']); 118 | $total = $this->calculateTotalElements('Users', $filters['conditions'], $filters['parameters']); 119 | $data = $this->buildListingObject($cities, $options['rows'], $total); 120 | $this->buildSuccessResponse(200, 'common.SUCCESSFUL_REQUEST', $data); 121 | } 122 | 123 | public function create() 124 | { 125 | $this->initializePost(); 126 | $this->checkForEmptyData([$this->request->getPost('username'), $this->request->getPost('firstname'), $this->request->getPost('new_password'), $this->request->getPost('email')]); 127 | $this->checkForbiddenUsername($this->request->getPost('username')); 128 | $this->checkIfUsernameAlreadyExists($this->request->getPost('username')); 129 | $user = $this->createUser(); 130 | $user = $user->toArray(); 131 | $user = $this->unsetPropertyFromArray($user, ['password', 'block_expires', 'login_attempts']); 132 | $this->registerLog(); 133 | $this->buildSuccessResponse(201, 'common.CREATED_SUCCESSFULLY', $user); 134 | } 135 | 136 | public function get($id) 137 | { 138 | $this->initializeGet(); 139 | $user = $this->findElementById('Users', $id); 140 | $user = $user->toArray(); 141 | $user = $this->unsetPropertyFromArray($user, ['password', 'block_expires', 'login_attempts']); 142 | $user = $this->findUserLastAccess($user); 143 | $this->buildSuccessResponse(200, 'common.SUCCESSFUL_REQUEST', $user); 144 | } 145 | 146 | public function update($id) 147 | { 148 | $this->initializePatch(); 149 | $this->checkForEmptyData([$this->request->getPut('firstname'), $this->request->getPut('authorised')]); 150 | $user = $this->updateUser($this->findElementById('Users', $id)); 151 | $user = $user->toArray(); 152 | $user = $this->unsetPropertyFromArray($user, ['password', 'block_expires', 'login_attempts']); 153 | $this->registerLog(); 154 | $this->buildSuccessResponse(200, 'common.UPDATED_SUCCESSFULLY', $user); 155 | } 156 | 157 | public function changePassword($id) 158 | { 159 | $this->initializePatch(); 160 | $this->checkForEmptyData([$this->request->getPut('new_password')]); 161 | $user = $this->findElementById('Users', $id); 162 | $this->setNewPassword($this->request->getPut('new_password'), $user); 163 | $this->registerLog(); 164 | $this->buildSuccessResponse(200, 'change-password.PASSWORD_SUCCESSFULLY_UPDATED'); 165 | } 166 | 167 | public function delete($id) 168 | { 169 | $this->initializeDelete(); 170 | if ($this->tryToDeleteData($this->findElementById('Users', $id))) { 171 | $this->registerLog(); 172 | $this->buildSuccessResponse(200, 'common.DELETED_SUCCESSFULLY'); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /deploy-production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROJECT=myproject 3 | USER=server_username 4 | URL=my_production_server_url 5 | read -p "$(echo $'\n******************************') $(echo $'\n******* API PRODUCTION *******') $(echo $'\n******************************') $(echo $'\nPress Return to Deploy...')"; rsync -avzhe ssh --exclude-from=$HOME/$PROJECT/exclude-production.txt $HOME/$PROJECT/ $USER@$URL:/home/$USER/$URL; read -p "Press Return to Close..." 6 | -------------------------------------------------------------------------------- /deploy-staging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROJECT=myproject 3 | USER=server_username 4 | URL=my_staging_server_url 5 | read -p "$(echo $'\n*****************************') $(echo $'\n******** API STAGING ********') $(echo $'\n*****************************') $(echo $'\nPress Return to Deploy...')"; rsync -avzhe ssh --exclude-from=$HOME/$PROJECT/exclude-staging.txt $HOME/$PROJECT/ $USER@$URL:/home/$USER/$URL; read -p "Press Return to Close..." 6 | -------------------------------------------------------------------------------- /exclude-production.txt: -------------------------------------------------------------------------------- 1 | .git/ 2 | *.DS_Store 3 | .phalcon/ 4 | .php/ 5 | cache/ 6 | .htaccess 7 | exclude-staging.txt 8 | exclude-production.txt 9 | deploy-staging.sh 10 | deploy-production.sh 11 | config/server.development.php 12 | config/server.staging.php 13 | public/.htaccess 14 | schemas/ 15 | -------------------------------------------------------------------------------- /exclude-staging.txt: -------------------------------------------------------------------------------- 1 | .git/ 2 | *.DS_Store 3 | .phalcon/ 4 | .php/ 5 | cache/ 6 | .htaccess 7 | exclude-staging.txt 8 | exclude-production.txt 9 | deploy-staging.sh 10 | deploy-production.sh 11 | config/server.development.php 12 | config/server.production.php 13 | public/.htaccess 14 | schemas/ 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |

Mod-Rewrite is not enabled

Please enable rewrite module on your web server to continue -------------------------------------------------------------------------------- /library/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davellanedam/phalcon-micro-rest-api-skeleton/3c1a9bb4264d3438920e8779dbc7884bf5283a4d/library/.gitkeep -------------------------------------------------------------------------------- /middlewares/AccessMiddleware.php: -------------------------------------------------------------------------------- 1 | getActiveHandler(); 18 | //get the controller for this handler 19 | $array = (array) $arrHandler[0]; 20 | $nameController = implode('', $array); 21 | $controller = str_replace('Controller', '', $nameController); 22 | // check if controller is Index, if it´s Index, then checks if any of functions are called if so return allow 23 | if ($controller === 'Index') { 24 | $allowed = 1; 25 | return $allowed; 26 | } 27 | 28 | // gets user token 29 | $mytoken = $this->getToken(); 30 | 31 | // Verifies Token exists and is not empty 32 | if (empty($mytoken) || $mytoken == '') { 33 | $this->buildErrorResponse(400, 'common.EMPTY_TOKEN_OR_NOT_RECEIVED'); 34 | } 35 | // Verifies Token 36 | try { 37 | $token_decoded = $this->decodeToken($mytoken); 38 | // Verifies User role Access 39 | $allowed_access = $acl->isAllowed($token_decoded->username_level, $controller, $arrHandler[1]); 40 | return (!$allowed_access) ? $this->buildErrorResponse(403, 'common.YOUR_USER_ROLE_DOES_NOT_HAVE_THIS_FEATURE') : $allowed_access; 41 | } catch (Exception $e) { 42 | // BAD TOKEN 43 | $this->buildErrorResponse(401, 'common.BAD_TOKEN_GET_A_NEW_ONE'); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davellanedam/phalcon-micro-rest-api-skeleton/3c1a9bb4264d3438920e8779dbc7884bf5283a4d/migrations/.gitkeep -------------------------------------------------------------------------------- /models/Cities.php: -------------------------------------------------------------------------------- 1 | setConnectionService('db'); 30 | } 31 | 32 | /** 33 | * Returns table name mapped in the model. 34 | * 35 | * @return string 36 | */ 37 | public function getSource() 38 | { 39 | return 'cities'; 40 | } 41 | 42 | /** 43 | * Allows to query a set of records that match the specified conditions 44 | * 45 | * @param mixed $parameters 46 | * @return Cities[] 47 | */ 48 | public static function find($parameters = null) 49 | { 50 | return parent::find($parameters); 51 | } 52 | 53 | /** 54 | * Allows to query the first record that match the specified conditions 55 | * 56 | * @param mixed $parameters 57 | * @return Cities 58 | */ 59 | public static function findFirst($parameters = null) 60 | { 61 | return parent::findFirst($parameters); 62 | } 63 | 64 | /** 65 | * Independent Column Mapping. 66 | * Keys are the real names in the table and the values their names in the application 67 | * 68 | * @return array 69 | */ 70 | public function columnMap() 71 | { 72 | return array( 73 | 'id' => 'id', 74 | 'name' => 'name', 75 | 'country' => 'country', 76 | ); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /models/Logs.php: -------------------------------------------------------------------------------- 1 | setConnectionService('db_log'); // Connection service for log database 36 | } 37 | 38 | /** 39 | * Returns table name mapped in the model. 40 | * 41 | * @return string 42 | */ 43 | public function getSource() 44 | { 45 | return 'logs'; 46 | } 47 | 48 | /** 49 | * Allows to query a set of records that match the specified conditions 50 | * 51 | * @param mixed $parameters 52 | * @return Logs[] 53 | */ 54 | public static function find($parameters = null) 55 | { 56 | return parent::find($parameters); 57 | } 58 | 59 | /** 60 | * Allows to query the first record that match the specified conditions 61 | * 62 | * @param mixed $parameters 63 | * @return Logs 64 | */ 65 | public static function findFirst($parameters = null) 66 | { 67 | return parent::findFirst($parameters); 68 | } 69 | 70 | /** 71 | * Independent Column Mapping. 72 | * Keys are the real names in the table and the values their names in the application 73 | * 74 | * @return array 75 | */ 76 | public function columnMap() 77 | { 78 | return array( 79 | 'id' => 'id', 80 | 'username' => 'username', 81 | 'route' => 'route', 82 | 'date' => 'date', 83 | ); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /models/Users.php: -------------------------------------------------------------------------------- 1 | setConnectionService('db'); 111 | } 112 | 113 | /** 114 | * Validations and business logic 115 | * 116 | * @return boolean 117 | */ 118 | public function validation() 119 | { 120 | $validator = new Validation(); 121 | 122 | $validator->add( 123 | 'email', 124 | new EmailValidator() 125 | ); 126 | 127 | return $this->validate($validator); 128 | } 129 | 130 | /** 131 | * Returns table name mapped in the model. 132 | * 133 | * @return string 134 | */ 135 | public function getSource() 136 | { 137 | return 'users'; 138 | } 139 | 140 | /** 141 | * Allows to query a set of records that match the specified conditions 142 | * 143 | * @param mixed $parameters 144 | * @return Users[] 145 | */ 146 | public static function find($parameters = null) 147 | { 148 | return parent::find($parameters); 149 | } 150 | 151 | /** 152 | * Allows to query the first record that match the specified conditions 153 | * 154 | * @param mixed $parameters 155 | * @return Users 156 | */ 157 | public static function findFirst($parameters = null) 158 | { 159 | return parent::findFirst($parameters); 160 | } 161 | 162 | /** 163 | * Independent Column Mapping. 164 | * Keys are the real names in the table and the values their names in the application 165 | * 166 | * @return array 167 | */ 168 | public function columnMap() 169 | { 170 | return array( 171 | 'id' => 'id', 172 | 'username' => 'username', 173 | 'password' => 'password', 174 | 'firstname' => 'firstname', 175 | 'lastname' => 'lastname', 176 | 'level' => 'level', 177 | 'email' => 'email', 178 | 'phone' => 'phone', 179 | 'mobile' => 'mobile', 180 | 'address' => 'address', 181 | 'country' => 'country', 182 | 'city' => 'city', 183 | 'birthday' => 'birthday', 184 | 'authorised' => 'authorised', 185 | 'block_expires' => 'block_expires', 186 | 'login_attempts' => 'login_attempts', 187 | ); 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /models/UsersAccess.php: -------------------------------------------------------------------------------- 1 | setConnectionService('db'); 54 | } 55 | 56 | /** 57 | * Returns table name mapped in the model. 58 | * 59 | * @return string 60 | */ 61 | public function getSource() 62 | { 63 | return 'users_access'; 64 | } 65 | 66 | /** 67 | * Allows to query a set of records that match the specified conditions 68 | * 69 | * @param mixed $parameters 70 | * @return UsersAccess[] 71 | */ 72 | public static function find($parameters = null) 73 | { 74 | return parent::find($parameters); 75 | } 76 | 77 | /** 78 | * Allows to query the first record that match the specified conditions 79 | * 80 | * @param mixed $parameters 81 | * @return UsersAccess 82 | */ 83 | public static function findFirst($parameters = null) 84 | { 85 | return parent::findFirst($parameters); 86 | } 87 | 88 | /** 89 | * Independent Column Mapping. 90 | * Keys are the real names in the table and the values their names in the application 91 | * 92 | * @return array 93 | */ 94 | public function columnMap() 95 | { 96 | return array( 97 | 'id' => 'id', 98 | 'username' => 'username', 99 | 'ip' => 'ip', 100 | 'domain' => 'domain', 101 | 'country' => 'country', 102 | 'browser' => 'browser', 103 | 'date' => 'date', 104 | ); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | AddDefaultCharset UTF-8 2 | 3 | ############################################################ 4 | # Possible values: development, staging, production # 5 | # Change value and upload ONCE to your server # 6 | # AVOID re-uploading when deployment, things will go crazy # 7 | ############################################################ 8 | SetEnv APPLICATION_ENV "development" 9 | 10 | #CORS 11 | Header always set Access-Control-Allow-Origin: "*" 12 | Header always set Access-Control-Allow-Headers: "Origin, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Authorization, Accept, X-Accept-Charset, X-Accept, Client-Security-Token" 13 | Header always set Access-Control-Allow-Methods: "POST, GET, PUT, OPTIONS, PATCH, DELETE" 14 | Header always set Access-Control-Allow-Credentials: "true" 15 | Header always set Access-Control-Max-Age: "1000" 16 | 17 | #CORS 18 | RewriteCond %{REQUEST_METHOD} OPTIONS 19 | RewriteRule ^(.*)$ $1 [R=200,L] 20 | 21 | #IMPORTANT TO GET JWT FROM HEADERS 22 | RewriteCond %{HTTP:Authorization} ^(.*) 23 | RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] 24 | 25 | 26 | RewriteEngine On 27 | RewriteCond %{REQUEST_FILENAME} !-f 28 | RewriteRule ^(.*)$ index.php?_url=/$1 [QSA,L] 29 | 30 | 31 | 32 | SetOutputFilter DEFLATE 33 | 34 | 35 | 36 | SetOutputFilter DEFLATE 37 | 38 | 39 | 40 | SetOutputFilter DEFLATE 41 | 42 | 43 | 44 | SetOutputFilter DEFLATE 45 | 46 | 47 | 48 | SetOutputFilter DEFLATE 49 | 50 | 51 | 52 | SetOutputFilter DEFLATE 53 | 54 | 55 | 56 | SetOutputFilter DEFLATE 57 | 58 | 59 | 60 | SetOutputFilter DEFLATE 61 | 62 | 63 | 64 | SetOutputFilter DEFLATE 65 | 66 | 67 | 68 | SetOutputFilter DEFLATE 69 | 70 | 71 | 72 | SetOutputFilter DEFLATE 73 | 74 | 75 | 76 | SetOutputFilter DEFLATE 77 | 78 | 79 | 80 | SetOutputFilter DEFLATE 81 | 82 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | listen(); 15 | } 16 | 17 | define('APP_PATH', realpath('..')); 18 | 19 | try { 20 | 21 | require __DIR__ . '/../vendor/autoload.php'; 22 | 23 | /* 24 | * Read the configuration 25 | */ 26 | $config = include __DIR__ . '/../config/config.php'; 27 | 28 | /** 29 | * Include Autoloader. 30 | */ 31 | include APP_PATH . '/config/loader.php'; 32 | 33 | /** 34 | * Include Services. 35 | */ 36 | include APP_PATH . '/config/services.php'; 37 | 38 | /** 39 | * Include ACL. 40 | */ 41 | include APP_PATH . '/config/acl.php'; 42 | 43 | /* 44 | * Starting the application 45 | * Assign service locator to the application 46 | */ 47 | $app = new Micro($di); 48 | 49 | /** 50 | * Include Application. 51 | */ 52 | include APP_PATH . '/app.php'; 53 | 54 | /* 55 | * Handle the request 56 | */ 57 | $app->handle(); 58 | 59 | } catch (\Exception $e) { 60 | if (APPLICATION_ENV === 'development') { 61 | print_r($e->getMessage() . '
'); 62 | print_r('

' . $e->getTraceAsString() . '
'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /schemas/myproject.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Pro SQL dump 3 | # Version 4541 4 | # 5 | # http://www.sequelpro.com/ 6 | # https://github.com/sequelpro/sequelpro 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.6.35) 9 | # Database: myproject 10 | # Generation Time: 2017-09-05 02:02:50 +0000 11 | # ************************************************************ 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 19 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 20 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 21 | 22 | 23 | # Dump of table cities 24 | # ------------------------------------------------------------ 25 | 26 | DROP TABLE IF EXISTS `cities`; 27 | 28 | CREATE TABLE `cities` ( 29 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 30 | `name` varchar(250) DEFAULT NULL, 31 | `country` varchar(250) DEFAULT NULL, 32 | PRIMARY KEY (`id`), 33 | UNIQUE KEY `name` (`name`), 34 | KEY `country` (`country`) 35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 36 | 37 | LOCK TABLES `cities` WRITE; 38 | /*!40000 ALTER TABLE `cities` DISABLE KEYS */; 39 | 40 | INSERT INTO `cities` (`id`, `name`, `country`) 41 | VALUES 42 | (1,'Bogotá','Colombia'), 43 | (2,'Medellín','Colombia'), 44 | (3,'Cali','Colombia'), 45 | (4,'Barranquilla','Colombia'), 46 | (5,'Cartagena','Colombia'), 47 | (6,'Bucaramanga','Colombia'); 48 | 49 | /*!40000 ALTER TABLE `cities` ENABLE KEYS */; 50 | UNLOCK TABLES; 51 | 52 | 53 | # Dump of table users 54 | # ------------------------------------------------------------ 55 | 56 | DROP TABLE IF EXISTS `users`; 57 | 58 | CREATE TABLE `users` ( 59 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 60 | `username` varchar(45) NOT NULL, 61 | `password` varchar(255) NOT NULL, 62 | `firstname` varchar(255) DEFAULT NULL, 63 | `lastname` varchar(255) DEFAULT NULL, 64 | `level` varchar(45) NOT NULL, 65 | `email` varchar(150) DEFAULT NULL, 66 | `phone` varchar(150) DEFAULT NULL, 67 | `mobile` varchar(150) DEFAULT NULL, 68 | `address` text, 69 | `country` varchar(255) DEFAULT NULL, 70 | `city` varchar(255) DEFAULT NULL, 71 | `birthday` date DEFAULT NULL, 72 | `authorised` tinyint(1) DEFAULT '0', 73 | `block_expires` datetime DEFAULT NULL, 74 | `login_attempts` int(10) unsigned DEFAULT '0', 75 | PRIMARY KEY (`id`) USING BTREE, 76 | UNIQUE KEY `username_2` (`username`), 77 | KEY `email` (`email`(100)), 78 | KEY `username` (`username`) USING BTREE, 79 | KEY `level` (`level`) 80 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 81 | 82 | LOCK TABLES `users` WRITE; 83 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 84 | 85 | INSERT INTO `users` (`id`, `username`, `password`, `firstname`, `lastname`, `level`, `email`, `phone`, `mobile`, `address`, `country`, `city`, `birthday`, `authorised`, `block_expires`, `login_attempts`) 86 | VALUES 87 | (1,'admin','$2y$10$QS.T0VwA5/a78y2s6m2Yiulu2bhGPzH/W1ay7QslX830PO4RTCVj.','Super','Admin','Superuser','my@email.com','123123','123123123','My address','Colombia','Bucaramanga','1980-01-01',1,NULL,0); 88 | 89 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 90 | UNLOCK TABLES; 91 | 92 | 93 | # Dump of table users_access 94 | # ------------------------------------------------------------ 95 | 96 | DROP TABLE IF EXISTS `users_access`; 97 | 98 | CREATE TABLE `users_access` ( 99 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 100 | `username` varchar(45) DEFAULT NULL, 101 | `ip` varchar(250) DEFAULT NULL, 102 | `browser` text, 103 | `date` datetime DEFAULT NULL, 104 | `domain` varchar(255) DEFAULT NULL, 105 | `country` varchar(11) DEFAULT NULL, 106 | PRIMARY KEY (`id`) 107 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 108 | 109 | 110 | 111 | 112 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 113 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 114 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 115 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 116 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 117 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 118 | -------------------------------------------------------------------------------- /schemas/myproject_log.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Pro SQL dump 3 | # Version 4541 4 | # 5 | # http://www.sequelpro.com/ 6 | # https://github.com/sequelpro/sequelpro 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.6.35) 9 | # Database: myproject_log 10 | # Generation Time: 2017-09-05 02:02:58 +0000 11 | # ************************************************************ 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 19 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 20 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 21 | 22 | 23 | # Dump of table logs 24 | # ------------------------------------------------------------ 25 | 26 | DROP TABLE IF EXISTS `logs`; 27 | 28 | CREATE TABLE `logs` ( 29 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 30 | `username` varchar(45) DEFAULT NULL, 31 | `route` varchar(255) DEFAULT NULL, 32 | `date` datetime DEFAULT NULL, 33 | PRIMARY KEY (`id`) 34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 35 | 36 | 37 | 38 | 39 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 40 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 41 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 42 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 43 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 44 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 45 | -------------------------------------------------------------------------------- /tests/0IndexTest.php: -------------------------------------------------------------------------------- 1 | http = new GuzzleHttp\Client(['base_uri' => 'http://api.myproject.local/', 'http_errors' => false]); 12 | } 13 | 14 | public function tearDown() 15 | { 16 | $this->http = null; 17 | } 18 | 19 | protected function postLogin($credentials) 20 | { 21 | $response = $this->http->request('POST', 'authenticate', [ 22 | 'headers' => [ 23 | 'Authorization' => 'Basic ' . base64_encode($credentials), 24 | ]]); 25 | return $response; 26 | } 27 | 28 | protected function getJSON($response) 29 | { 30 | $contentType = $response->getHeaders()["Content-Type"][0]; 31 | $this->assertEquals("application/json; charset=UTF-8", $contentType); 32 | $json = json_decode($response->getBody(), true); 33 | return $json; 34 | } 35 | 36 | public function testLogin() 37 | { 38 | $response = $this->postLogin('admin:admin1234'); 39 | $this->assertEquals(200, $response->getStatusCode()); 40 | $json = $this->getJSON($response); 41 | $this->assertEquals('common.SUCCESSFUL_REQUEST', $json['messages']); 42 | $this->assertEquals('1', $json['data']['user']['id']); 43 | $this->assertEquals('admin', $json['data']['user']['username']); 44 | } 45 | 46 | public function testLoginUserNotRegistered() 47 | { 48 | $response = $this->postLogin('admin1234:admin1234'); 49 | $this->assertEquals(404, $response->getStatusCode()); 50 | $json = $this->getJSON($response); 51 | $this->assertEquals('login.USER_IS_NOT_REGISTERED', $json['messages']); 52 | } 53 | 54 | public function testLoginWrongPassword() 55 | { 56 | $response = $this->postLogin('admin:admin12345'); 57 | $this->assertEquals(400, $response->getStatusCode()); 58 | $json = $this->getJSON($response); 59 | $this->assertEquals('login.WRONG_USER_PASSWORD', $json['messages']); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/1CitiesTest.php: -------------------------------------------------------------------------------- 1 | http = new GuzzleHttp\Client(['base_uri' => 'http://api.myproject.local/', 'http_errors' => false]); 12 | $this->credentials = $this->getToken(); 13 | } 14 | 15 | public function tearDown() 16 | { 17 | $this->http = null; 18 | } 19 | 20 | protected function getJSON($response) 21 | { 22 | $contentType = $response->getHeaders()["Content-Type"][0]; 23 | $this->assertEquals("application/json; charset=UTF-8", $contentType); 24 | $json = json_decode($response->getBody(), true); 25 | return $json; 26 | } 27 | 28 | protected function getToken() 29 | { 30 | $credentials = base64_encode('admin:admin1234'); 31 | 32 | $response = $this->http->request('POST', 'authenticate', [ 33 | 'headers' => [ 34 | 'Authorization' => 'Basic ' . $credentials, 35 | ]]); 36 | 37 | $this->assertEquals(200, $response->getStatusCode()); 38 | $json = $this->getJSON($response); 39 | $this->assertEquals('common.SUCCESSFUL_REQUEST', $json['messages']); 40 | $this->assertEquals('1', $json['data']['user']['id']); 41 | $this->assertEquals('admin', $json['data']['user']['username']); 42 | return $json['data']['token']; 43 | } 44 | 45 | protected function buildGetRequest($endpoint) 46 | { 47 | $response = $this->http->request('GET', $endpoint, [ 48 | 'headers' => [ 49 | 'Authorization' => 'Bearer ' . $this->credentials, 50 | ]]); 51 | return $response; 52 | } 53 | 54 | protected function buildGetOrDeleteRequestById($method, $endpoint, $id) 55 | { 56 | $response = $this->http->request($method, $endpoint . $id, [ 57 | 'headers' => [ 58 | 'Authorization' => 'Bearer ' . $this->credentials, 59 | ]]); 60 | return $response; 61 | } 62 | 63 | protected function buildPostRequest($endpoint, $params) 64 | { 65 | $response = $this->http->request('POST', $endpoint, [ 66 | 'headers' => [ 67 | 'Authorization' => 'Bearer ' . $this->credentials, 68 | ], 69 | 'form_params' => $params]); 70 | return $response; 71 | } 72 | 73 | protected function buildPatchRequest($endpoint, $params, $id) 74 | { 75 | $response = $this->http->request('PATCH', $endpoint . $id, [ 76 | 'headers' => [ 77 | 'Authorization' => 'Bearer ' . $this->credentials, 78 | ], 79 | 'form_params' => $params]); 80 | return $response; 81 | } 82 | 83 | protected function deleteItem($id) 84 | { 85 | $response = $this->buildGetOrDeleteRequestById('DELETE', 'cities/delete/', $id); 86 | $this->assertEquals(200, $response->getStatusCode()); 87 | $json = $this->getJSON($response); 88 | $this->assertEquals('common.DELETED_SUCCESSFULLY', $json['messages']); 89 | } 90 | 91 | protected function createItem() 92 | { 93 | $params = [ 94 | 'name' => 'Girón', 95 | 'country' => 'Colombia', 96 | ]; 97 | $response = $this->buildPostRequest('cities/create', $params); 98 | $this->assertEquals(201, $response->getStatusCode()); 99 | $json = $this->getJSON($response); 100 | $this->assertEquals('common.CREATED_SUCCESSFULLY', $json['messages']); 101 | $id = $json['data']['id']; 102 | return $id; 103 | } 104 | 105 | public function testGetCities() 106 | { 107 | $response = $this->buildGetRequest('cities'); 108 | $this->assertEquals(200, $response->getStatusCode()); 109 | $json = $this->getJSON($response); 110 | $this->assertEquals('common.SUCCESSFUL_REQUEST', $json['messages']); 111 | } 112 | 113 | public function testCreateCity() 114 | { 115 | $id = $this->createItem(); 116 | $this->deleteItem($id); 117 | } 118 | 119 | public function testCannotCreateDuplicatedCity() 120 | { 121 | $params = [ 122 | 'name' => 'Bogotá', 123 | 'country' => 'Colombia', 124 | ]; 125 | $response = $this->buildPostRequest('cities/create', $params); 126 | $this->assertEquals(409, $response->getStatusCode()); 127 | $json = $this->getJSON($response); 128 | $this->assertEquals('common.THERE_IS_ALREADY_A_RECORD_WITH_THAT_NAME', $json['messages']); 129 | } 130 | 131 | public function testGetCityById() 132 | { 133 | $response = $this->buildGetOrDeleteRequestById('GET', 'cities/get/', 1); 134 | $this->assertEquals(200, $response->getStatusCode()); 135 | $json = $this->getJSON($response); 136 | $this->assertEquals('Bogotá', $json['data']['name']); 137 | } 138 | 139 | public function testUpdateCity() 140 | { 141 | $id = $this->createItem(); 142 | $params = [ 143 | 'name' => 'Girón2', 144 | 'country' => 'Colombia2', 145 | ]; 146 | $response = $this->buildPatchRequest('cities/update/', $params, $id); 147 | $this->assertEquals(200, $response->getStatusCode()); 148 | $json = $this->getJSON($response); 149 | $this->assertEquals('Girón2', $json['data']['name']); 150 | $this->deleteItem($id); 151 | } 152 | 153 | public function testCannotUpdateCityThatAlreadyExists() 154 | { 155 | $id = $this->createItem(); 156 | $params = [ 157 | 'name' => 'Bogotá', 158 | 'country' => 'Colombia', 159 | ]; 160 | $response = $this->buildPatchRequest('cities/update/', $params, $id); 161 | $this->assertEquals(409, $response->getStatusCode()); 162 | $json = $this->getJSON($response); 163 | $this->assertEquals('common.THERE_IS_ALREADY_A_RECORD_WITH_THAT_NAME', $json['messages']); 164 | $this->deleteItem($id); 165 | } 166 | 167 | public function testDeleteCity() 168 | { 169 | $id = $this->createItem(); 170 | $this->deleteItem($id); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/2UsersTest.php: -------------------------------------------------------------------------------- 1 | http = new GuzzleHttp\Client(['base_uri' => 'http://api.myproject.local/', 'http_errors' => false]); 12 | $this->credentials = $this->getToken(); 13 | } 14 | 15 | public function tearDown() 16 | { 17 | $this->http = null; 18 | } 19 | 20 | protected function getJSON($response) 21 | { 22 | $contentType = $response->getHeaders()["Content-Type"][0]; 23 | $this->assertEquals("application/json; charset=UTF-8", $contentType); 24 | $json = json_decode($response->getBody(), true); 25 | return $json; 26 | } 27 | 28 | protected function getToken() 29 | { 30 | $credentials = base64_encode('admin:admin1234'); 31 | 32 | $response = $this->http->request('POST', 'authenticate', [ 33 | 'headers' => [ 34 | 'Authorization' => 'Basic ' . $credentials, 35 | ]]); 36 | 37 | $this->assertEquals(200, $response->getStatusCode()); 38 | $json = $this->getJSON($response); 39 | $this->assertEquals('common.SUCCESSFUL_REQUEST', $json['messages']); 40 | $this->assertEquals('1', $json['data']['user']['id']); 41 | $this->assertEquals('admin', $json['data']['user']['username']); 42 | return $json['data']['token']; 43 | } 44 | 45 | protected function buildGetRequest($endpoint) 46 | { 47 | $response = $this->http->request('GET', $endpoint, [ 48 | 'headers' => [ 49 | 'Authorization' => 'Bearer ' . $this->credentials, 50 | ]]); 51 | return $response; 52 | } 53 | 54 | protected function buildGetOrDeleteRequestById($method, $endpoint, $id) 55 | { 56 | $response = $this->http->request($method, $endpoint . $id, [ 57 | 'headers' => [ 58 | 'Authorization' => 'Bearer ' . $this->credentials, 59 | ]]); 60 | return $response; 61 | } 62 | 63 | protected function buildPostRequest($endpoint, $params) 64 | { 65 | $response = $this->http->request('POST', $endpoint, [ 66 | 'headers' => [ 67 | 'Authorization' => 'Bearer ' . $this->credentials, 68 | ], 69 | 'form_params' => $params]); 70 | return $response; 71 | } 72 | 73 | protected function buildPatchRequest($endpoint, $params, $id) 74 | { 75 | $response = $this->http->request('PATCH', $endpoint . $id, [ 76 | 'headers' => [ 77 | 'Authorization' => 'Bearer ' . $this->credentials, 78 | ], 79 | 'form_params' => $params]); 80 | return $response; 81 | } 82 | 83 | protected function deleteItem($id) 84 | { 85 | $response = $this->buildGetOrDeleteRequestById('DELETE', 'users/delete/', $id); 86 | $this->assertEquals(200, $response->getStatusCode()); 87 | $json = $this->getJSON($response); 88 | $this->assertEquals('common.DELETED_SUCCESSFULLY', $json['messages']); 89 | } 90 | 91 | protected function createItem() 92 | { 93 | $params = [ 94 | 'email' => 'another@email.com', 95 | 'new_password' => 'test123', 96 | 'username' => 'admintest', 97 | 'firstname' => 'My name', 98 | 'lastname' => 'My last name', 99 | 'level' => 'Superuser', 100 | 'phone' => '12312312', 101 | 'mobile' => '31312312', 102 | 'address' => 'Calle 10', 103 | 'city' => 'Bogotá', 104 | 'country' => 'Colombia', 105 | 'birthday' => '1979-01-01', 106 | 'authorised' => '1', 107 | ]; 108 | $response = $this->buildPostRequest('users/create', $params); 109 | $this->assertEquals(201, $response->getStatusCode()); 110 | $json = $this->getJSON($response); 111 | $this->assertEquals('common.CREATED_SUCCESSFULLY', $json['messages']); 112 | $id = $json['data']['id']; 113 | return $id; 114 | } 115 | 116 | public function testGetUsers() 117 | { 118 | $response = $this->buildGetRequest('users'); 119 | $this->assertEquals(200, $response->getStatusCode()); 120 | $json = $this->getJSON($response); 121 | $this->assertEquals('common.SUCCESSFUL_REQUEST', $json['messages']); 122 | } 123 | 124 | public function testCreateUser() 125 | { 126 | $id = $this->createItem(); 127 | $this->deleteItem($id); 128 | } 129 | 130 | public function testCannotCreateDuplicatedUser() 131 | { 132 | $id = $this->createItem(); 133 | $params = [ 134 | 'email' => 'another@email.com', 135 | 'new_password' => 'test123', 136 | 'username' => 'admintest', 137 | 'firstname' => 'My name', 138 | 'lastname' => 'My last name', 139 | 'level' => 'Superuser', 140 | 'phone' => '12312312', 141 | 'mobile' => '31312312', 142 | 'address' => 'Calle 10', 143 | 'city' => 'Bogotá', 144 | 'country' => 'Colombia', 145 | 'birthday' => '1979-01-01', 146 | 'authorised' => '1', 147 | ]; 148 | $response = $this->buildPostRequest('users/create', $params); 149 | $this->assertEquals(409, $response->getStatusCode()); 150 | $json = $this->getJSON($response); 151 | $this->assertEquals('profile.ANOTHER_USER_ALREADY_REGISTERED_WITH_THIS_USERNAME', $json['messages']); 152 | $this->deleteItem($id); 153 | } 154 | 155 | public function testGetUserById() 156 | { 157 | $response = $this->buildGetOrDeleteRequestById('GET', 'users/get/', 1); 158 | $this->assertEquals(200, $response->getStatusCode()); 159 | $json = $this->getJSON($response); 160 | $this->assertEquals('1', $json['data']['id']); 161 | } 162 | 163 | public function testUpdateUser() 164 | { 165 | $id = $this->createItem(); 166 | $params = [ 167 | 'email' => 'onemore@email.com', 168 | 'new_password' => 'test123', 169 | 'username' => 'admintest', 170 | 'firstname' => 'Other name', 171 | 'lastname' => 'Other last name', 172 | 'level' => 'Superuser', 173 | 'phone' => '7654333', 174 | 'mobile' => '234568', 175 | 'address' => 'Calle 11', 176 | 'city' => 'Bogotá', 177 | 'country' => 'Colombia', 178 | 'birthday' => '1979-01-01', 179 | 'authorised' => '1', 180 | ]; 181 | $response = $this->buildPatchRequest('users/update/', $params, $id); 182 | $this->assertEquals(200, $response->getStatusCode()); 183 | $json = $this->getJSON($response); 184 | $this->assertEquals('onemore@email.com', $json['data']['email']); 185 | $this->deleteItem($id); 186 | } 187 | 188 | public function testDeleteUser() 189 | { 190 | $id = $this->createItem(); 191 | $this->deleteItem($id); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/3ProfileTest.php: -------------------------------------------------------------------------------- 1 | http = new GuzzleHttp\Client(['base_uri' => 'http://api.myproject.local/', 'http_errors' => false]); 12 | $this->credentials = $this->getToken(); 13 | } 14 | 15 | public function tearDown() 16 | { 17 | $this->http = null; 18 | } 19 | 20 | protected function getJSON($response) 21 | { 22 | $contentType = $response->getHeaders()["Content-Type"][0]; 23 | $this->assertEquals("application/json; charset=UTF-8", $contentType); 24 | $json = json_decode($response->getBody(), true); 25 | return $json; 26 | } 27 | 28 | protected function getToken() 29 | { 30 | $credentials = base64_encode('admin:admin1234'); 31 | 32 | $response = $this->http->request('POST', 'authenticate', [ 33 | 'headers' => [ 34 | 'Authorization' => 'Basic ' . $credentials, 35 | ]]); 36 | 37 | $this->assertEquals(200, $response->getStatusCode()); 38 | $json = $this->getJSON($response); 39 | $this->assertEquals('common.SUCCESSFUL_REQUEST', $json['messages']); 40 | $this->assertEquals('1', $json['data']['user']['id']); 41 | $this->assertEquals('admin', $json['data']['user']['username']); 42 | return $json['data']['token']; 43 | } 44 | 45 | protected function buildGetRequest($endpoint) 46 | { 47 | $response = $this->http->request('GET', $endpoint, [ 48 | 'headers' => [ 49 | 'Authorization' => 'Bearer ' . $this->credentials, 50 | ]]); 51 | return $response; 52 | } 53 | 54 | protected function buildPatchRequest($endpoint, $params) 55 | { 56 | $response = $this->http->request('PATCH', $endpoint, [ 57 | 'headers' => [ 58 | 'Authorization' => 'Bearer ' . $this->credentials, 59 | ], 60 | 'form_params' => $params]); 61 | return $response; 62 | } 63 | 64 | public function testGetProfile() 65 | { 66 | $response = $this->buildGetRequest('profile'); 67 | $this->assertEquals(200, $response->getStatusCode()); 68 | $json = $this->getJSON($response); 69 | $this->assertEquals('1', $json['data']['id']); 70 | $this->assertEquals('admin', $json['data']['username']); 71 | } 72 | 73 | public function testUpdateProfile() 74 | { 75 | $params = [ 76 | 'email' => 'asd@asd.com', 77 | 'firstname' => 'Other name', 78 | 'lastname' => 'Lastname', 79 | 'phone' => '12312312', 80 | 'mobile' => '31312312', 81 | 'address' => 'Calle 10', 82 | 'city' => 'Bogotá', 83 | 'country' => 'Colombia', 84 | 'birthday' => '1979-01-01', 85 | ]; 86 | $response = $this->buildPatchRequest('profile/update', $params); 87 | $this->assertEquals(200, $response->getStatusCode()); 88 | $json = $this->getJSON($response); 89 | $this->assertEquals('1', $json['data']['id']); 90 | $this->assertEquals('admin', $json['data']['username']); 91 | $this->assertEquals('Other name', $json['data']['firstname']); 92 | } 93 | 94 | public function testChangePassword() 95 | { 96 | $params = [ 97 | 'current_password' => 'admin1234', 98 | 'new_password' => 'admin12345', 99 | ]; 100 | $response = $this->buildPatchRequest('profile/change-password', $params); 101 | $this->assertEquals(200, $response->getStatusCode()); 102 | $json = $this->getJSON($response); 103 | $this->assertEquals('change-password.PASSWORD_SUCCESSFULLY_UPDATED', $json['messages']); 104 | 105 | // restore default password 106 | $params = [ 107 | 'current_password' => 'admin12345', 108 | 'new_password' => 'admin1234', 109 | ]; 110 | $response = $this->buildPatchRequest('profile/change-password', $params); 111 | $this->assertEquals(200, $response->getStatusCode()); 112 | $json = $this->getJSON($response); 113 | $this->assertEquals('change-password.PASSWORD_SUCCESSFULLY_UPDATED', $json['messages']); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /views/404.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | API 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 |

Sorry, requested page does not exists.

18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /views/index.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | API 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 |

This is our API

18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------