├── .env.example ├── .env.travis ├── .gitignore ├── .travis.yml ├── Vagrantfile ├── after.sh ├── aliases ├── app ├── Console │ ├── Commands │ │ ├── .gitkeep │ │ └── CreateAuthenticationKeyCommand.php │ └── Kernel.php ├── Events │ ├── Event.php │ └── ExampleEvent.php ├── Exceptions │ └── Handler.php ├── Extensions │ └── Authentication │ │ └── PasetoAuthGuard.php ├── Http │ ├── Controllers │ │ ├── AuthController.php │ │ └── Controller.php │ ├── Middleware │ │ ├── Authenticate.php │ │ └── CorsMiddleware.php │ └── Requests │ │ ├── LoginRequest.php │ │ └── RegisterRequest.php ├── Jobs │ ├── ExampleJob.php │ └── Job.php ├── Listeners │ └── ExampleListener.php ├── Mail │ ├── PasswordReset.php │ └── Welcome.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthenticationProvider.php │ └── EventServiceProvider.php └── User.php ├── artisan ├── bootstrap └── app.php ├── composer.json ├── composer.lock ├── config ├── apidoc.php ├── auth.php ├── constants.php ├── mail.php └── permission.php ├── database ├── factories │ └── ModelFactory.php ├── migrations │ ├── .gitkeep │ ├── 2018_01_01_000000_create_permission_tables.php │ ├── 2018_03_03_232931_create_users_table.php │ └── 2018_03_03_232948_create_password_resets_table.php └── seeds │ ├── DatabaseSeeder.php │ └── RolesSeeder.php ├── helpers.php ├── phpunit-printer.yml ├── phpunit.xml ├── public ├── .htaccess └── index.php ├── readme.md ├── resources ├── lang │ └── en │ │ ├── messages.php │ │ └── validation.php └── views │ └── emails │ ├── password-reset.blade.php │ └── welcome.blade.php ├── routes └── api.php ├── storage ├── app │ └── .gitignore ├── clockwork │ └── .gitignore ├── framework │ ├── cache │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore └── tests ├── AuthenticationTest.php └── TestCase.php /.env.example: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_DEBUG=true 3 | APP_KEY= 4 | APP_TIMEZONE=Europe/Berlin 5 | 6 | PASETO_AUTH_KEY= 7 | PASETO_AUTH_EXPIRE_AFTER_HOURS=12 8 | PASETO_AUTH_ISSUER=api 9 | 10 | LOG_CHANNEL=stack 11 | LOG_SLACK_WEBHOOK_URL= 12 | 13 | DB_CONNECTION=mysql 14 | DB_HOST=localhost 15 | DB_PORT=3306 16 | DB_DATABASE=homestead 17 | DB_USERNAME=homestead 18 | DB_PASSWORD=secret 19 | 20 | CACHE_DRIVER=file 21 | QUEUE_DRIVER=sync 22 | 23 | ALLOWED_ORIGIN=* 24 | 25 | APP_LOCALE=en 26 | 27 | WEBSITE_NAME= 28 | WEBSITE_EMAIL= 29 | FRONTEND_URL= 30 | -------------------------------------------------------------------------------- /.env.travis: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_DEBUG=true 3 | APP_KEY= 4 | APP_TIMEZONE=Europe/Berlin 5 | 6 | PASETO_AUTH_KEY= 7 | PASETO_AUTH_EXPIRE_AFTER_HOURS=12 8 | PASETO_AUTH_ISSUER=api 9 | 10 | LOG_CHANNEL=stack 11 | LOG_SLACK_WEBHOOK_URL= 12 | 13 | DB_CONNECTION=mysql 14 | DB_HOST=localhost 15 | DB_PORT=3306 16 | DB_DATABASE=homestead 17 | DB_USERNAME=root 18 | DB_PASSWORD= 19 | 20 | CACHE_DRIVER=file 21 | QUEUE_DRIVER=sync 22 | 23 | ALLOWED_ORIGIN=* 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | Homestead.json 4 | Homestead.yaml 5 | .env 6 | /database/database.sqlite 7 | /.vagrant/ 8 | /.phpstorm.meta.php 9 | /_ide_helper.php 10 | /_ide_helper_models.php 11 | /public/docs/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | 5 | php: 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | - 7.4snapshot 10 | 11 | branches: 12 | only: 13 | master 14 | 15 | sudo: true 16 | 17 | cache: 18 | directories: 19 | - $HOME/.composer/cache 20 | 21 | addons: 22 | apt: 23 | sources: 24 | - mysql-5.7-trusty 25 | packages: 26 | - mysql-server 27 | - mysql-client 28 | 29 | before_install: 30 | - mysql -e 'CREATE DATABASE IF NOT EXISTS homestead DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;' 31 | 32 | install: 33 | - travis_retry composer install --no-interaction --prefer-source 34 | - cp .env.travis .env 35 | - composer keys 36 | - php artisan migrate 37 | 38 | 39 | script: vendor/bin/phpunit 40 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | require 'json' 5 | require 'yaml' 6 | 7 | VAGRANTFILE_API_VERSION ||= "2" 8 | confDir = $confDir ||= File.expand_path("vendor/laravel/homestead", File.dirname(__FILE__)) 9 | 10 | homesteadYamlPath = File.expand_path("Homestead.yaml", File.dirname(__FILE__)) 11 | homesteadJsonPath = File.expand_path("Homestead.json", File.dirname(__FILE__)) 12 | afterScriptPath = "after.sh" 13 | customizationScriptPath = "user-customizations.sh" 14 | aliasesPath = "aliases" 15 | 16 | require File.expand_path(confDir + '/scripts/homestead.rb') 17 | 18 | Vagrant.require_version '>= 1.9.0' 19 | 20 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 21 | if File.exist? aliasesPath then 22 | config.vm.provision "file", source: aliasesPath, destination: "/tmp/bash_aliases" 23 | config.vm.provision "shell" do |s| 24 | s.inline = "awk '{ sub(\"\r$\", \"\"); print }' /tmp/bash_aliases > /home/vagrant/.bash_aliases" 25 | end 26 | end 27 | 28 | if File.exist? homesteadYamlPath then 29 | settings = YAML::load(File.read(homesteadYamlPath)) 30 | elsif File.exist? homesteadJsonPath then 31 | settings = JSON.parse(File.read(homesteadJsonPath)) 32 | else 33 | abort "Homestead settings file not found in " + File.dirname(__FILE__) 34 | end 35 | 36 | Homestead.configure(config, settings) 37 | 38 | if File.exist? afterScriptPath then 39 | config.vm.provision "shell", path: afterScriptPath, privileged: false, keep_color: true 40 | end 41 | 42 | if File.exist? customizationScriptPath then 43 | config.vm.provision "shell", path: customizationScriptPath, privileged: false, keep_color: true 44 | end 45 | 46 | if defined? VagrantPlugins::HostsUpdater 47 | config.hostsupdater.aliases = settings['sites'].map { |site| site['map'] } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /after.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # If you would like to do some extra provisioning you may 4 | # add any commands you wish to this file and they will 5 | # be run after the Homestead machine is provisioned. 6 | -------------------------------------------------------------------------------- /aliases: -------------------------------------------------------------------------------- 1 | alias ..="cd .." 2 | alias ...="cd ../.." 3 | 4 | alias h='cd ~' 5 | alias c='clear' 6 | alias art=artisan 7 | 8 | alias phpspec='vendor/bin/phpspec' 9 | alias phpunit='vendor/bin/phpunit' 10 | alias serve=serve-laravel 11 | 12 | alias xoff='sudo phpdismod -s cli xdebug' 13 | alias xon='sudo phpenmod -s cli xdebug' 14 | 15 | function artisan() { 16 | php artisan "$@" 17 | } 18 | 19 | function dusk() { 20 | pids=$(pidof /usr/bin/Xvfb) 21 | 22 | if [ ! -n "$pids" ]; then 23 | Xvfb :0 -screen 0 1280x960x24 & 24 | fi 25 | 26 | php artisan dusk "$@" 27 | } 28 | 29 | function php56() { 30 | sudo update-alternatives --set php /usr/bin/php5.6 31 | } 32 | 33 | function php70() { 34 | sudo update-alternatives --set php /usr/bin/php7.0 35 | } 36 | 37 | function php71() { 38 | sudo update-alternatives --set php /usr/bin/php7.1 39 | } 40 | 41 | function php72() { 42 | sudo update-alternatives --set php /usr/bin/php7.2 43 | } 44 | 45 | function serve-apache() { 46 | if [[ "$1" && "$2" ]] 47 | then 48 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 49 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-apache.sh 50 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-apache.sh "$1" "$2" 80 443 "${3:-7.1}" 51 | else 52 | echo "Error: missing required parameters." 53 | echo "Usage: " 54 | echo " serve-apache domain path" 55 | fi 56 | } 57 | 58 | function serve-laravel() { 59 | if [[ "$1" && "$2" ]] 60 | then 61 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 62 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-laravel.sh 63 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-laravel.sh "$1" "$2" 80 443 "${3:-7.1}" 64 | else 65 | echo "Error: missing required parameters." 66 | echo "Usage: " 67 | echo " serve domain path" 68 | fi 69 | } 70 | 71 | function serve-proxy() { 72 | if [[ "$1" && "$2" ]] 73 | then 74 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-proxy.sh 75 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-proxy.sh "$1" "$2" 80 443 "${3:-7.1}" 76 | else 77 | echo "Error: missing required parameters." 78 | echo "Usage: " 79 | echo " serve-proxy domain port" 80 | fi 81 | } 82 | 83 | function serve-silverstripe() { 84 | if [[ "$1" && "$2" ]] 85 | then 86 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 87 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-silverstripe.sh 88 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-silverstripe.sh "$1" "$2" 80 443 "${3:-7.1}" 89 | else 90 | echo "Error: missing required parameters." 91 | echo "Usage: " 92 | echo " serve-silverstripe domain path" 93 | fi 94 | } 95 | 96 | function serve-spa() { 97 | if [[ "$1" && "$2" ]] 98 | then 99 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 100 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-spa.sh 101 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-spa.sh "$1" "$2" 80 443 "${3:-7.1}" 102 | else 103 | echo "Error: missing required parameters." 104 | echo "Usage: " 105 | echo " serve-spa domain path" 106 | fi 107 | } 108 | 109 | function serve-statamic() { 110 | if [[ "$1" && "$2" ]] 111 | then 112 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 113 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-statamic.sh 114 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-statamic.sh "$1" "$2" 80 443 "${3:-7.1}" 115 | else 116 | echo "Error: missing required parameters." 117 | echo "Usage: " 118 | echo " serve-statamic domain path" 119 | fi 120 | } 121 | 122 | function serve-symfony2() { 123 | if [[ "$1" && "$2" ]] 124 | then 125 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 126 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-symfony2.sh 127 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-symfony2.sh "$1" "$2" 80 443 "${3:-7.1}" 128 | else 129 | echo "Error: missing required parameters." 130 | echo "Usage: " 131 | echo " serve-symfony2 domain path" 132 | fi 133 | } 134 | 135 | function serve-symfony4() { 136 | if [[ "$1" && "$2" ]] 137 | then 138 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 139 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-symfony4.sh 140 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-symfony4.sh "$1" "$2" 80 443 "${3:-7.1}" 141 | else 142 | echo "Error: missing required parameters." 143 | echo "Usage: " 144 | echo " serve-symfony4 domain path" 145 | fi 146 | } 147 | 148 | function serve-pimcore() { 149 | if [[ "$1" && "$2" ]] 150 | then 151 | sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" 152 | sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-pimcore.sh 153 | sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-pimcore.sh "$1" "$2" 80 443 "${3:-7.1}" 154 | else 155 | echo "Error: missing required parameters." 156 | echo "Usage: " 157 | echo " serve-pimcore domain path" 158 | fi 159 | } 160 | 161 | function share() { 162 | if [[ "$1" ]] 163 | then 164 | ngrok http ${@:2} -host-header="$1" 80 165 | else 166 | echo "Error: missing required parameters." 167 | echo "Usage: " 168 | echo " share domain" 169 | echo "Invocation with extra params passed directly to ngrok" 170 | echo " share domain -region=eu -subdomain=test1234" 171 | fi 172 | } 173 | 174 | function flip() { 175 | sudo bash /vagrant/vendor/laravel/homestead/scripts/flip-webserver.sh 176 | } 177 | 178 | function __has_pv() { 179 | $(hash pv 2>/dev/null); 180 | 181 | return $? 182 | } 183 | 184 | function __pv_install_message() { 185 | if ! __has_pv; then 186 | echo $1 187 | echo "Install pv with \`sudo apt-get install -y pv\` then run this command again." 188 | echo "" 189 | fi 190 | } 191 | 192 | function dbexport() { 193 | FILE=${1:-/vagrant/mysqldump.sql.gz} 194 | 195 | # This gives an estimate of the size of the SQL file 196 | # It appears that 80% is a good approximation of 197 | # the ratio of estimated size to actual size 198 | SIZE_QUERY="select ceil(sum(data_length) * 0.8) as size from information_schema.TABLES" 199 | 200 | __pv_install_message "Want to see export progress?" 201 | 202 | echo "Exporting databases to '$FILE'" 203 | 204 | if __has_pv; then 205 | ADJUSTED_SIZE=$(mysql --vertical -uhomestead -psecret -e "$SIZE_QUERY" 2>/dev/null | grep 'size' | awk '{print $2}') 206 | HUMAN_READABLE_SIZE=$(numfmt --to=iec-i --suffix=B --format="%.3f" $ADJUSTED_SIZE) 207 | 208 | echo "Estimated uncompressed size: $HUMAN_READABLE_SIZE" 209 | mysqldump -uhomestead -psecret --all-databases --skip-lock-tables 2>/dev/null | pv --size=$ADJUSTED_SIZE | gzip > "$FILE" 210 | else 211 | mysqldump -uhomestead -psecret --all-databases --skip-lock-tables 2>/dev/null | gzip > "$FILE" 212 | fi 213 | 214 | echo "Done." 215 | } 216 | 217 | function dbimport() { 218 | FILE=${1:-/vagrant/mysqldump.sql.gz} 219 | 220 | __pv_install_message "Want to see import progress?" 221 | 222 | echo "Importing databases from '$FILE'" 223 | 224 | if __has_pv; then 225 | pv "$FILE" --progress --eta | zcat | mysql -uhomestead -psecret 2>/dev/null 226 | else 227 | cat "$FILE" | zcat | mysql -uhomestead -psecret 2>/dev/null 228 | fi 229 | 230 | echo "Done." 231 | } 232 | 233 | function xphp() { 234 | (php -m | grep -q xdebug) 235 | if [[ $? -eq 0 ]] 236 | then 237 | XDEBUG_ENABLED=true 238 | else 239 | XDEBUG_ENABLED=false 240 | fi 241 | 242 | if ! $XDEBUG_ENABLED; then xon; fi 243 | 244 | php \ 245 | -dxdebug.remote_host=192.168.10.1 \ 246 | -dxdebug.remote_autostart=1 \ 247 | "$@" 248 | 249 | if ! $XDEBUG_ENABLED; then xoff; fi 250 | } 251 | -------------------------------------------------------------------------------- /app/Console/Commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mstaack/lumen-api-starter/c22cc5afe669609b744045440e37f0303d3f98b3/app/Console/Commands/.gitkeep -------------------------------------------------------------------------------- /app/Console/Commands/CreateAuthenticationKeyCommand.php: -------------------------------------------------------------------------------- 1 | encode(); 33 | 34 | if (file_exists($envFilePath = $this->getPathToEnvFile()) === false) { 35 | $this->info("Could not find env file! Key: $key"); 36 | } 37 | 38 | if ($this->updateEnvFile($envFilePath, $key)) { 39 | $this->info("File .env updated with key: $key"); 40 | } 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | private function getPathToEnvFile() 47 | { 48 | return base_path('.env'); 49 | } 50 | 51 | private function updateEnvFile($path, $key) 52 | { 53 | if (file_exists($path)) { 54 | 55 | $oldContent = file_get_contents($path); 56 | $search = 'PASETO_AUTH_KEY=' . env('PASETO_AUTH_KEY'); 57 | 58 | if (!str_contains($oldContent, $search)) { 59 | $search = 'PASETO_AUTH_KEY='; 60 | } 61 | 62 | $newContent = str_replace($search, 'PASETO_AUTH_KEY=' . $key, $oldContent); 63 | 64 | return file_put_contents($path, $newContent); 65 | } 66 | 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | getResponse(); 54 | } 55 | 56 | // AuthorizationException 57 | if ($e instanceof AuthorizationException) { 58 | return response()->json([ 59 | 'errors' => [ 60 | 'title' => 'Unauthorized' 61 | ] 62 | ], 401); 63 | } 64 | 65 | //Http Exceptions 66 | if ($e instanceof HttpException) { 67 | $response['message'] = $e->getMessage() ?: Response::$statusTexts[$e->getStatusCode()]; 68 | $response['status'] = $e->getStatusCode(); 69 | 70 | return response()->json($response, $response['status']); 71 | } 72 | 73 | //Default Exception Response 74 | $response = [ 75 | 'message' => 'Whoops! Something went wrong.', 76 | 'status' => 500 77 | ]; 78 | 79 | if ($this->isDebugMode()) { 80 | $response['debug'] = [ 81 | 'message' => $e->getMessage(), 82 | 'exception' => get_class($e), 83 | 'code' => $e->getCode(), 84 | 'file' => $e->getFile(), 85 | 'line' => $e->getLine(), 86 | ]; 87 | 88 | //clean trace 89 | foreach ($e->getTrace() as $item) { 90 | if (isset($item['args']) && is_array($item['args'])) { 91 | $item['args'] = $this->cleanTraceArgs($item['args']); 92 | } 93 | $response['debug']['trace'][] = $item; 94 | } 95 | } 96 | 97 | return response()->json($response, $response['status']); 98 | } 99 | /** 100 | * Determine if the application is in debug mode. 101 | * 102 | * @return Boolean 103 | */ 104 | public function isDebugMode() 105 | { 106 | return (boolean)env('APP_DEBUG'); 107 | } 108 | /** 109 | * @param array $args 110 | * @param int $level 111 | * @param int $count 112 | * 113 | * @return array|string 114 | */ 115 | private function cleanTraceArgs(array $args, $level = 0, &$count = 0) 116 | { 117 | $result = []; 118 | foreach ($args as $key => $value) { 119 | if (++$count > 1e4) { 120 | return '*SKIPPED over 10000 entries*'; 121 | } 122 | if (is_object($value)) { 123 | $result[$key] = get_class($value); 124 | } elseif (is_array($value)) { 125 | if ($level > 10) { 126 | $result[$key] = '*DEEP NESTED ARRAY*'; 127 | } else { 128 | $result[$key] = $this->cleanTraceArgs($value, $level + 1, $count); 129 | } 130 | } elseif (is_null($value)) { 131 | $result[$key] = null; 132 | } elseif (is_bool($value)) { 133 | $result[$key] = $value; 134 | } elseif (is_int($value)) { 135 | $result[$key] = $value; 136 | } elseif (is_resource($value)) { 137 | $result[$key] = get_resource_type($value); 138 | } elseif ($value instanceof \__PHP_Incomplete_Class) { 139 | $array = new \ArrayObject($value); 140 | $result[$key] = $array['__PHP_Incomplete_Class_Name']; 141 | } elseif (is_string($value) && mb_detect_encoding($value) === false) { 142 | $result[$key] = 'REMOVED-BINARY-BLOB'; 143 | } else { 144 | $result[$key] = (string)$value; 145 | } 146 | } 147 | return $result; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/Extensions/Authentication/PasetoAuthGuard.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 54 | $this->request = $request; 55 | } 56 | 57 | /** 58 | * Determine if the current request is a guest user. 59 | * 60 | * @return bool 61 | * @throws TypeError 62 | * @throws InvalidVersionException 63 | * @throws PasetoException 64 | */ 65 | public function guest() 66 | { 67 | if ($this->user !== null) { 68 | return false; 69 | } 70 | 71 | return !$this->check(); 72 | } 73 | 74 | /** 75 | * Determine if the current user is authenticated. 76 | * 77 | * @return bool 78 | * @throws TypeError 79 | * @throws InvalidVersionException 80 | * @throws PasetoException 81 | */ 82 | public function check() 83 | { 84 | $parser = Parser::getLocal($this->getSharedKey(), ProtocolCollection::v2()); 85 | 86 | $parser->addRule(new NotExpired); 87 | $parser->addRule(new IssuedBy($this->getIssuer())); 88 | 89 | try { 90 | $this->token = $parser->parse($this->getTokenFromRequest()); 91 | } catch (PasetoException $e) { 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | /** 99 | * @return SymmetricKey 100 | * @throws TypeError 101 | */ 102 | private function getSharedKey() 103 | { 104 | return SymmetricKey::fromEncodedString(env('PASETO_AUTH_KEY')); 105 | } 106 | 107 | /** 108 | * @return mixed 109 | */ 110 | private function getIssuer() 111 | { 112 | return env('PASETO_AUTH_ISSUER'); 113 | } 114 | 115 | /** 116 | * @return string|bool 117 | */ 118 | private function getTokenFromRequest() 119 | { 120 | if ($token = $this->request->headers->get('Authorization')) { 121 | return str_after($token, 'Bearer '); 122 | } 123 | 124 | return false; 125 | } 126 | 127 | /** 128 | * Attempt to authenticate the user using the given credentials and return the token. 129 | * 130 | * @param array $credentials 131 | * 132 | * @return mixed 133 | * @throws Exception 134 | * @throws InvalidKeyException 135 | * @throws InvalidPurposeException 136 | * @throws PasetoException 137 | * @throws TypeError 138 | */ 139 | public function attempt(array $credentials = []) 140 | { 141 | $user = $this->provider->retrieveByCredentials($credentials); 142 | 143 | if ($user && $user->verified && $this->provider->validateCredentials($user, $credentials)) { 144 | return $this->login($user); 145 | } 146 | 147 | return false; 148 | } 149 | 150 | /** 151 | * @param $user 152 | * @return string 153 | * @throws TypeError 154 | * @throws Exception 155 | * @throws InvalidKeyException 156 | * @throws PasetoException 157 | * @throws InvalidPurposeException 158 | */ 159 | private function login($user) 160 | { 161 | $this->setUser($user); 162 | 163 | return $this->generateTokenForUser(); 164 | } 165 | 166 | /** 167 | * Set the current user. 168 | * 169 | * @param Authenticatable $user 170 | * @return $this 171 | */ 172 | public function setUser(Authenticatable $user) 173 | { 174 | $this->user = $user; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * @return string 181 | * @throws InvalidKeyException 182 | * @throws InvalidPurposeException 183 | * @throws PasetoException 184 | * @throws TypeError 185 | */ 186 | private function generateTokenForUser() 187 | { 188 | $claims = [ 189 | 'id' => $this->user->id 190 | ]; 191 | 192 | $token = $this->getTokenBuilder() 193 | ->setExpiration(Carbon::now()->addHours($this->getExpireTime())) 194 | ->setIssuer($this->getIssuer()) 195 | ->setClaims($claims); 196 | 197 | return (string)$token; 198 | } 199 | 200 | /** 201 | * @return Builder 202 | * @throws PasetoException 203 | * @throws TypeError 204 | * @throws InvalidKeyException 205 | * @throws InvalidPurposeException 206 | */ 207 | private function getTokenBuilder() 208 | { 209 | return (new Builder) 210 | ->setKey($this->getSharedKey()) 211 | ->setVersion(new Version2) 212 | ->setPurpose(Purpose::local()); 213 | } 214 | 215 | /** 216 | * @return mixed 217 | */ 218 | private function getExpireTime() 219 | { 220 | return env('PASETO_AUTH_EXPIRE_AFTER_HOURS'); 221 | } 222 | 223 | /** 224 | * Get the currently authenticated user. 225 | * 226 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 227 | * @throws PasetoException 228 | */ 229 | public function user() 230 | { 231 | if (!$this->user && $this->token) { 232 | $this->user = $this->provider->retrieveById($this->token->get('id')); 233 | } 234 | 235 | return $this->user; 236 | } 237 | 238 | /** 239 | * Get the ID for the currently authenticated user. 240 | * 241 | * @return int|null 242 | */ 243 | public function id() 244 | { 245 | return $this->user->id; 246 | } 247 | 248 | /** 249 | * Validate a user's credentials. 250 | * 251 | * @param array $credentials 252 | * @return bool 253 | */ 254 | public function validate(array $credentials = []) 255 | { 256 | return $this->provider->validateCredentials($this->user, $credentials); 257 | } 258 | 259 | /** 260 | * Set the current request instance. 261 | * 262 | * @param \Illuminate\Http\Request $request 263 | * @return $this 264 | */ 265 | public function setRequest(Request $request) 266 | { 267 | $this->request = $request; 268 | 269 | return $this; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /app/Http/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | json(['data' => Auth::user()]); 35 | } 36 | 37 | /** 38 | * Login 39 | * 40 | * @bodyParam email string required The email 41 | * @bodyParam password string required The password 42 | * 43 | * @param LoginRequest $request 44 | * @return JsonResponse 45 | */ 46 | public function login(LoginRequest $request) 47 | { 48 | $credentials = $request->only('email', 'password'); 49 | 50 | $token = Auth::attempt($credentials); 51 | 52 | if (!$token) { 53 | return response()->json(['message' => trans('messages.login_failed')], 401); 54 | } 55 | 56 | return response()->json(['data' => ['user' => Auth::user(), 'token' => $token]]); 57 | } 58 | 59 | /** 60 | * Register 61 | * 62 | * @bodyParam email string required The email 63 | * @bodyParam password string required The password 64 | * @bodyParam name string required The name 65 | * 66 | * @param RegisterRequest $request 67 | * @return JsonResponse 68 | */ 69 | public function register(RegisterRequest $request) 70 | { 71 | $email = $request->input('email'); 72 | $password = $request->input('password'); 73 | $name = $request->input('name'); 74 | 75 | $user = User::createFromValues($name, $email, $password); 76 | 77 | Mail::to($user)->send(new Welcome($user)); 78 | 79 | return response()->json(['data' => ['message' => 'Account created. Please verify via email.']]); 80 | } 81 | 82 | /** 83 | * Verify User 84 | * 85 | * @queryParam token required The token 86 | * 87 | * @param String $token 88 | * @return JsonResponse 89 | * @throws Exception 90 | */ 91 | public function verify($token) 92 | { 93 | $user = User::verifyByToken($token); 94 | 95 | if (!$user) { 96 | return response()->json(['data' => ['message' => 'Invalid verification token']], 400); 97 | } 98 | 99 | return response()->json(['data' => ['message' => 'Account has been verified']]); 100 | } 101 | 102 | /** 103 | * Send new Password Request 104 | * 105 | * @bodyParam email string required The email 106 | * 107 | * @param Request $request 108 | * @return JsonResponse 109 | */ 110 | public function forgotPassword(Request $request) 111 | { 112 | $validator = Validator::make($request->all(), [ 113 | 'email' => 'required|exists:users,email' 114 | ]); 115 | 116 | if ($validator->passes()) { 117 | $user = User::byEmail($request->input('email')); 118 | 119 | Mail::to($user)->send(new PasswordReset($user)); 120 | } 121 | 122 | return response()->json(['data' => ['message' => 'Please check your email to reset your password.']]); 123 | } 124 | 125 | /** 126 | * Create new P assword 127 | * 128 | * @bodyParam password string required The new password 129 | * 130 | * @param Request $request 131 | * @param $token 132 | * @return JsonResponse 133 | * @throws ValidationException 134 | */ 135 | public function recoverPassword(Request $request, $token) 136 | { 137 | $this->validate($request, [ 138 | 'password' => 'required|min:8', 139 | ]); 140 | 141 | $user = User::newPasswordByResetToken($token, $request->input('password')); 142 | 143 | if ($user) { 144 | return response()->json(['data' => ['message' => 'Password has been changed.']]); 145 | } else { 146 | return response()->json(['data' => ['message' => 'Invalid password reset token']], 400); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 30 | } 31 | 32 | /** 33 | * Handle an incoming request. 34 | * 35 | * @param \Illuminate\Http\Request $request 36 | * @param \Closure $next 37 | * @param string|null $guard 38 | * @return mixed 39 | */ 40 | public function handle($request, Closure $next, $guard = null) 41 | { 42 | if ($this->auth->guard($guard)->guest()) { 43 | return response()->json([ 44 | 'errors' => [ 45 | 'title' => 'Unauthorized' 46 | ] 47 | ], 401); 48 | } 49 | 50 | return $next($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Middleware/CorsMiddleware.php: -------------------------------------------------------------------------------- 1 | json(['data' => ['message' => 'No valid cors settings found!']], 401); 30 | } 31 | 32 | $headers = [ 33 | 'Access-Control-Allow-Origin' => $allowedOrigin, 34 | 'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT, DELETE', 35 | 'Access-Control-Allow-Credentials' => 'true', 36 | 'Access-Control-Max-Age' => '86400', 37 | 'Access-Control-Allow-Headers' => $this->getAllowedHeaders() 38 | ]; 39 | 40 | if ($request->isMethod('OPTIONS')) { 41 | return response()->json('{"method":"OPTIONS"}', 200, $headers); 42 | } 43 | 44 | $response = $next($request); 45 | 46 | foreach ($headers as $key => $value) { 47 | if (method_exists($response, 'header')) { 48 | $response->header($key, $value); 49 | } 50 | } 51 | 52 | return $response; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | private function getAllowedHeaders() 59 | { 60 | return implode(', ', $this->allowedHeaders); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Http/Requests/LoginRequest.php: -------------------------------------------------------------------------------- 1 | merge(['email' => strtoLower($this->input('email'))]); 15 | } 16 | 17 | /** 18 | * Determine if the user is authorized to make this request. 19 | * 20 | * @return bool 21 | */ 22 | public function authorize() 23 | { 24 | return true; 25 | } 26 | 27 | /** 28 | * Get the validation rules that apply to the request. 29 | * 30 | * @return array 31 | */ 32 | public function rules() 33 | { 34 | return [ 35 | 'email' => 'required', 36 | 'password' => 'required', 37 | ]; 38 | } 39 | 40 | /** 41 | * Get custom messages for validator errors. 42 | * 43 | * @return array 44 | */ 45 | public function messages() 46 | { 47 | return [ 48 | // 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Requests/RegisterRequest.php: -------------------------------------------------------------------------------- 1 | merge(['email' => strtoLower($this->input('email'))]); 15 | } 16 | 17 | /** 18 | * Determine if the user is authorized to make this request. 19 | * 20 | * @return bool 21 | */ 22 | public function authorize() 23 | { 24 | return true; 25 | } 26 | 27 | /** 28 | * Get the validation rules that apply to the request. 29 | * 30 | * @return array 31 | */ 32 | public function rules() 33 | { 34 | return [ 35 | 'email' => 'required|unique:users,email', 36 | 'password' => 'required', 37 | 'name' => 'string' 38 | ]; 39 | } 40 | 41 | /** 42 | * Get custom messages for validator errors. 43 | * 44 | * @return array 45 | */ 46 | public function messages() 47 | { 48 | return [ 49 | // 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Jobs/ExampleJob.php: -------------------------------------------------------------------------------- 1 | user = $user; 27 | } 28 | 29 | /** 30 | * Build the message. 31 | * 32 | * @return $this 33 | */ 34 | public function build() 35 | { 36 | return $this 37 | ->subject(trans('messages.password_reset_subject')) 38 | ->view('emails.password-reset') 39 | ->with(['name' => $this->user->name, 'token' => $this->user->createPasswordRecoveryToken()]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Mail/Welcome.php: -------------------------------------------------------------------------------- 1 | user = $user; 27 | } 28 | 29 | /** 30 | * Build the message. 31 | * 32 | * @return $this 33 | */ 34 | public function build() 35 | { 36 | return $this 37 | ->subject(trans('messages.welcome_subject')) 38 | ->view('emails.welcome') 39 | ->with(['name' => $this->user->name, 'token' => $this->user->verification_token]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['auth']->extend('paseto', function ($app, $name, array $config) { 26 | 27 | $guard = new PasetoAuthGuard( 28 | $app['auth']->createUserProvider($config['provider']), 29 | $app['request'] 30 | ); 31 | 32 | $app->refresh('request', $guard, 'setRequest'); 33 | 34 | return $guard; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'App\Listeners\EventListener', 17 | ], 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/User.php: -------------------------------------------------------------------------------- 1 | roles->pluck('name'); 56 | } 57 | 58 | /** 59 | * Create a user 60 | * 61 | * @param $name 62 | * @param $email 63 | * @param $password 64 | * @return User|bool 65 | */ 66 | public static function createFromValues($name, $email, $password) 67 | { 68 | $user = new static; 69 | 70 | $user->name = $name; 71 | $user->email = $email; 72 | $user->password = Hash::make($password); 73 | $user->verification_token = Str::random(64); 74 | 75 | return $user->save() ? $user : false; 76 | } 77 | 78 | /** 79 | * Get user by email 80 | * 81 | * @param $email 82 | * @return User 83 | */ 84 | public static function byEmail($email) 85 | { 86 | return (new static)->where(compact('email'))->first(); 87 | } 88 | 89 | /** 90 | * Verify by token 91 | * 92 | * @param $token 93 | * @return false|User 94 | */ 95 | public static function verifyByToken($token) 96 | { 97 | $user = (new static)->where(['verification_token' => $token, 'verified' => 0])->first(); 98 | 99 | if (!$user) { 100 | return false; 101 | } 102 | 103 | $user->verify(); 104 | 105 | return $user; 106 | } 107 | 108 | /** 109 | * Verifiy a user 110 | * 111 | * @return bool 112 | */ 113 | public function verify() 114 | { 115 | $this->verification_token = null; 116 | $this->verified = 1; 117 | 118 | return $this->save(); 119 | } 120 | 121 | /** 122 | * Create password recovery token 123 | */ 124 | public function createPasswordRecoveryToken() 125 | { 126 | $token = Str::random(64); 127 | 128 | $created = DB::table('password_resets')->updateOrInsert( 129 | ['email' => $this->email], 130 | ['email' => $this->email, 'token' => $token] 131 | ); 132 | 133 | return $created ? $token : false; 134 | } 135 | 136 | /** 137 | * Restore password by token 138 | * 139 | * @param $token 140 | * @param $password 141 | * @return false|User 142 | */ 143 | public static function newPasswordByResetToken($token, $password) 144 | { 145 | $query = DB::table('password_resets')->where(compact('token')); 146 | $record = $query->first(); 147 | 148 | if (!$record) { 149 | return false; 150 | } 151 | 152 | $user = self::byEmail($record->email); 153 | 154 | $query->delete(); 155 | 156 | return $user->setPassword($password); 157 | } 158 | 159 | /** 160 | * Persist a new password for the user 161 | * 162 | * @param $password 163 | * @return bool 164 | */ 165 | public function setPassword($password) 166 | { 167 | $this->password = Hash::make($password); 168 | return $this->save(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make( 32 | 'Illuminate\Contracts\Console\Kernel' 33 | ); 34 | 35 | exit($kernel->handle(new ArgvInput, new ConsoleOutput)); 36 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | bootstrap(); 8 | 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Create The Application 12 | |-------------------------------------------------------------------------- 13 | | 14 | | Here we will load the environment and create the application instance 15 | | that serves as the central piece of this framework. We'll use this 16 | | application as an "IoC" container and router for this framework. 17 | | 18 | */ 19 | 20 | $app = new Laravel\Lumen\Application( 21 | realpath(__DIR__ . '/../') 22 | ); 23 | 24 | $app->withFacades(); 25 | 26 | $app->withEloquent(); 27 | 28 | $app->alias('cache', 'Illuminate\Cache\CacheManager'); 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Register Configs 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Lumen uses a simpler way to load config variables. 36 | | 37 | */ 38 | $app->configure('mail'); 39 | $app->configure('permission'); 40 | $app->configure('constants'); 41 | $app->configure('apidoc'); 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Register Container Bindings 46 | |-------------------------------------------------------------------------- 47 | | 48 | | Now we will register a few bindings in the service container. We will 49 | | register the exception handler and the console kernel. You may add 50 | | your own bindings here if you like or you can make another file. 51 | | 52 | */ 53 | 54 | $app->singleton( 55 | Illuminate\Contracts\Debug\ExceptionHandler::class, 56 | App\Exceptions\Handler::class 57 | ); 58 | 59 | $app->singleton( 60 | Illuminate\Contracts\Console\Kernel::class, 61 | App\Console\Kernel::class 62 | ); 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Register Middleware 67 | |-------------------------------------------------------------------------- 68 | | 69 | | Next, we will register the middleware with the application. These can 70 | | be global middleware that run before and after each request into a 71 | | route or middleware that'll be assigned to some specific routes. 72 | | 73 | */ 74 | 75 | $app->middleware([ 76 | \App\Http\Middleware\CorsMiddleware::class 77 | ]); 78 | 79 | $app->routeMiddleware([ 80 | 'auth' => App\Http\Middleware\Authenticate::class, 81 | 'permission' => Spatie\Permission\Middlewares\PermissionMiddleware::class, 82 | 'role' => Spatie\Permission\Middlewares\RoleMiddleware::class, 83 | ]); 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Register Service Providers 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Here we will register all of the application's service providers which 91 | | are used to bind services into the container. Service providers are 92 | | totally optional, so you are not required to uncomment this line. 93 | | 94 | */ 95 | 96 | $app->register(\App\Providers\AuthenticationProvider::class); 97 | $app->register(Pearl\RequestValidate\RequestServiceProvider::class); 98 | $app->register(\BeyondCode\DumpServer\DumpServerServiceProvider::class); 99 | $app->register(\Flipbox\LumenGenerator\LumenGeneratorServiceProvider::class); 100 | $app->register(Clockwork\Support\Lumen\ClockworkServiceProvider::class); 101 | $app->register(\Illuminate\Mail\MailServiceProvider::class); 102 | $app->register(Spatie\Permission\PermissionServiceProvider::class); 103 | 104 | if ($app->environment() !== 'production') { 105 | $app->register(\Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class); 106 | $app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class); 107 | } 108 | 109 | // $app->register(App\Providers\AppServiceProvider::class); 110 | // $app->register(App\Providers\EventServiceProvider::class); 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Load The Application Routes 115 | |-------------------------------------------------------------------------- 116 | | 117 | | Next we will include the routes file so that they can all be added to 118 | | the application. This will provide all of the URLs the application 119 | | can respond to, as well as the controllers that may handle them. 120 | | 121 | */ 122 | 123 | $app->router->group([ 124 | 'namespace' => 'App\Http\Controllers', 125 | ], function ($router) { 126 | require __DIR__ . '/../routes/api.php'; 127 | }); 128 | 129 | return $app; 130 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mstaack/lumen-api-starter", 3 | "description": "Lumen Starter for APIs", 4 | "keywords": [ 5 | "framework", 6 | "laravel", 7 | "lumen", 8 | "json", 9 | "rest", 10 | "api" 11 | ], 12 | "license": "MIT", 13 | "type": "project", 14 | "require": { 15 | "php": ">=7.1.3", 16 | "laravel/lumen-framework": "5.8.*", 17 | "vlucas/phpdotenv": "^3.3", 18 | "flipbox/lumen-generator": "^5.6", 19 | "paragonie/paseto": "^0.5.0", 20 | "itsgoingd/clockwork": "^2.2", 21 | "pearl/lumen-request-validate": "^1.0", 22 | "illuminate/mail": "^5.7", 23 | "spatie/laravel-permission": "^2.25", 24 | "beyondcode/laravel-dump-server": "^1.1", 25 | "mpociot/laravel-apidoc-generator": "^3.4" 26 | }, 27 | "require-dev": { 28 | "fzaninotto/faker": "~1.4", 29 | "phpunit/phpunit": "~7.0", 30 | "mockery/mockery": "~1.0", 31 | "barryvdh/laravel-ide-helper": "^2.4", 32 | "nunomaduro/collision": "^2.0", 33 | "codedungeon/phpunit-result-printer": "^0.19.10", 34 | "laravel/homestead": "^7.19" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "App\\": "app/" 39 | }, 40 | "files": [ 41 | "helpers.php" 42 | ] 43 | }, 44 | "autoload-dev": { 45 | "classmap": [ 46 | "tests/", 47 | "database/" 48 | ] 49 | }, 50 | "scripts": { 51 | "post-create-project-cmd": [ 52 | "php -r \"copy('.env.example', '.env');\"", 53 | "php artisan key:generate" 54 | ], 55 | "post-root-package-install": [ 56 | "php vendor/bin/homestead make", 57 | "composer keys", 58 | "composer meta" 59 | ], 60 | "meta": [ 61 | "php artisan ide-helper:generate", 62 | "php artisan ide-helper:meta", 63 | "php artisan ide-helper:model", 64 | "php artisan optimize" 65 | ], 66 | "keys": [ 67 | "php artisan key:generate", 68 | "php artisan auth:generate-paseto-key" 69 | ] 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true, 73 | "config": { 74 | "optimize-autoloader": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/apidoc.php: -------------------------------------------------------------------------------- 1 | 'public/docs', 9 | 10 | /* 11 | * The router to be used (Laravel or Dingo). 12 | */ 13 | 'router' => 'laravel', 14 | 15 | /* 16 | * Generate a Postman collection in addition to HTML docs. 17 | */ 18 | 'postman' => [ 19 | /* 20 | * Specify whether the Postman collection should be generated. 21 | */ 22 | 'enabled' => true, 23 | 24 | /* 25 | * The name for the exported Postman collection. Default: config('app.name')." API" 26 | */ 27 | 'name' => null, 28 | 29 | /* 30 | * The description for the exported Postman collection. 31 | */ 32 | 'description' => null, 33 | ], 34 | 35 | /* 36 | * The routes for which documentation should be generated. 37 | * Each group contains rules defining which routes should be included ('match', 'include' and 'exclude' sections) 38 | * and rules which should be applied to them ('apply' section). 39 | */ 40 | 'routes' => [ 41 | [ 42 | /* 43 | * Specify conditions to determine what routes will be parsed in this group. 44 | * A route must fulfill ALL conditions to pass. 45 | */ 46 | 'match' => [ 47 | 48 | /* 49 | * Match only routes whose domains match this pattern (use * as a wildcard to match any characters). 50 | */ 51 | 'domains' => [ 52 | '*', 53 | // 'domain1.*', 54 | ], 55 | 56 | /* 57 | * Match only routes whose paths match this pattern (use * as a wildcard to match any characters). 58 | */ 59 | 'prefixes' => [ 60 | '*', 61 | // 'users/*', 62 | ], 63 | 64 | /* 65 | * Match only routes registered under this version. This option is ignored for Laravel router. 66 | * Note that wildcards are not supported. 67 | */ 68 | 'versions' => [ 69 | 'v1', 70 | ], 71 | ], 72 | 73 | /* 74 | * Include these routes when generating documentation, 75 | * even if they did not match the rules above. 76 | * Note that the route must be referenced by name here (wildcards are supported). 77 | */ 78 | 'include' => [ 79 | // 'users.index', 'healthcheck*' 80 | ], 81 | 82 | /* 83 | * Exclude these routes when generating documentation, 84 | * even if they matched the rules above. 85 | * Note that the route must be referenced by name here (wildcards are supported). 86 | */ 87 | 'exclude' => [ 88 | // 'users.create', 'admin.*' 89 | ], 90 | 91 | /* 92 | * Specify rules to be applied to all the routes in this group when generating documentation 93 | */ 94 | 'apply' => [ 95 | /* 96 | * Specify headers to be added to the example requests 97 | */ 98 | 'headers' => [ 99 | // 'Authorization' => 'Bearer {token}', 100 | // 'Api-Version' => 'v2', 101 | ], 102 | 103 | /* 104 | * If no @response or @transformer declarations are found for the route, 105 | * we'll try to get a sample response by attempting an API call. 106 | * Configure the settings for the API call here, 107 | */ 108 | 'response_calls' => [ 109 | /* 110 | * API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc). 111 | * List the methods here or use '*' to mean all methods. Leave empty to disable API calls. 112 | */ 113 | 'methods' => ['GET'], 114 | 115 | /* 116 | * For URLs which have parameters (/users/{user}, /orders/{id?}), 117 | * specify what values the parameters should be replaced with. 118 | * Note that you must specify the full parameter, including curly brackets and question marks if any. 119 | */ 120 | 'bindings' => [ 121 | // '{user}' => 1 122 | ], 123 | 124 | /* 125 | * Environment variables which should be set for the API call. 126 | * This is a good place to ensure that notifications, emails 127 | * and other external services are not triggered during the documentation API calls 128 | */ 129 | 'env' => [ 130 | 'APP_ENV' => 'documentation', 131 | 'APP_DEBUG' => false, 132 | // 'env_var' => 'value', 133 | ], 134 | 135 | /* 136 | * Headers which should be sent with the API call. 137 | */ 138 | 'headers' => [ 139 | 'Content-Type' => 'application/json', 140 | 'Accept' => 'application/json', 141 | // 'key' => 'value', 142 | ], 143 | 144 | /* 145 | * Cookies which should be sent with the API call. 146 | */ 147 | 'cookies' => [ 148 | // 'name' => 'value' 149 | ], 150 | 151 | /* 152 | * Query parameters which should be sent with the API call. 153 | */ 154 | 'query' => [ 155 | // 'key' => 'value', 156 | ], 157 | 158 | /* 159 | * Body parameters which should be sent with the API call. 160 | */ 161 | 'body' => [ 162 | // 'key' => 'value', 163 | ], 164 | ], 165 | ], 166 | ], 167 | ], 168 | 169 | /* 170 | * Custom logo path. Will be copied during generate command. Set this to false to use the default logo. 171 | * 172 | * Change to an absolute path to use your custom logo. For example: 173 | * 'logo' => resource_path('views') . '/api/logo.png' 174 | * 175 | * If you want to use this, please be aware of the following rules: 176 | * - size: 230 x 52 177 | */ 178 | 'logo' => false, 179 | 180 | /* 181 | * Configure how responses are transformed using @transformer and @transformerCollection 182 | * Requires league/fractal package: composer require league/fractal 183 | * 184 | * If you are using a custom serializer with league/fractal, 185 | * you can specify it here. 186 | * 187 | * Serializers included with league/fractal: 188 | * - \League\Fractal\Serializer\ArraySerializer::class 189 | * - \League\Fractal\Serializer\DataArraySerializer::class 190 | * - \League\Fractal\Serializer\JsonApiSerializer::class 191 | * 192 | * Leave as null to use no serializer or return a simple JSON. 193 | */ 194 | 'fractal' => [ 195 | 'serializer' => null, 196 | ], 197 | ]; 198 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'api'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Authentication Guards 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Next, you may define every authentication guard for your application. 26 | | Of course, a great default configuration has been defined for you 27 | | here which uses session storage and the Eloquent user provider. 28 | | 29 | | All authentication drivers have a user provider. This defines how the 30 | | users are actually retrieved out of your database or other storage 31 | | mechanisms used by this application to persist your user's data. 32 | | 33 | | Supported: "token" 34 | | 35 | */ 36 | 37 | 'guards' => [ 38 | 'api' => [ 39 | 'driver' => 'paseto', 40 | 'provider' => 'users' 41 | ] 42 | ], 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | User Providers 47 | |-------------------------------------------------------------------------- 48 | | 49 | | All authentication drivers have a user provider. This defines how the 50 | | users are actually retrieved out of your database or other storage 51 | | mechanisms used by this application to persist your user's data. 52 | | 53 | | If you have multiple user tables or models you may configure multiple 54 | | sources which represent each model / table. These sources may then 55 | | be assigned to any extra authentication guards you have defined. 56 | | 57 | | Supported: "database", "eloquent" 58 | | 59 | */ 60 | 61 | 'providers' => [ 62 | 'users' => [ 63 | 'driver' => 'eloquent', 64 | 'model' => \App\User::class, 65 | ] 66 | ], 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Resetting Passwords 71 | |-------------------------------------------------------------------------- 72 | | 73 | | Here you may set the options for resetting passwords including the view 74 | | that is your password reset e-mail. You may also set the name of the 75 | | table that maintains all of the reset tokens for your application. 76 | | 77 | | You may specify multiple password reset configurations if you have more 78 | | than one user table or model in the application and you want to have 79 | | separate password reset settings based on the specific user types. 80 | | 81 | | The expire time is the number of minutes that the reset token should be 82 | | considered valid. This security feature keeps tokens short-lived so 83 | | they have less time to be guessed. You may change this as needed. 84 | | 85 | */ 86 | 87 | 'passwords' => [ 88 | // 89 | ], 90 | 91 | ]; 92 | -------------------------------------------------------------------------------- /config/constants.php: -------------------------------------------------------------------------------- 1 | env('WEBSITE_NAME','Acme Inc'), 17 | 'website_email' => env('WEBSITE_EMAIL','contact@acme.inc'), 18 | 'frontend_url' => env('FRONTEND_URL', 'http://127.0.0.1:8080'), 19 | ]; 20 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_DRIVER', 'log'), 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | SMTP Host Address 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Here you may provide the host address of the SMTP server used by your 23 | | applications. A default option is provided that is compatible with 24 | | the Mailgun mail service which will provide reliable deliveries. 25 | | 26 | */ 27 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | SMTP Host Port 31 | |-------------------------------------------------------------------------- 32 | | 33 | | This is the SMTP port used by your application to deliver e-mails to 34 | | users of the application. Like the host we have set this value to 35 | | stay compatible with the Mailgun e-mail application by default. 36 | | 37 | */ 38 | 'port' => env('MAIL_PORT', 587), 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Global "From" Address 42 | |-------------------------------------------------------------------------- 43 | | 44 | | You may wish for all e-mails sent by your application to be sent from 45 | | the same address. Here, you may specify a name and address that is 46 | | used globally for all e-mails that are sent by your application. 47 | | 48 | */ 49 | 'from' => [ 50 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 51 | 'name' => env('MAIL_FROM_NAME', 'Example'), 52 | ], 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | E-Mail Encryption Protocol 56 | |-------------------------------------------------------------------------- 57 | | 58 | | Here you may specify the encryption protocol that should be used when 59 | | the application send e-mail messages. A sensible default using the 60 | | transport layer security protocol should provide great security. 61 | | 62 | */ 63 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | SMTP Server Username 67 | |-------------------------------------------------------------------------- 68 | | 69 | | If your SMTP server requires a username for authentication, you should 70 | | set it here. This will get used to authenticate with your server on 71 | | connection. You may also set the "password" value below this one. 72 | | 73 | */ 74 | 'username' => env('MAIL_USERNAME'), 75 | 'password' => env('MAIL_PASSWORD'), 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Sendmail System Path 79 | |-------------------------------------------------------------------------- 80 | | 81 | | When using the "sendmail" driver to send e-mails, we will need to know 82 | | the path to where Sendmail lives on this server. A default path has 83 | | been provided here, which will work well on most of your systems. 84 | | 85 | */ 86 | 'sendmail' => '/usr/sbin/sendmail -bs', 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Markdown Mail Settings 90 | |-------------------------------------------------------------------------- 91 | | 92 | | If you are using Markdown based email rendering, you may configure your 93 | | theme and component paths here, allowing you to customize the design 94 | | of the emails. Or, you may simply stick with the Laravel defaults! 95 | | 96 | */ 97 | 'markdown' => [ 98 | 'theme' => 'default', 99 | 'paths' => [ 100 | resource_path('views/vendor/mail'), 101 | ], 102 | ], 103 | ]; -------------------------------------------------------------------------------- /config/permission.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | /* 8 | * When using the "HasRoles" trait from this package, we need to know which 9 | * Eloquent model should be used to retrieve your permissions. Of course, it 10 | * is often just the "Permission" model but you may use whatever you like. 11 | * 12 | * The model you want to use as a Permission model needs to implement the 13 | * `Spatie\Permission\Contracts\Permission` contract. 14 | */ 15 | 16 | 'permission' => Spatie\Permission\Models\Permission::class, 17 | 18 | /* 19 | * When using the "HasRoles" trait from this package, we need to know which 20 | * Eloquent model should be used to retrieve your roles. Of course, it 21 | * is often just the "Role" model but you may use whatever you like. 22 | * 23 | * The model you want to use as a Role model needs to implement the 24 | * `Spatie\Permission\Contracts\Role` contract. 25 | */ 26 | 27 | 'role' => Spatie\Permission\Models\Role::class, 28 | 29 | ], 30 | 31 | 'table_names' => [ 32 | 33 | /* 34 | * When using the "HasRoles" trait from this package, we need to know which 35 | * table should be used to retrieve your roles. We have chosen a basic 36 | * default value but you may easily change it to any table you like. 37 | */ 38 | 39 | 'roles' => 'roles', 40 | 41 | /* 42 | * When using the "HasRoles" trait from this package, we need to know which 43 | * table should be used to retrieve your permissions. We have chosen a basic 44 | * default value but you may easily change it to any table you like. 45 | */ 46 | 47 | 'permissions' => 'permissions', 48 | 49 | /* 50 | * When using the "HasRoles" trait from this package, we need to know which 51 | * table should be used to retrieve your models permissions. We have chosen a 52 | * basic default value but you may easily change it to any table you like. 53 | */ 54 | 55 | 'model_has_permissions' => 'model_has_permissions', 56 | 57 | /* 58 | * When using the "HasRoles" trait from this package, we need to know which 59 | * table should be used to retrieve your models roles. We have chosen a 60 | * basic default value but you may easily change it to any table you like. 61 | */ 62 | 63 | 'model_has_roles' => 'model_has_roles', 64 | 65 | /* 66 | * When using the "HasRoles" trait from this package, we need to know which 67 | * table should be used to retrieve your roles permissions. We have chosen a 68 | * basic default value but you may easily change it to any table you like. 69 | */ 70 | 71 | 'role_has_permissions' => 'role_has_permissions', 72 | ], 73 | 74 | /* 75 | * By default all permissions will be cached for 24 hours unless a permission or 76 | * role is updated. Then the cache will be flushed immediately. 77 | */ 78 | 79 | 'cache_expiration_time' => 60 * 24, 80 | 81 | /* 82 | * When set to true, the required permission/role names are added to the exception 83 | * message. This could be considered an information leak in some contexts, so 84 | * the default setting is false here for optimum safety. 85 | */ 86 | 87 | 'display_permission_in_exception' => false, 88 | ]; 89 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | increments('id'); 20 | $table->string('name'); 21 | $table->string('guard_name'); 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create($tableNames['roles'], function (Blueprint $table) { 26 | $table->increments('id'); 27 | $table->string('name'); 28 | $table->string('guard_name'); 29 | $table->timestamps(); 30 | }); 31 | 32 | Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames) { 33 | $table->unsignedInteger('permission_id'); 34 | $table->morphs('model'); 35 | 36 | $table->foreign('permission_id') 37 | ->references('id') 38 | ->on($tableNames['permissions']) 39 | ->onDelete('cascade'); 40 | 41 | $table->primary(['permission_id', 'model_id', 'model_type'], 'model_has_permissions_permission_model_type_primary'); 42 | }); 43 | 44 | Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames) { 45 | $table->unsignedInteger('role_id'); 46 | $table->morphs('model'); 47 | 48 | $table->foreign('role_id') 49 | ->references('id') 50 | ->on($tableNames['roles']) 51 | ->onDelete('cascade'); 52 | 53 | $table->primary(['role_id', 'model_id', 'model_type']); 54 | }); 55 | 56 | Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { 57 | $table->unsignedInteger('permission_id'); 58 | $table->unsignedInteger('role_id'); 59 | 60 | $table->foreign('permission_id') 61 | ->references('id') 62 | ->on($tableNames['permissions']) 63 | ->onDelete('cascade'); 64 | 65 | $table->foreign('role_id') 66 | ->references('id') 67 | ->on($tableNames['roles']) 68 | ->onDelete('cascade'); 69 | 70 | $table->primary(['permission_id', 'role_id']); 71 | 72 | app('cache')->forget('spatie.permission.cache'); 73 | }); 74 | } 75 | 76 | /** 77 | * Reverse the migrations. 78 | * 79 | * @return void 80 | */ 81 | public function down() 82 | { 83 | $tableNames = config('permission.table_names'); 84 | 85 | Schema::drop($tableNames['role_has_permissions']); 86 | Schema::drop($tableNames['model_has_roles']); 87 | Schema::drop($tableNames['model_has_permissions']); 88 | Schema::drop($tableNames['roles']); 89 | Schema::drop($tableNames['permissions']); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /database/migrations/2018_03_03_232931_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('name')->nullable(); 18 | $table->string('email')->unique(); 19 | $table->string('password', 60); 20 | $table->boolean('verified')->default(false); 21 | $table->string('verification_token')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('users'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2018_03_03_232948_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 17 | $table->string('token')->index(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('password_resets'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 16 | RolesSeeder::class, 17 | ]); 18 | User::createFromValues('John Doe', 'demo@demo.com', 'password')->assignRole('administrator'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/seeds/RolesSeeder.php: -------------------------------------------------------------------------------- 1 | forget('spatie.permission.cache'); 13 | 14 | $role = Role::create(['name' => 'administrator']); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /helpers.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./app 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | run(); 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Lumen 5.8 API Starter with Paseto 2 | 3 | [![Build Status](https://travis-ci.org/mstaack/lumen-api-starter.svg?branch=master)](https://travis-ci.org/mstaack/lumen-api-starter) 4 | 5 | # Notes 6 | - Comes with make & route command for all your needs 7 | - Uses jwt token alternative **paseto**. Read [Paseto](https://github.com/paragonie/paseto) 8 | 9 | # Included Packages 10 | - [Clockwork](https://underground.works/clockwork/) Easier debugging with APIs 11 | - [PHPUnit Pretty Result Printer](https://github.com/mikeerickson/phpunit-pretty-result-printer) Nice phpunit results 12 | - [Collision](https://github.com/nunomaduro/collision) Better Console Error Handling 13 | - [Lumen Form Requests](https://github.com/pearlkrishn/lumen-request-validate) Abstract Validation & Authorization into classes 14 | - [Laravel Dump Server](https://github.com/beyondcode/laravel-dump-server) Dump data to the artisan server 15 | - [Laravel-Apidoc-Generator](https://mpociot/laravel-apidoc-generator) Generate api docs 16 | - [spatie/laravel-permission](https://github.com/spatie/laravel-permission) Roles & Permissions 17 | 18 | # Installation 19 | - run `git clone git@github.com:mstaack/lumen-api-starter.git` 20 | - reinit your repository with `rm -rf .git && git init` 21 | - run `composer install` to install dependencies (consider using homestead via `vagrant up`) 22 | - copy `env.example` to `.env` 23 | - Setup your application & auth keys with `composer keys` & check `.env`file (automatically done via composer hook) 24 | - run migrations & seeders with `artisan migrate --seed` (within your vm using `vagrant ssh`) 25 | - A default user is created during seeding: `demo@demo.com` / `password` 26 | - To quickly start a dev server run `./artisan serve` (or via `homestead.test` for the vm) 27 | - Also consider running `composer meta` when adding models for better autocompletion (automatically done via composer hook) 28 | - Run included tests with `phpunit` within vagrant's code directory 29 | - Generate your api docs with `artisan apidoc:generate` 30 | 31 | # Routes 32 | ``` 33 | ➜ lumen-api-starter git:(update-5.8) ✗ ./artisan route:list 34 | +------+--------------------------------+-----------------------+-------------------------------------+-----------------+--------------------------+ 35 | | Verb | Path | NamedRoute | Controller | Action | Middleware | 36 | +------+--------------------------------+-----------------------+-------------------------------------+-----------------+--------------------------+ 37 | | GET | / | | None | Closure | | 38 | | POST | /auth/register | auth.register | App\Http\Controllers\AuthController | register | | 39 | | POST | /auth/login | auth.login | App\Http\Controllers\AuthController | login | | 40 | | GET | /auth/verify/{token} | auth.verify | App\Http\Controllers\AuthController | verify | | 41 | | POST | /auth/password/forgot | auth.password.forgot | App\Http\Controllers\AuthController | forgotPassword | | 42 | | POST | /auth/password/recover/{token} | auth.password.recover | App\Http\Controllers\AuthController | recoverPassword | | 43 | | GET | /auth/user | auth.user | App\Http\Controllers\AuthController | getUser | auth | 44 | | GET | /admin | | None | Closure | auth, role:administrator | 45 | +------+--------------------------------+-----------------------+-------------------------------------+-----------------+--------------------------+ 46 | ``` 47 | 48 | # Artisan Commands 49 | ``` 50 | ➜ lumen-api-starter git:(master) ./artisan 51 | Laravel Framework Lumen (5.6.4) (Laravel Components 5.6.*) 52 | 53 | Usage: 54 | command [options] [arguments] 55 | 56 | Options: 57 | -h, --help Display this help message 58 | -q, --quiet Do not output any message 59 | -V, --version Display this application version 60 | --ansi Force ANSI output 61 | --no-ansi Disable ANSI output 62 | -n, --no-interaction Do not ask any interactive question 63 | --env[=ENV] The environment the command should run under 64 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 65 | 66 | Available commands: 67 | clear-compiled Remove the compiled class file 68 | dump-server Start the dump server to collect dump information. 69 | help Displays help for a command 70 | list Lists commands 71 | migrate Run the database migrations 72 | optimize Optimize the framework for better performance 73 | serve Serve the application on the PHP development server 74 | tinker Interact with your application 75 | auth 76 | auth:clear-resets Flush expired password reset tokens 77 | auth:generate-paseto-key Creates a new authentication key for paseto 78 | cache 79 | cache:clear Flush the application cache 80 | cache:forget Remove an item from the cache 81 | cache:table Create a migration for the cache database table 82 | clockwork 83 | clockwork:clean Cleans Clockwork request metadata 84 | db 85 | db:seed Seed the database with records 86 | ide-helper 87 | ide-helper:eloquent Add \Eloquent helper to \Eloquent\Model 88 | ide-helper:generate Generate a new IDE Helper file. 89 | ide-helper:meta Generate metadata for PhpStorm 90 | ide-helper:models Generate autocompletion for models 91 | key 92 | key:generate Set the application key 93 | make 94 | make:command Create a new Artisan command 95 | make:controller Create a new controller class 96 | make:event Create a new event class 97 | make:job Create a new job class 98 | make:listener Create a new event listener class 99 | make:mail Create a new email class 100 | make:middleware Create a new middleware class 101 | make:migration Create a new migration file 102 | make:model Create a new Eloquent model class 103 | make:policy Create a new policy class 104 | make:provider Create a new service provider class 105 | make:request Create a new form request class 106 | make:resource Create a new resource 107 | make:seeder Create a new seeder class 108 | make:test Create a new test class 109 | migrate 110 | migrate:fresh Drop all tables and re-run all migrations 111 | migrate:install Create the migration repository 112 | migrate:refresh Reset and re-run all migrations 113 | migrate:reset Rollback all database migrations 114 | migrate:rollback Rollback the last database migration 115 | migrate:status Show the status of each migration 116 | queue 117 | queue:failed List all of the failed queue jobs 118 | queue:failed-table Create a migration for the failed queue jobs database table 119 | queue:flush Flush all of the failed queue jobs 120 | queue:forget Delete a failed queue job 121 | queue:listen Listen to a given queue 122 | queue:restart Restart queue worker daemons after their current job 123 | queue:retry Retry a failed queue job 124 | queue:table Create a migration for the queue jobs database table 125 | queue:work Start processing jobs on the queue as a daemon 126 | route 127 | route:list Display all registered routes. 128 | schedule 129 | schedule:run Run the scheduled commands 130 | ``` 131 | 132 | # Generated Docs Screenshot 133 | 134 | ![image](https://user-images.githubusercontent.com/10169509/54946091-a154de00-4f37-11e9-8a96-3ce71c189b6d.png) 135 | -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 'Email address or password is incorrect.', 7 | 'welcome_subject' => 'Please verify your account', 8 | 'welcome_header' => 'Welcome to :Website, :Name', 9 | 'welcome_text' => 'In order to complete your registration, you will have to verify your email address.', 10 | 'welcome_link' => 'Please click here to verify your account.', 11 | 'password_reset_subject' => 'You have requested to reset your password', 12 | 'password_reset_text' => 'You have requested to reset your password on :Website. Use the link below to set a new password.', 13 | 'password_reset_link' => 'Please click here in order to reset your password.', 14 | ]; 15 | -------------------------------------------------------------------------------- /resources/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute must be accepted.', 17 | 'active_url' => 'The :attribute is not a valid URL.', 18 | 'after' => 'The :attribute must be a date after :date.', 19 | 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', 20 | 'alpha' => 'The :attribute may only contain letters.', 21 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', 22 | 'alpha_num' => 'The :attribute may only contain letters and numbers.', 23 | 'array' => 'The :attribute must be an array.', 24 | 'before' => 'The :attribute must be a date before :date.', 25 | 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', 26 | 'between' => [ 27 | 'numeric' => 'The :attribute must be between :min and :max.', 28 | 'file' => 'The :attribute must be between :min and :max kilobytes.', 29 | 'string' => 'The :attribute must be between :min and :max characters.', 30 | 'array' => 'The :attribute must have between :min and :max items.', 31 | ], 32 | 'boolean' => 'The :attribute field must be true or false.', 33 | 'confirmed' => 'The :attribute confirmation does not match.', 34 | 'date' => 'The :attribute is not a valid date.', 35 | 'date_format' => 'The :attribute does not match the format :format.', 36 | 'different' => 'The :attribute and :other must be different.', 37 | 'digits' => 'The :attribute must be :digits digits.', 38 | 'digits_between' => 'The :attribute must be between :min and :max digits.', 39 | 'dimensions' => 'The :attribute has invalid image dimensions.', 40 | 'distinct' => 'The :attribute field has a duplicate value.', 41 | 'email' => 'The :attribute must be a valid email address.', 42 | 'exists' => 'The selected :attribute is invalid.', 43 | 'file' => 'The :attribute must be a file.', 44 | 'filled' => 'The :attribute field must have a value.', 45 | 'gt' => [ 46 | 'numeric' => 'The :attribute must be greater than :value.', 47 | 'file' => 'The :attribute must be greater than :value kilobytes.', 48 | 'string' => 'The :attribute must be greater than :value characters.', 49 | 'array' => 'The :attribute must have more than :value items.', 50 | ], 51 | 'gte' => [ 52 | 'numeric' => 'The :attribute must be greater than or equal :value.', 53 | 'file' => 'The :attribute must be greater than or equal :value kilobytes.', 54 | 'string' => 'The :attribute must be greater than or equal :value characters.', 55 | 'array' => 'The :attribute must have :value items or more.', 56 | ], 57 | 'image' => 'The :attribute must be an image.', 58 | 'in' => 'The selected :attribute is invalid.', 59 | 'in_array' => 'The :attribute field does not exist in :other.', 60 | 'integer' => 'The :attribute must be an integer.', 61 | 'ip' => 'The :attribute must be a valid IP address.', 62 | 'ipv4' => 'The :attribute must be a valid IPv4 address.', 63 | 'ipv6' => 'The :attribute must be a valid IPv6 address.', 64 | 'json' => 'The :attribute must be a valid JSON string.', 65 | 'lt' => [ 66 | 'numeric' => 'The :attribute must be less than :value.', 67 | 'file' => 'The :attribute must be less than :value kilobytes.', 68 | 'string' => 'The :attribute must be less than :value characters.', 69 | 'array' => 'The :attribute must have less than :value items.', 70 | ], 71 | 'lte' => [ 72 | 'numeric' => 'The :attribute must be less than or equal :value.', 73 | 'file' => 'The :attribute must be less than or equal :value kilobytes.', 74 | 'string' => 'The :attribute must be less than or equal :value characters.', 75 | 'array' => 'The :attribute must not have more than :value items.', 76 | ], 77 | 'max' => [ 78 | 'numeric' => 'The :attribute may not be greater than :max.', 79 | 'file' => 'The :attribute may not be greater than :max kilobytes.', 80 | 'string' => 'The :attribute may not be greater than :max characters.', 81 | 'array' => 'The :attribute may not have more than :max items.', 82 | ], 83 | 'mimes' => 'The :attribute must be a file of type: :values.', 84 | 'mimetypes' => 'The :attribute must be a file of type: :values.', 85 | 'min' => [ 86 | 'numeric' => 'The :attribute must be at least :min.', 87 | 'file' => 'The :attribute must be at least :min kilobytes.', 88 | 'string' => 'The :attribute must be at least :min characters.', 89 | 'array' => 'The :attribute must have at least :min items.', 90 | ], 91 | 'not_in' => 'The selected :attribute is invalid.', 92 | 'not_regex' => 'The :attribute format is invalid.', 93 | 'numeric' => 'The :attribute must be a number.', 94 | 'present' => 'The :attribute field must be present.', 95 | 'regex' => 'The :attribute format is invalid.', 96 | 'required' => 'The :attribute field is required.', 97 | 'required_if' => 'The :attribute field is required when :other is :value.', 98 | 'required_unless' => 'The :attribute field is required unless :other is in :values.', 99 | 'required_with' => 'The :attribute field is required when :values is present.', 100 | 'required_with_all' => 'The :attribute field is required when :values is present.', 101 | 'required_without' => 'The :attribute field is required when :values is not present.', 102 | 'required_without_all' => 'The :attribute field is required when none of :values are present.', 103 | 'same' => 'The :attribute and :other must match.', 104 | 'size' => [ 105 | 'numeric' => 'The :attribute must be :size.', 106 | 'file' => 'The :attribute must be :size kilobytes.', 107 | 'string' => 'The :attribute must be :size characters.', 108 | 'array' => 'The :attribute must contain :size items.', 109 | ], 110 | 'string' => 'The :attribute must be a string.', 111 | 'timezone' => 'The :attribute must be a valid zone.', 112 | 'unique' => 'The :attribute has already been taken.', 113 | 'uploaded' => 'The :attribute failed to upload.', 114 | 'url' => 'The :attribute format is invalid.', 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Custom Validation Language Lines 119 | |-------------------------------------------------------------------------- 120 | | 121 | | Here you may specify custom validation messages for attributes using the 122 | | convention "attribute.rule" to name the lines. This makes it quick to 123 | | specify a specific custom language line for a given attribute rule. 124 | | 125 | */ 126 | 127 | 'custom' => [ 128 | 'attribute-name' => [ 129 | 'rule-name' => 'custom-message', 130 | ], 131 | ], 132 | 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Custom Validation Attributes 136 | |-------------------------------------------------------------------------- 137 | | 138 | | The following language lines are used to swap attribute place-holders 139 | | with something more reader friendly such as E-Mail Address instead 140 | | of "email". This simply helps us make messages a little cleaner. 141 | | 142 | */ 143 | 144 | 'attributes' => [], 145 | 146 | ]; 147 | -------------------------------------------------------------------------------- /resources/views/emails/password-reset.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ trans('messages.password_reset_text', ['website' => config('constants.website_name')]) }} 7 |
8 | {{ trans('messages.password_reset_link') }} 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/emails/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

7 | {{ trans('messages.welcome_header', ['website' => config('constants.website_name'), 'name' => $name]) }}. 8 |

9 |
10 | {{ trans('messages.welcome_text') }} 11 |

12 | {{ trans('messages.welcome_link') }} 13 | 14 | 15 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 8 | return response()->json(['message' => 'Welcome to Lumen API Starter']); 9 | }); 10 | 11 | /* Auth Routes */ 12 | $router->group(['prefix' => 'auth', 'as' => 'auth'], function (Router $router) { 13 | 14 | /* Defaults */ 15 | $router->post('/register', [ 16 | 'as' => 'register', 17 | 'uses' => 'AuthController@register', 18 | ]); 19 | $router->post('/login', [ 20 | 'as' => 'login', 21 | 'uses' => 'AuthController@login', 22 | ]); 23 | $router->get('/verify/{token}', [ 24 | 'as' => 'verify', 25 | 'uses' => 'AuthController@verify' 26 | ]); 27 | 28 | /* Password Reset */ 29 | $router->post('/password/forgot', [ 30 | 'as' => 'password.forgot', 31 | 'uses' => 'AuthController@forgotPassword' 32 | ]); 33 | $router->post('/password/recover/{token}', [ 34 | 'as' => 'password.recover', 35 | 'uses' => 'AuthController@recoverPassword' 36 | ]); 37 | 38 | /* Protected User Endpoint */ 39 | $router->get('/user', [ 40 | 'uses' => 'AuthController@getUser', 41 | 'as' => 'user', 42 | 'middleware' => 'auth' 43 | ]); 44 | }); 45 | 46 | /* Protected Routes */ 47 | $router->group(['middleware' => 'auth'], function (Router $router) { 48 | 49 | $router->group(['middleware' => 'role:administrator'], function (Router $router) { 50 | 51 | $router->get('/admin', function () { 52 | return response()->json(['message' => 'You are authorized as an administrator.']); 53 | }); 54 | 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/clockwork/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | post('/auth/register', [ 15 | 'name' => $this->testName, 16 | 'email' => $this->testEmail, 17 | 'password' => $this->testPassword 18 | ]); 19 | 20 | $this->assertResponseOk(); 21 | } 22 | 23 | public function test_login_fails_without_activation() 24 | { 25 | $this->createTestUser(); 26 | 27 | $this->post('/auth/login', [ 28 | 'email' => $this->testEmail, 29 | 'password' => $this->testPassword 30 | ]); 31 | 32 | $this->assertUnauthorized(); 33 | } 34 | 35 | public function test_login_with_activation() 36 | { 37 | $this->createTestUser($verified = true); 38 | 39 | $this->post('/auth/login', [ 40 | 'email' => $this->testEmail, 41 | 'password' => $this->testPassword 42 | ]); 43 | 44 | $this->assertResponseOk(); 45 | } 46 | 47 | public function test_protected_route_with_authenticated_user() 48 | { 49 | $user = $this->createTestUser($verified = true); 50 | 51 | $this->be($user); 52 | 53 | $this->get('auth/user')->assertResponseOk(); 54 | } 55 | 56 | public function test_protected_route_without_authenticated_user() 57 | { 58 | $this->get('auth/user'); 59 | 60 | $this->assertUnauthorized(); 61 | } 62 | 63 | public function test_activation_process() 64 | { 65 | $token = $this->createTestUser($verified = false)->verification_token; 66 | 67 | $this->seeInDatabase('users', ['verification_token' => $token]); 68 | 69 | $this->get("auth/verify/$token")->assertResponseOk(); 70 | 71 | $this->notSeeInDatabase('users', ['verification_token' => $token]); 72 | } 73 | 74 | public function test_password_forgotten_process() 75 | { 76 | $user = $this->createTestUser($verified = true); 77 | $newPassword = Str::random(8); 78 | 79 | //request 80 | $this->post('auth/password/forgot', ['email' => $user->email])->assertResponseOk(); 81 | 82 | //get token "from" mail 83 | $token = DB::table('password_resets')->where('email', $user->email)->first()->token; 84 | 85 | //change 86 | $this->post("auth/password/recover/$token", ['password' => $newPassword])->assertResponseOk(); 87 | 88 | $this->assertNotFalse(Auth::attempt(['email' => $user->email, 'password' => $newPassword])); 89 | } 90 | 91 | public function test_validation_register() 92 | { 93 | $this->post('/auth/register'); 94 | 95 | $this->assertValidationFailedResponse(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | testName, $this->testEmail, $this->testPassword); 44 | 45 | if ($user && $verified) { 46 | $user->verify(); 47 | } 48 | 49 | return $user ?: false; 50 | } 51 | 52 | /** 53 | * Check 422 status code 54 | */ 55 | public function assertValidationFailedResponse() 56 | { 57 | $this->assertResponseStatus(422); 58 | } 59 | 60 | /** 61 | * Check 401 status code 62 | */ 63 | protected function assertUnauthorized() 64 | { 65 | $this->assertResponseStatus(401); 66 | } 67 | } 68 | --------------------------------------------------------------------------------