├── .docker ├── Dockerfile ├── entrypoint.sh ├── queue-entrypoint.sh └── schedule-entrypoint.sh ├── .dockerignore ├── .docs └── api │ └── readme.md ├── .editorconfig ├── .env.example ├── .env.testing ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── phpunit.yml │ └── sonar.yaml ├── .gitignore ├── LICENSE ├── app ├── Auth │ ├── Dispensary │ │ ├── Dispensary.php │ │ ├── Exceptions │ │ │ └── TokenExpiredException.php │ │ └── Repository.php │ ├── EmailDispensary.php │ ├── LoginDispensary.php │ └── RegistrationDispensary.php ├── Console │ └── Kernel.php ├── Contracts │ └── Http │ │ └── Responses │ │ └── ResponseFactory.php ├── Events │ └── User │ │ └── Deleting.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ └── Api │ │ │ ├── Auth │ │ │ ├── Dispense.php │ │ │ ├── Login.php │ │ │ └── Logout.php │ │ │ ├── Invitation │ │ │ ├── Accept.php │ │ │ └── Resend.php │ │ │ ├── Password │ │ │ ├── Forgotten.php │ │ │ └── Reset.php │ │ │ ├── Profile │ │ │ ├── Email │ │ │ │ ├── Update.php │ │ │ │ └── Verify.php │ │ │ ├── Password │ │ │ │ └── Update.php │ │ │ ├── Show.php │ │ │ └── Update.php │ │ │ ├── Registration │ │ │ ├── Store.php │ │ │ └── Verify.php │ │ │ └── User │ │ │ ├── Destroy.php │ │ │ ├── Index.php │ │ │ ├── Show.php │ │ │ ├── Store.php │ │ │ └── Update.php │ ├── Header.php │ ├── Kernel.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── ThrottleRequests.php │ │ ├── TrimStrings.php │ │ ├── TrustProxies.php │ │ └── VerifyCsrfToken.php │ ├── Requests │ │ └── Api │ │ │ ├── Auth │ │ │ └── LoginRequest.php │ │ │ ├── Invitation │ │ │ ├── AcceptRequest.php │ │ │ └── ResendRequest.php │ │ │ ├── Password │ │ │ ├── ForgottenRequest.php │ │ │ └── ResetRequest.php │ │ │ ├── Profile │ │ │ ├── Email │ │ │ │ ├── UpdateRequest.php │ │ │ │ └── VerifyRequest.php │ │ │ ├── Password │ │ │ │ └── UpdateRequest.php │ │ │ └── UpdateRequest.php │ │ │ ├── Registration │ │ │ ├── StoreRequest.php │ │ │ └── VerifyRequest.php │ │ │ └── User │ │ │ ├── IndexRequest.php │ │ │ ├── StoreRequest.php │ │ │ └── UpdateRequest.php │ └── Responses │ │ └── ResponseFactory.php ├── Listeners │ └── User │ │ └── Deleting │ │ └── CleanUp.php ├── Mail │ ├── Registration │ │ ├── AlreadyExists.php │ │ └── Verify.php │ └── User │ │ ├── Email │ │ ├── CantUpdate.php │ │ └── VerifyUpdate.php │ │ ├── Invitation.php │ │ └── PasswordReset.php ├── Models │ ├── Dispense.php │ ├── User.php │ └── UserToken.php ├── Notifications │ └── User │ │ ├── Email │ │ ├── CantUpdate.php │ │ └── VerifyUpdate.php │ │ ├── Invitation.php │ │ └── PasswordReset.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── RouteServiceProvider.php │ └── SPAServiceProvider.php └── SPA │ └── UrlGenerator.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── filterable.php ├── hashing.php ├── logging.php ├── mail.php ├── queue.php ├── services.php ├── session.php ├── spa.php └── view.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2019_05_15_083540_create_user_invitations_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2020_01_23_104553_create_user_tokens_table.php │ └── 2020_08_05_061418_create_dispenses_table.php └── seeders │ ├── DatabaseSeeder.php │ └── UsersTableSeeder.php ├── docker-compose.yml ├── lang └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── phpunit.xml ├── public ├── .htaccess ├── index.php ├── robots.txt └── web.config ├── readme.md ├── resources └── views │ └── mail │ ├── registration │ ├── already_exists.blade.php │ └── verify.blade.php │ └── user │ ├── email │ ├── cant_update.blade.php │ └── verify_update.blade.php │ ├── invitation.blade.php │ └── password_reset.blade.php ├── routes ├── api.php ├── channels.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── stubs ├── console.stub ├── controller.plain.stub ├── factory.stub ├── job.queued.stub ├── middleware.stub ├── migration.create.stub ├── migration.stub ├── migration.update.stub ├── model.pivot.stub ├── model.stub ├── policy.plain.stub ├── policy.stub ├── request.stub ├── rule.stub ├── seeder.stub ├── test.stub └── test.unit.stub ├── supervisord.pid └── tests ├── CreatesApplication.php ├── Feature ├── Http │ └── Api │ │ ├── Auth │ │ ├── DispenseTest.php │ │ ├── LoginTest.php │ │ └── LogoutTest.php │ │ ├── GuardTest.php │ │ ├── Invitation │ │ ├── AcceptTest.php │ │ └── ResendTest.php │ │ ├── Password │ │ ├── ForgottenTest.php │ │ └── ResetTest.php │ │ ├── Profile │ │ ├── Email │ │ │ ├── UpdateTest.php │ │ │ └── VerifyTest.php │ │ ├── Password │ │ │ └── UpdateTest.php │ │ ├── ShowTest.php │ │ └── UpdateTest.php │ │ ├── Registration │ │ ├── StoreTest.php │ │ └── VerifyTest.php │ │ └── User │ │ ├── DestroyTest.php │ │ ├── IndexTest.php │ │ ├── ShowTest.php │ │ ├── StoreTest.php │ │ └── UpdateTest.php └── Models │ └── UserTest.php ├── TestCase.php └── Unit ├── Http └── Middleware │ └── ThrottleRequestsTest.php ├── Mail ├── Registration │ ├── AlreadyExistsTest.php │ └── VerifyTest.php └── User │ ├── Email │ ├── CantUpdateTest.php │ └── VerifyUpdateTest.php │ ├── InvitationTest.php │ └── PasswordResetTest.php └── Notifications └── User ├── Email ├── CantUpdateTest.php └── VerifyUpdateTest.php ├── InvitationTest.php └── PasswordResetTest.php /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thekingscode/php8.2-apache as base 2 | 3 | RUN rm /var/run/apache2/apache2.pid 4 | 5 | FROM base as dev 6 | 7 | ARG user=king 8 | ARG uid=1000 9 | 10 | # Use development php ini 11 | RUN mv /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini 12 | 13 | # Setup user. 14 | RUN useradd -G www-data,root -u $uid -d /home/$user $user 15 | RUN mkdir -p /home/$user/.composer 16 | RUN chown -R $user:$user /home/$user 17 | RUN chown $user:$user -Rf /opt && chmod -R 777 /opt; # Make /opt writable for PHPStorm (coverage) 18 | 19 | RUN echo 'alias pa="php artisan"' >> /home/$user/.bashrc 20 | RUN echo 'alias phpunit="./vendor/bin/phpunit"' >> /home/$user/.bashrc 21 | RUN echo 'alias refresh="pa migrate:fresh"' >> /home/$user/.bashrc 22 | 23 | USER $user 24 | 25 | FROM base as prod 26 | 27 | RUN docker-php-ext-install opcache && docker-php-ext-enable opcache; 28 | 29 | # Use development php ini 30 | RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini 31 | 32 | COPY . /var/www 33 | WORKDIR /var/www 34 | 35 | RUN composer install --no-dev --optimize-autoloader 36 | 37 | # Make storage and bootstrap writable for the www-data user 38 | RUN chown -R www-data:www-data /var/www/storage && chmod -R 777 /var/www/storage 39 | RUN chown -R www-data:www-data /var/www/bootstrap && chmod -R 777 /var/www/bootstrap 40 | 41 | COPY .docker/entrypoint.sh /usr/local/bin/entrypoint.sh 42 | RUN chmod +x /usr/local/bin/entrypoint.sh 43 | ENTRYPOINT [ "sh","/usr/local/bin/entrypoint.sh" ] 44 | 45 | FROM php:8.2 as base-cli 46 | 47 | # TODO Install any required libraries and/or php extensions that you might need in the queue-worker. 48 | 49 | RUN curl -sS https://getcomposer.org/installer -o composer-setup.php 50 | RUN php composer-setup.php --install-dir=/usr/local/bin --filename=composer 51 | RUN rm -rf composer-setup.php 52 | 53 | FROM base-cli as prod-cli 54 | 55 | RUN docker-php-ext-install opcache && docker-php-ext-enable opcache; 56 | 57 | COPY . /var/www 58 | WORKDIR /var/www 59 | 60 | RUN composer install --prefer-dist --no-interaction 61 | 62 | FROM prod-cli as schedule 63 | 64 | COPY .docker/schedule-entrypoint.sh /usr/local/bin/entrypoint.sh 65 | RUN chmod +x /usr/local/bin/entrypoint.sh 66 | ENTRYPOINT [ "sh","/usr/local/bin/entrypoint.sh" ] 67 | 68 | FROM prod-cli as queue 69 | 70 | COPY .docker/queue-entrypoint.sh /usr/local/bin/entrypoint.sh 71 | RUN chmod +x /usr/local/bin/entrypoint.sh 72 | ENTRYPOINT [ "sh","/usr/local/bin/entrypoint.sh" ] 73 | -------------------------------------------------------------------------------- /.docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!bin/sh 2 | 3 | php artisan migrate --force 4 | php artisan optimize 5 | 6 | apachectl -D FOREGROUND 7 | -------------------------------------------------------------------------------- /.docker/queue-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!bin/sh 2 | 3 | cd /var/www && \ 4 | php artisan optimize && \ 5 | php artisan queue:work -v 6 | -------------------------------------------------------------------------------- /.docker/schedule-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!bin/sh 2 | 3 | cd /var/www && \ 4 | php artisan optimize && \ 5 | php artisan schedule:run 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ./vendor/** 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | SPA_FORCE_URL=true 8 | SPA_URL=http://localhost:8080 9 | 10 | LOG_CHANNEL=stack 11 | 12 | DB_CONNECTION=mysql 13 | DB_HOST=db 14 | DB_PORT=3306 15 | DB_DATABASE=application 16 | DB_USERNAME=root 17 | DB_PASSWORD=secret 18 | 19 | BROADCAST_DRIVER=log 20 | CACHE_DRIVER=redis 21 | QUEUE_CONNECTION=redis 22 | SESSION_DRIVER=redis 23 | SESSION_LIFETIME=120 24 | 25 | REDIS_DB=0 26 | REDIS_CACHE_DB=1 27 | 28 | REDIS_HOST=redis 29 | REDIS_PASSWORD=null 30 | REDIS_PORT=6379 31 | 32 | MAIL_MAILER=log 33 | MAIL_HOST=smtp.mailtrap.io 34 | MAIL_PORT=2525 35 | MAIL_USERNAME=null 36 | MAIL_PASSWORD=null 37 | MAIL_ENCRYPTION=null 38 | MAIL_FROM_ADDRESS="me@local.nl" 39 | MAIL_FROM_NAME="${APP_NAME}" 40 | 41 | AWS_ACCESS_KEY_ID= 42 | AWS_SECRET_ACCESS_KEY= 43 | AWS_DEFAULT_REGION=us-east-1 44 | AWS_BUCKET= 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_APP_CLUSTER=mt1 50 | 51 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 53 | 54 | SENTRY_LARAVEL_DSN= 55 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | SPA_FORCE_URL=true 8 | SPA_URL=http://localhost:8080 9 | 10 | LOG_CHANNEL=stack 11 | 12 | DB_CONNECTION=mysql 13 | DB_HOST=db 14 | DB_PORT=3306 15 | DB_DATABASE=application 16 | DB_USERNAME=root 17 | DB_PASSWORD=secret 18 | 19 | BROADCAST_DRIVER=log 20 | CACHE_DRIVER=redis 21 | QUEUE_CONNECTION=redis 22 | SESSION_DRIVER=redis 23 | SESSION_LIFETIME=120 24 | 25 | REDIS_DB=0 26 | REDIS_CACHE_DB=1 27 | 28 | REDIS_HOST=redis 29 | REDIS_PASSWORD=null 30 | REDIS_PORT=6379 31 | 32 | MAIL_MAILER=log 33 | MAIL_HOST=smtp.mailtrap.io 34 | MAIL_PORT=2525 35 | MAIL_USERNAME=null 36 | MAIL_PASSWORD=null 37 | MAIL_ENCRYPTION=null 38 | MAIL_FROM_ADDRESS="me@local.nl" 39 | MAIL_FROM_NAME="${APP_NAME}" 40 | 41 | AWS_ACCESS_KEY_ID= 42 | AWS_SECRET_ACCESS_KEY= 43 | AWS_DEFAULT_REGION=us-east-1 44 | AWS_BUCKET= 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_APP_CLUSTER=mt1 50 | 51 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 53 | 54 | SENTRY_LARAVEL_DSN= 55 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | * @roelgonzalez @dennislammers 4 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | name: PHPUnit 14 | runs-on: ubuntu-latest 15 | services: 16 | mysql: 17 | image: mysql:8 18 | env: 19 | MYSQL_ROOT_PASSWORD: secret 20 | MYSQL_DATABASE: application 21 | ports: 22 | - 33306:3306 23 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v2 28 | 29 | - name: Setup PHP 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: 8.2 33 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, gd 34 | tools: composer 35 | coverage: none 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-interaction --no-progress 39 | 40 | - name: Start MariaDB 41 | run: sudo /etc/init.d/mysql start 42 | 43 | - name: Execute tests 44 | run: | 45 | php artisan migrate --force --quiet --no-interaction 46 | vendor/bin/phpunit 47 | env: 48 | DB_HOST: 127.0.0.1 49 | DB_PORT: ${{ job.services.mysql.ports[3306] }} 50 | DB_DATABASE: application 51 | DB_USERNAME: root 52 | DB_PASSWORD: secret 53 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yaml: -------------------------------------------------------------------------------- 1 | name: Sonar analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 16 | 17 | - name: Setup Sonar properties 18 | run: echo "sonar.projectKey=laravel-api-starter" > sonar-project.properties 19 | 20 | - uses: sonarsource/sonarqube-scan-action@master 21 | env: 22 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 23 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 24 | 25 | # If you wish to fail your job when the Quality Gate is red, uncomment the 26 | - uses: sonarsource/sonarqube-quality-gate-action@master 27 | timeout-minutes: 5 28 | env: 29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | .env 7 | .phpunit.result.cache 8 | Homestead.json 9 | Homestead.yaml 10 | npm-debug.log 11 | yarn-error.log 12 | .idea 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kings Code 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 | -------------------------------------------------------------------------------- /app/Auth/Dispensary/Dispensary.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 20 | $this->hasher = $hasher; 21 | } 22 | 23 | public function dispense(string $key, int $ttl, int $chars): string 24 | { 25 | $token = $this->generateToken($chars); 26 | 27 | $this->repository->put($key, $this->hasher->make($token), $ttl); 28 | 29 | return $token; 30 | } 31 | 32 | private function generateToken(int $chars): string 33 | { 34 | return Str::random($chars); 35 | } 36 | 37 | /** 38 | * @param string $key 39 | * @param string $token 40 | * @return bool 41 | * @throws \App\Auth\Dispensary\Exceptions\TokenExpiredException 42 | */ 43 | public function verify(string $key, string $token): bool 44 | { 45 | $hashedToken = $this->repository->get($key) ?? throw new TokenExpiredException(); 46 | 47 | return $this->hasher->check($token, $hashedToken); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Auth/Dispensary/Exceptions/TokenExpiredException.php: -------------------------------------------------------------------------------- 1 | updateOrCreate(['key' => $key], [ 15 | 'token' => $token, 16 | 'expires_at' => Carbon::now()->addSeconds($ttl), 17 | ]); 18 | } 19 | 20 | public function get(string $key): ?string 21 | { 22 | $dispense = Dispense::query()->where('key', $key)->first(); 23 | 24 | if (! $dispense instanceof Dispense) { 25 | return null; 26 | } 27 | 28 | if ($dispense->getExpiresAt()->lessThan(Carbon::now())) { 29 | $dispense->delete(); 30 | 31 | return null; 32 | } 33 | 34 | return $dispense->getToken(); 35 | } 36 | 37 | public function clear(): void 38 | { 39 | Dispense::query()->delete(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Auth/EmailDispensary.php: -------------------------------------------------------------------------------- 1 | dispensary = $dispensary; 21 | } 22 | 23 | public function dispense(Authenticatable $user, string $email): string 24 | { 25 | return $this->dispensary->dispense($this->getKey($user, $email), self::TTL, self::CHARS); 26 | } 27 | 28 | private function getKey(Authenticatable $user, string $email): string 29 | { 30 | return implode('_', [ 31 | class_basename($user), 32 | $user->getAuthIdentifier(), 33 | 'email', 34 | $email, 35 | ]); 36 | } 37 | 38 | public function verify(Authenticatable $user, string $email, string $token): bool 39 | { 40 | return $this->dispensary->verify($this->getKey($user, $email), $token); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Auth/LoginDispensary.php: -------------------------------------------------------------------------------- 1 | dispensary = $dispensary; 21 | } 22 | 23 | public function dispense(Authenticatable $user): string 24 | { 25 | return $this->dispensary->dispense($this->getKey($user), self::TTL, self::CHARS); 26 | } 27 | 28 | private function getKey(Authenticatable $user): string 29 | { 30 | return implode('_', [ 31 | class_basename($user), 32 | $user->getAuthIdentifier(), 33 | 'login', 34 | ]); 35 | } 36 | 37 | public function verify(Authenticatable $user, string $token): bool 38 | { 39 | return $this->dispensary->verify($this->getKey($user), $token); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Auth/RegistrationDispensary.php: -------------------------------------------------------------------------------- 1 | dispensary = $dispensary; 21 | } 22 | 23 | public function dispense(Authenticatable $user): string 24 | { 25 | return $this->dispensary->dispense($this->getKey($user), self::TTL, self::CHARS); 26 | } 27 | 28 | private function getKey(Authenticatable $user): string 29 | { 30 | return implode('_', [ 31 | class_basename($user), 32 | $user->getAuthIdentifier(), 33 | 'agency', 34 | 'registration', 35 | ]); 36 | } 37 | 38 | public function verify(Authenticatable $user, string $token): bool 39 | { 40 | return $this->dispensary->verify($this->getKey($user), $token); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/Commands'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Contracts/Http/Responses/ResponseFactory.php: -------------------------------------------------------------------------------- 1 | user = $user; 16 | } 17 | 18 | public function getUser(): User 19 | { 20 | return $this->user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | container->bound('sentry') && $this->shouldReport($e)) { 25 | $this->container->make('sentry')->captureException($e); 26 | } 27 | 28 | parent::report($e); 29 | } 30 | 31 | public function render($request, Throwable $e) 32 | { 33 | $request->headers->set(Header::ACCEPT, 'application/json'); 34 | 35 | return parent::render($request, $e); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Auth/Dispense.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 35 | $this->urlGenerator = $urlGenerator; 36 | $this->dispensary = $dispensary; 37 | $this->config = $config; 38 | } 39 | 40 | /** 41 | * @param \Illuminate\Http\Request $request 42 | * @return \Illuminate\Http\RedirectResponse 43 | */ 44 | public function __invoke(Request $request): RedirectResponse 45 | { 46 | $url = $this->createUrl($request); 47 | 48 | if (! $request->filled(['email', 'token'])) { 49 | return $this->responseFactory->redirectTo($url); 50 | } 51 | 52 | $user = User::query()->where('email', $request->input('email'))->first(); 53 | 54 | if (! $user instanceof User) { 55 | return $this->responseFactory->redirectTo($url); 56 | } 57 | 58 | try { 59 | $verified = $this->dispensary->verify($user, $request->input('token')); 60 | 61 | if (! $verified) { 62 | return $this->responseFactory->redirectTo($url); 63 | } 64 | 65 | do { 66 | $token = Str::random(128); 67 | } while (UserToken::query()->where('token', $token)->exists()); 68 | 69 | $user->tokens()->create(['token' => $token]); 70 | 71 | return $this->responseFactory->redirectTo($url . '#token=' . $token); 72 | } catch (TokenExpiredException $exception) { 73 | return $this->responseFactory->redirectTo($url); 74 | } 75 | } 76 | 77 | private function createUrl(Request $request): string 78 | { 79 | if (false === $this->config->get('spa.force_url') && 80 | null !== $request->headers->get('referer')) { 81 | $urlInfo = parse_url($request->headers->get('referer')); 82 | if (is_array($urlInfo) && isset($urlInfo['scheme']) && isset($urlInfo['host'])) { 83 | if (isset($urlInfo['port'])) { 84 | $urlGenerator = new UrlGenerator("{$urlInfo['scheme']}://{$urlInfo['host']}:{$urlInfo['port']}"); 85 | } else { 86 | $urlGenerator = new UrlGenerator("{$urlInfo['scheme']}://{$urlInfo['host']}"); 87 | } 88 | 89 | return $urlGenerator->to('auth/callback', [ 90 | 'redirect_uri' => $request->input('redirect_uri'), 91 | ]); 92 | } 93 | } 94 | 95 | return $this->urlGenerator->to('auth/callback', [ 96 | 'redirect_uri' => $request->input('redirect_uri'), 97 | ]); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Auth/Login.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 33 | $this->dispensary = $dispensary; 34 | $this->hasher = $hasher; 35 | $this->translator = $translator; 36 | } 37 | 38 | /** 39 | * @param \App\Http\Requests\Api\Auth\LoginRequest $request 40 | * @return \Illuminate\Http\JsonResponse 41 | * @throws \Illuminate\Validation\ValidationException 42 | */ 43 | public function __invoke(LoginRequest $request): JsonResponse 44 | { 45 | $user = User::query()->where('email', $request->input('email'))->first(); 46 | 47 | if (! $user instanceof User) { 48 | $this->fail(); 49 | } 50 | 51 | if (! $this->hasher->check($request->input('password'), $user->getAuthPassword())) { 52 | $this->fail(); 53 | } 54 | 55 | return $this->responseFactory->json([ 56 | 'data' => [ 57 | 'token' => $this->dispensary->dispense($user), 58 | ], 59 | ]); 60 | } 61 | 62 | /** 63 | * @throws \Illuminate\Validation\ValidationException 64 | */ 65 | private function fail(): void 66 | { 67 | throw ValidationException::withMessages([ 68 | 'email' => $this->translator->get('auth.failed'), 69 | ]); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Auth/Logout.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 18 | } 19 | 20 | public function __invoke(Guard $guard): Response 21 | { 22 | /** @var \App\Models\User $user */ 23 | $user = $guard->user(); 24 | 25 | $token = $user->getCurrentToken(); 26 | 27 | $token->delete(); 28 | 29 | return $this->responseFactory->noContent(Response::HTTP_OK); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Invitation/Accept.php: -------------------------------------------------------------------------------- 1 | passwordBrokerManager = $passwordBrokerManager; 40 | $this->hasher = $hasher; 41 | $this->eventDispatcher = $eventDispatcher; 42 | $this->responseFactory = $responseFactory; 43 | $this->translator = $translator; 44 | } 45 | 46 | public function __invoke(AcceptRequest $request): JsonResponse 47 | { 48 | $credentials = $request->only(['token', 'email', 'password', 'password_confirmation']); 49 | 50 | $response = $this->getPasswordBroker()->reset($credentials, function (User $user, string $password) { 51 | $user->setAttribute('password', $this->hasher->make($password)); 52 | 53 | $user->setRememberToken(Str::random(60)); 54 | 55 | $user->save(); 56 | 57 | $this->eventDispatcher->dispatch(new PasswordReset($user)); 58 | }); 59 | 60 | if (PasswordBroker::PASSWORD_RESET === $response) { 61 | return $this->responseFactory->json([ 62 | 'message' => $this->translator->get($response), 63 | ]); 64 | } 65 | 66 | return $this->responseFactory->json([ 67 | 'message' => $this->translator->get(PasswordBroker::INVALID_TOKEN), 68 | ], Response::HTTP_BAD_REQUEST); 69 | } 70 | 71 | private function getPasswordBroker(): PasswordBroker 72 | { 73 | return $this->passwordBrokerManager->broker('user-invitations'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Invitation/Resend.php: -------------------------------------------------------------------------------- 1 | notificationDispatcher = $notificationDispatcher; 30 | $this->passwordBrokerManager = $passwordBrokerManager; 31 | $this->responseFactory = $responseFactory; 32 | } 33 | 34 | public function __invoke(ResendRequest $request): Response 35 | { 36 | $user = User::query()->where('email', $request->input('email'))->first(); 37 | 38 | if ($user instanceof User) { 39 | $this->notificationDispatcher->send($user, 40 | new Invitation($this->getPasswordBroker()->createToken($user)) 41 | ); 42 | } 43 | 44 | return $this->responseFactory->noContent(Response::HTTP_OK); 45 | } 46 | 47 | private function getPasswordBroker(): PasswordBroker 48 | { 49 | return $this->passwordBrokerManager->broker('user-invitations'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Password/Forgotten.php: -------------------------------------------------------------------------------- 1 | passwordBrokerManager = $passwordBrokerManager; 34 | $this->responseFactory = $responseFactory; 35 | $this->translator = $translator; 36 | $this->notificationDispatcher = $notificationDispatcher; 37 | } 38 | 39 | public function __invoke(ForgottenRequest $request): JsonResponse 40 | { 41 | $user = User::query()->where('email', $request->input('email'))->first(); 42 | 43 | if ($user instanceof User) { 44 | $this->notificationDispatcher->send($user, 45 | new PasswordReset($this->getPasswordBroker()->createToken($user)) 46 | ); 47 | } 48 | 49 | // We'll always send a successful response so that an attack can't find 50 | // what email addresses exist in the application. 51 | return $this->responseFactory->json([ 52 | 'message' => $this->translator->get(PasswordBroker::RESET_LINK_SENT), 53 | ]); 54 | } 55 | 56 | private function getPasswordBroker(): PasswordBroker 57 | { 58 | return $this->passwordBrokerManager->broker('users'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Password/Reset.php: -------------------------------------------------------------------------------- 1 | passwordBrokerManager = $passwordBrokerManager; 40 | $this->hasher = $hasher; 41 | $this->eventDispatcher = $eventDispatcher; 42 | $this->responseFactory = $responseFactory; 43 | $this->translator = $translator; 44 | } 45 | 46 | public function __invoke(ResetRequest $request): JsonResponse 47 | { 48 | $credentials = $request->only(['token', 'email', 'password', 'password_confirmation']); 49 | 50 | $response = $this->getPasswordBroker()->reset($credentials, function (User $user, string $password) { 51 | $user->setAttribute('password', $this->hasher->make($password)); 52 | 53 | $user->setRememberToken(Str::random(60)); 54 | 55 | $user->save(); 56 | 57 | $this->eventDispatcher->dispatch(new PasswordReset($user)); 58 | }); 59 | 60 | if (PasswordBroker::PASSWORD_RESET === $response) { 61 | return $this->responseFactory->json([ 62 | 'message' => $this->translator->get($response), 63 | ]); 64 | } 65 | 66 | return $this->responseFactory->json([ 67 | 'message' => $this->translator->get(PasswordBroker::INVALID_TOKEN), 68 | ], Response::HTTP_BAD_REQUEST); 69 | } 70 | 71 | private function getPasswordBroker(): PasswordBroker 72 | { 73 | return $this->passwordBrokerManager->broker('users'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Profile/Email/Update.php: -------------------------------------------------------------------------------- 1 | dispensary = $dispensary; 31 | $this->notificationDispatcher = $notificationDispatcher; 32 | $this->responseFactory = $responseFactory; 33 | } 34 | 35 | public function __invoke(Guard $guard, UpdateRequest $request): Response 36 | { 37 | /** @var \App\Models\User $user */ 38 | $user = $guard->user(); 39 | 40 | $canUpdate = User::query() 41 | ->whereKeyNot($user->getKey()) 42 | ->where('email', $request->input('email')) 43 | ->doesntExist(); 44 | 45 | if ($canUpdate) { 46 | $token = $this->dispensary->dispense($user, $request->input('email')); 47 | 48 | $this->notificationDispatcher->send($user, 49 | new VerifyUpdate($token, $request->input('email')) 50 | ); 51 | 52 | return $this->responseFactory->noContent(Response::HTTP_OK); 53 | } 54 | 55 | $this->notificationDispatcher->send($user, 56 | new CantUpdate() 57 | ); 58 | 59 | return $this->responseFactory->noContent(Response::HTTP_OK); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Profile/Email/Verify.php: -------------------------------------------------------------------------------- 1 | dispensary = $dispensary; 23 | $this->responseFactory = $responseFactory; 24 | } 25 | 26 | public function __invoke(Guard $guard, VerifyRequest $request): Response 27 | { 28 | /** @var \App\Models\User $user */ 29 | $user = $guard->user(); 30 | 31 | $email = $request->input('email'); 32 | 33 | try { 34 | if ($this->dispensary->verify($user, $email, $request->input('token'))) { 35 | $user->update(['email' => $email]); 36 | 37 | return $this->responseFactory->noContent(Response::HTTP_OK); 38 | } 39 | 40 | return $this->responseFactory->noContent(Response::HTTP_BAD_REQUEST); 41 | } catch (TokenExpiredException $e) { 42 | return $this->responseFactory->noContent(Response::HTTP_BAD_REQUEST); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Profile/Password/Update.php: -------------------------------------------------------------------------------- 1 | hasher = $hasher; 22 | $this->responseFactory = $responseFactory; 23 | } 24 | 25 | public function __invoke(Guard $guard, UpdateRequest $request): Response 26 | { 27 | /** @var \App\Models\User $user */ 28 | $user = $guard->user(); 29 | 30 | $user->update([ 31 | 'password' => $this->hasher->make($request->input('password')), 32 | ]); 33 | 34 | return $this->responseFactory->noContent(Response::HTTP_OK); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Profile/Show.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 19 | } 20 | 21 | public function __invoke(Guard $guard, Request $request): JsonResponse 22 | { 23 | /** @var \App\Models\User $user */ 24 | $user = $guard->user(); 25 | 26 | return $this->responseFactory->json([ 27 | 'id' => $user->getKey(), 28 | 'name' => $user->getAttribute('name'), 29 | 'email' => $user->getAttribute('email'), 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Profile/Update.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 19 | } 20 | 21 | public function __invoke(Guard $guard, UpdateRequest $request): Response 22 | { 23 | /** @var \App\Models\User $user */ 24 | $user = $guard->user(); 25 | 26 | $user->update( 27 | $request->validated() 28 | ); 29 | 30 | return $this->responseFactory->noContent(Response::HTTP_OK); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Registration/Store.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 34 | $this->mailer = $mailer; 35 | $this->hasher = $hasher; 36 | $this->dispensary = $dispensary; 37 | } 38 | 39 | public function __invoke(StoreRequest $request): Response 40 | { 41 | $email = $request->input('email'); 42 | 43 | if (User::query()->where('email', $email)->exists()) { 44 | $this->mailer->send( 45 | (new AlreadyExists())->to($email) 46 | ); 47 | 48 | return $this->responseFactory->noContent(Response::HTTP_OK); 49 | } 50 | 51 | $userData = array_merge($request->validated(), [ 52 | 'password' => 'not-logged-in-yet', 53 | ]); 54 | 55 | /** @var User $user */ 56 | $user = User::query()->create($userData); 57 | 58 | $token = $this->dispensary->dispense($user); 59 | 60 | $this->mailer->send( 61 | (new Verify($token, $email))->to($email) 62 | ); 63 | 64 | return $this->responseFactory->noContent(Response::HTTP_OK); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Registration/Verify.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 29 | $this->hasher = $hasher; 30 | $this->dispensary = $dispensary; 31 | } 32 | 33 | /** 34 | * @param \App\Http\Requests\Api\Registration\VerifyRequest $request 35 | * @return \Illuminate\Http\Response 36 | * @throws \Psr\SimpleCache\InvalidArgumentException 37 | */ 38 | public function __invoke(VerifyRequest $request): Response 39 | { 40 | $email = $request->input('email'); 41 | $token = $request->input('token'); 42 | $password = $request->input('password'); 43 | 44 | /** @var User $user */ 45 | $user = User::query()->where('email', $email)->first(); 46 | 47 | if (! $user instanceof User) { 48 | return $this->responseFactory->noContent(Response::HTTP_BAD_REQUEST); 49 | } 50 | 51 | try { 52 | if ($this->dispensary->verify($user, $token)) { 53 | $user->update([ 54 | 'password' => $this->hasher->make($password), 55 | ]); 56 | 57 | return $this->responseFactory->noContent(Response::HTTP_OK); 58 | } 59 | 60 | return $this->responseFactory->noContent(Response::HTTP_BAD_REQUEST); 61 | } catch (TokenExpiredException $e) { 62 | return $this->responseFactory->noContent(Response::HTTP_BAD_REQUEST); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/User/Destroy.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 19 | } 20 | 21 | public function __invoke(Guard $guard, User $user): Response 22 | { 23 | /** @var User $authenticatedUser */ 24 | $authenticatedUser = $guard->user(); 25 | 26 | if ($user->is($authenticatedUser)) { 27 | return $this->responseFactory->noContent(Response::HTTP_CONFLICT); 28 | } 29 | 30 | $user->delete(); 31 | 32 | return $this->responseFactory->noContent(Response::HTTP_OK); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/User/Index.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 23 | $this->filtering = $filtering; 24 | } 25 | 26 | public function __invoke(IndexRequest $request): JsonResponse 27 | { 28 | $builder = User::query()->select(['id', 'name', 'email']); 29 | 30 | $this->filtering->builder($builder) 31 | ->filterFor('search', static function (Builder $builder, string $value) { 32 | $builder->where('name', 'like', '%' . $value . '%'); 33 | $builder->orWhere('email', 'like', '%' . $value . '%'); 34 | }) 35 | ->sortFor('name') 36 | ->sortFor('email') 37 | ->defaultSorting('name') 38 | ->filter(); 39 | 40 | $paginator = $builder->paginate( 41 | $request->input('per_page') 42 | ); 43 | 44 | return $this->responseFactory->paginator($paginator); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/User/Show.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 19 | } 20 | 21 | public function __invoke(Request $request, User $user): JsonResponse 22 | { 23 | return $this->responseFactory->json([ 24 | 'id' => $user->getKey(), 25 | 'name' => $user->getAttribute('name'), 26 | 'email' => $user->getAttribute('email'), 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/User/Store.php: -------------------------------------------------------------------------------- 1 | validated(); 31 | 32 | // Don't need an actual password yet as we're going to send an invite. 33 | Arr::set($attributes, 'password', Str::random(64)); 34 | 35 | /** @var User $user */ 36 | $user = User::query()->create($attributes); 37 | 38 | $this->notificationDispatcher->send( 39 | $user, 40 | new Invitation($this->getPasswordBroker()->createToken($user)) 41 | ); 42 | 43 | return $this->responseFactory->noContent(Response::HTTP_CREATED); 44 | } 45 | 46 | private function getPasswordBroker(): PasswordBroker 47 | { 48 | return $this->passwordBrokerManager->broker('user-invitations'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/User/Update.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 19 | } 20 | 21 | public function __invoke(UpdateRequest $request, User $user): Response 22 | { 23 | $user->update( 24 | $request->validated() 25 | ); 26 | 27 | return $this->responseFactory->noContent(Response::HTTP_OK); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Header.php: -------------------------------------------------------------------------------- 1 | [ 55 | EncryptCookies::class, 56 | AddQueuedCookiesToResponse::class, 57 | StartSession::class, 58 | ShareErrorsFromSession::class, 59 | VerifyCsrfToken::class, 60 | SubstituteBindings::class, 61 | ], 62 | 63 | 'api' => [ 64 | SubstituteBindings::class, 65 | ], 66 | ]; 67 | 68 | /** 69 | * The application's route middleware. 70 | * 71 | * These middleware may be assigned to groups or used individually. 72 | * 73 | * @var array 74 | */ 75 | protected $routeMiddleware = [ 76 | 'auth' => Authenticate::class, 77 | 'auth.basic' => AuthenticateWithBasicAuth::class, 78 | 'cache.headers' => SetCacheHeaders::class, 79 | 'can' => Authorize::class, 80 | 'password.confirm' => RequirePassword::class, 81 | 'signed' => ValidateSignature::class, 82 | 'throttle' => ThrottleRequests::class, 83 | 'verified' => EnsureEmailIsVerified::class, 84 | ]; 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | throttleRequests = $throttleRequests; 20 | $this->config = $config; 21 | } 22 | 23 | public function handle($request, Closure $next, string $limiter) 24 | { 25 | if ($this->isEnabled()) { 26 | return $this->throttleRequests->handle($request, $next, $limiter); 27 | } 28 | 29 | return $next($request); 30 | } 31 | 32 | private function isEnabled(): bool 33 | { 34 | return (bool) $this->config->get('app.throttling'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email:rfc,dns',], 15 | 'password' => ['required', 'string'], 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Invitation/AcceptRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string'], 15 | 'email' => ['required', 'string', 'email:rfc,dns',], 16 | 'password' => ['required', 'string', 'min:10', 'confirmed'], 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Invitation/ResendRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email:rfc,dns'], 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Password/ForgottenRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email:rfc,dns',], 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Password/ResetRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string'], 15 | 'email' => ['required', 'string', 'email:rfc,dns'], 16 | 'password' => ['required', 'string', 'min:10', 'confirmed'], 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Profile/Email/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email:rfc,dns'], 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Profile/Email/VerifyRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email:rfc,dns',], 15 | 'token' => ['required', 'string'], 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Profile/Password/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'required', 17 | 'string', 18 | 'min:10', 19 | 'confirmed', 20 | ], 21 | 'current_password' => [ 22 | 'required', 23 | 'string', 24 | function ($attribute, $value, $fail) { 25 | /** @var \App\Models\User $user */ 26 | $user = $this->user(); 27 | 28 | /** @var Hasher $hasher */ 29 | $hasher = $this->container->make(Hasher::class); 30 | 31 | if (! $hasher->check($value, $user->getAuthPassword())) { 32 | $fail('Current password is invalid.'); 33 | } 34 | }, 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Profile/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string'], 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Registration/StoreRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string'], 15 | 'email' => ['required', 'string', 'email:rfc,dns',], 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/Registration/VerifyRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email:rfc,dns',], 15 | 'token' => ['required', 'string'], 16 | 'password' => ['required', 'string', 'min:10', 'confirmed'], 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/User/IndexRequest.php: -------------------------------------------------------------------------------- 1 | ['sometimes', 'string'], 15 | 'sort_by' => ['sometimes', 'string', 'in:name,email'], 16 | 'desc' => ['sometimes', 'boolean'], 17 | 'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'], 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/User/StoreRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string'], 16 | 'email' => ['required', 'string', 'email:rfc,dns', Rule::unique('users', 'email')], 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Requests/Api/User/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | route()->parameter('user'); 15 | 16 | return [ 17 | 'name' => ['required', 'string'], 18 | 'email' => ['required', 'string', 'email:rfc,dns', Rule::unique('users', 'email')->ignore($user)], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Responses/ResponseFactory.php: -------------------------------------------------------------------------------- 1 | redirector = $redirector; 23 | } 24 | 25 | public function noContent(int $status = Response::HTTP_NO_CONTENT, array $headers = []): Response 26 | { 27 | return $this->make('', $status, $headers); 28 | } 29 | 30 | public function make(string $content = '', int $status = Response::HTTP_OK, array $headers = []): Response 31 | { 32 | return new Response($content, $status, $headers); 33 | } 34 | 35 | public function mappedPaginator( 36 | LengthAwarePaginator $paginator, 37 | callable $map, 38 | int $status = Response::HTTP_OK, 39 | array $headers = [], 40 | int $options = 0 41 | ): JsonResponse { 42 | $paginator->setCollection($paginator->getCollection()->map($map)); 43 | 44 | return $this->paginator($paginator, $status, $headers, $options); 45 | } 46 | 47 | public function paginator( 48 | LengthAwarePaginator $paginator, 49 | int $status = Response::HTTP_OK, 50 | array $headers = [], 51 | int $options = 0 52 | ): JsonResponse { 53 | return $this->json([ 54 | 'data' => $paginator->items(), 55 | 'meta' => [ 56 | 'current_page' => $paginator->currentPage(), 57 | 'last_page' => $paginator->lastPage(), 58 | 'from' => $paginator->firstItem(), 59 | 'to' => $paginator->lastItem(), 60 | 'total' => $paginator->total(), 61 | 'per_page' => $paginator->perPage(), 62 | ], 63 | ]); 64 | } 65 | 66 | public function json( 67 | array $data = [], 68 | int $status = Response::HTTP_OK, 69 | array $headers = [], 70 | int $options = 0 71 | ): JsonResponse { 72 | if (! array_key_exists('data', $data)) { 73 | $data = ['data' => $data]; 74 | } 75 | 76 | return new JsonResponse($data, $status, $headers, $options); 77 | } 78 | 79 | public function stream(callable $callback, int $status = Response::HTTP_OK, array $headers = []): StreamedResponse 80 | { 81 | return new StreamedResponse($callback, $status, $headers); 82 | } 83 | 84 | public function redirectTo(string $path, int $status = Response::HTTP_FOUND, array $headers = []): RedirectResponse 85 | { 86 | return $this->redirector->to($path, $status, $headers, true); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/Listeners/User/Deleting/CleanUp.php: -------------------------------------------------------------------------------- 1 | getUser(); 14 | 15 | $user->tokens()->delete(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Mail/Registration/AlreadyExists.php: -------------------------------------------------------------------------------- 1 | markdown('mail.registration.already_exists') 21 | ->subject('Registration verification') 22 | ->with([ 23 | 'front_end_url' => $urlGenerator->to(''), 24 | 'password_forgotten_url' => $urlGenerator->to('password/forgotten'), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Mail/Registration/Verify.php: -------------------------------------------------------------------------------- 1 | token = $token; 25 | $this->email = $email; 26 | } 27 | 28 | public function build(UrlGenerator $urlGenerator) 29 | { 30 | return $this->markdown('mail.registration.verify') 31 | ->subject('Registration verification') 32 | ->with([ 33 | 'front_end_url' => $urlGenerator->to(''), 34 | 'verify_url' => $urlGenerator->to('registration/verify') . "#token={$this->token}&email={$this->email}", 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Mail/User/Email/CantUpdate.php: -------------------------------------------------------------------------------- 1 | user = $user; 17 | } 18 | 19 | public function build() 20 | { 21 | return $this 22 | ->subject('Email address update request') 23 | ->to($this->user->getEmail(), $this->user->getName()) 24 | ->markdown('mail.user.email.cant_update') 25 | ->with([ 26 | 'user' => $this->user, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Mail/User/Email/VerifyUpdate.php: -------------------------------------------------------------------------------- 1 | user = $user; 22 | $this->token = $token; 23 | $this->email = $email; 24 | } 25 | 26 | public function build(UrlGenerator $urlGenerator) 27 | { 28 | $verifyUrl = $urlGenerator->to('profile/email/verify') . "#token={$this->token}&email={$this->email}"; 29 | 30 | return $this 31 | ->subject('Email address update request') 32 | ->to($this->user->getEmail(), $this->user->getName()) 33 | ->markdown('mail.user.email.verify_update') 34 | ->with([ 35 | 'user' => $this->user, 36 | 'verify_url' => $verifyUrl, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Mail/User/Invitation.php: -------------------------------------------------------------------------------- 1 | to( 23 | 'invitation/accept/' . $this->token, 24 | [ 25 | 'email' => $this->user->getEmail(), 26 | ] 27 | ); 28 | 29 | return $this 30 | ->subject('Account invitation') 31 | ->to( 32 | $this->user->getEmail(), 33 | $this->user->getName() 34 | ) 35 | ->markdown('mail.user.invitation') 36 | ->with([ 37 | 'user' => $this->user, 38 | 'acceptation_url' => $acceptationUrl, 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Mail/User/PasswordReset.php: -------------------------------------------------------------------------------- 1 | user = $user; 20 | $this->token = $token; 21 | } 22 | 23 | public function build(UrlGenerator $urlGenerator) 24 | { 25 | $passwordResetUrl = $urlGenerator->to('password/reset/' . $this->token, [ 26 | 'email' => $this->user->getEmail(), 27 | ]); 28 | 29 | return $this 30 | ->subject('Password reset') 31 | ->to($this->user->getEmail(), $this->user->getName()) 32 | ->markdown('mail.user.password_reset') 33 | ->with([ 34 | 'user' => $this->user, 35 | 'password_reset_url' => $passwordResetUrl, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/Dispense.php: -------------------------------------------------------------------------------- 1 | 'datetime', 18 | ]; 19 | 20 | public function getExpiresAt(): Carbon 21 | { 22 | return $this->getAttributeValue('expires_at'); 23 | } 24 | 25 | public function getToken(): string 26 | { 27 | return $this->getAttributeValue('token'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | Deleting::class, 22 | ]; 23 | 24 | private ?UserToken $currentToken = null; 25 | 26 | public function tokens(): HasMany 27 | { 28 | return $this->hasMany(UserToken::class); 29 | } 30 | 31 | public function getCurrentToken(): ?UserToken 32 | { 33 | return $this->currentToken; 34 | } 35 | 36 | public function setCurrentToken(UserToken $token) 37 | { 38 | $this->currentToken = $token; 39 | } 40 | 41 | public function getEmail(): string 42 | { 43 | return $this->getAttributeValue('email'); 44 | } 45 | 46 | public function getName(): string 47 | { 48 | return $this->getAttributeValue('name'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Models/UserToken.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 19 | } 20 | 21 | public function getUser(): User 22 | { 23 | return $this->getRelationValue('user'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Notifications/User/Email/CantUpdate.php: -------------------------------------------------------------------------------- 1 | token = $token; 20 | $this->email = $email; 21 | } 22 | 23 | public function via(): array 24 | { 25 | return ['mail']; 26 | } 27 | 28 | public function toMail(User $user): VerifyUpdateMail 29 | { 30 | return new VerifyUpdateMail($user, $this->token, $this->email); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Notifications/User/Invitation.php: -------------------------------------------------------------------------------- 1 | token = $token; 18 | } 19 | 20 | public function via(): array 21 | { 22 | return ['mail']; 23 | } 24 | 25 | public function toMail(User $user): InvitationMail 26 | { 27 | return new InvitationMail($user, $this->token); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Notifications/User/PasswordReset.php: -------------------------------------------------------------------------------- 1 | token = $token; 18 | } 19 | 20 | public function via(): array 21 | { 22 | return ['mail']; 23 | } 24 | 25 | public function toMail(User $user): PasswordResetMail 26 | { 27 | return new PasswordResetMail($user, $this->token); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerPolicies(); 32 | 33 | $this->registerSpaRequestGuard(); 34 | } 35 | 36 | /** 37 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 38 | */ 39 | private function registerSpaRequestGuard(): void 40 | { 41 | /** @var AuthManager $authManager */ 42 | $authManager = $this->app->make(AuthManager::class); 43 | 44 | $authManager->viaRequest('spa', static function (Request $request): User { 45 | /** @var UserToken $userToken */ 46 | $userToken = UserToken::query() 47 | ->with('user') 48 | ->where('token', $request->bearerToken()) 49 | ->whereNotNull('token') 50 | ->firstOr(static function () { 51 | throw new AuthenticationException(); 52 | }); 53 | 54 | $user = $userToken->getUser(); 55 | 56 | $user->setCurrentToken($userToken); 57 | 58 | $userToken->touch(); 59 | 60 | return $user; 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 20 | CleanUp::class, 21 | ], 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 28 | 29 | $this->routes(function () { 30 | $router = $this->app->make(Router::class); 31 | 32 | $this->mapApiRoutes($router); 33 | 34 | $this->mapWebRoutes($router); 35 | }); 36 | } 37 | 38 | /** 39 | * Configure the rate limiters for the application. 40 | * 41 | * @return void 42 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 43 | */ 44 | private function configureRateLimiting() 45 | { 46 | /** @var RateLimiter $rateLimiter */ 47 | $rateLimiter = $this->app->make(RateLimiter::class); 48 | 49 | $rateLimiter->for('spa_login_lock', function (Request $request) { 50 | return new Limit($this->resolveRequestSignature($request), 15, 5); 51 | }); 52 | 53 | $rateLimiter->for('spa_invitation_lock', function (Request $request) { 54 | return new Limit($this->resolveRequestSignature($request), 15, 5); 55 | }); 56 | 57 | $rateLimiter->for('spa_password_reset_lock', function (Request $request) { 58 | return new Limit($this->resolveRequestSignature($request), 15, 5); 59 | }); 60 | } 61 | 62 | private function resolveRequestSignature(Request $request) 63 | { 64 | if ($user = $request->user()) { 65 | return sha1($user->getAuthIdentifier()); 66 | } elseif ($route = $request->route()) { 67 | return sha1($route->getDomain() . '|' . $request->ip()); 68 | } 69 | 70 | throw new RuntimeException('Unable to generate the request signature. Route unavailable.'); 71 | } 72 | 73 | /** 74 | * Define the "api" routes for the application. 75 | * 76 | * These routes are typically stateless. 77 | * 78 | * @param \Illuminate\Routing\Router $router 79 | * @return void 80 | */ 81 | private function mapApiRoutes(Router $router): void 82 | { 83 | $router->middleware('api')->namespace($this->namespace)->group(base_path('routes/api.php')); 84 | } 85 | 86 | /** 87 | * Define the "web" routes for the application. 88 | * 89 | * These routes all receive session state, CSRF protection, etc. 90 | * 91 | * @param \Illuminate\Routing\Router $router 92 | * @return void 93 | */ 94 | private function mapWebRoutes(Router $router): void 95 | { 96 | $router->middleware('web')->namespace($this->namespace)->group(base_path('routes/web.php')); 97 | } 98 | 99 | public function register() 100 | { 101 | parent::register(); 102 | 103 | $this->app->singleton(ResponseFactoryContract::class, static function (Container $container) { 104 | return $container->make(ResponseFactory::class); 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/Providers/SPAServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(Repository::class); 23 | 24 | $this->app->singleton(UrlGenerator::class, static fn() => new UrlGenerator($config->get('spa.url'))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/SPA/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | baseUrl = $baseUrl; 17 | } 18 | 19 | public function to(string $path, array $query = []): string 20 | { 21 | $query = array_filter($query); 22 | 23 | if (count($query) === 0) { 24 | return $this->format($path); 25 | } 26 | 27 | return $this->format($path) . '?' . http_build_query($query); 28 | } 29 | 30 | private function format(string $path): string 31 | { 32 | return $this->baseUrl . (Str::startsWith($path, '/') ? $path : '/' . $path); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kingscode/laravel-api-starter", 3 | "type": "project", 4 | "description": "The KingsCode Laravel Api Starter.", 5 | "keywords": [ 6 | "laravel", 7 | "api", 8 | "starter" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "^8.2", 13 | "doctrine/dbal": "^3.0", 14 | "fruitcake/php-cors": "^1.0", 15 | "guzzlehttp/guzzle": "^7.3", 16 | "koenhoeijmakers/headers": "^1.0", 17 | "koenhoeijmakers/laravel-filterable": "^6", 18 | "laravel/framework": "^10", 19 | "sentry/sentry-laravel": "^3.2" 20 | }, 21 | "require-dev": { 22 | "fakerphp/faker": "^1.14", 23 | "mockery/mockery": "^1.4", 24 | "nunomaduro/collision": "^7.1", 25 | "phpunit/phpunit": "^10", 26 | "roave/security-advisories": "dev-latest" 27 | }, 28 | "config": { 29 | "optimize-autoloader": true, 30 | "preferred-install": "dist", 31 | "sort-packages": true, 32 | "allow-plugins": { 33 | "php-http/discovery": true 34 | } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "dont-discover": [] 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "App\\": "app/", 44 | "Database\\Factories\\": "database/factories/", 45 | "Database\\Seeders\\": "database/seeders/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Tests\\": "tests/" 51 | } 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true, 55 | "scripts": { 56 | "post-autoload-dump": [ 57 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 58 | "@php artisan package:discover --ansi" 59 | ], 60 | "post-root-package-install": [ 61 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 62 | ], 63 | "post-create-project-cmd": [ 64 | "@php artisan key:generate --ansi" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'encrypted' => true, 41 | ], 42 | ], 43 | 44 | 'redis' => [ 45 | 'driver' => 'redis', 46 | 'connection' => 'default', 47 | ], 48 | 49 | 'log' => [ 50 | 'driver' => 'log', 51 | ], 52 | 53 | 'null' => [ 54 | 'driver' => 'null', 55 | ], 56 | 57 | ], 58 | 59 | ]; 60 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Cache Stores 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may define all of the cache "stores" for your application as 29 | | well as their drivers. You may even define multiple stores for the 30 | | same cache driver to group types of items stored in your caches. 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | ], 43 | 44 | 'database' => [ 45 | 'driver' => 'database', 46 | 'table' => 'cache', 47 | 'connection' => null, 48 | ], 49 | 50 | 'file' => [ 51 | 'driver' => 'file', 52 | 'path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => 'cache', 77 | ], 78 | 79 | 'dynamodb' => [ 80 | 'driver' => 'dynamodb', 81 | 'key' => env('AWS_ACCESS_KEY_ID'), 82 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 83 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 84 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 85 | ], 86 | 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Cache Key Prefix 92 | |-------------------------------------------------------------------------- 93 | | 94 | | When utilizing a RAM based store such as APC or Memcached, there might 95 | | be other applications utilizing the same cache. So, we'll specify a 96 | | value to get prefixed to all our keys so we can avoid collisions. 97 | | 98 | */ 99 | 100 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'), 101 | 102 | ]; 103 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['*'], 21 | 22 | 'allowed_methods' => ['*'], 23 | 24 | 'allowed_origins' => ['*'], 25 | 26 | 'allowed_origins_patterns' => [], 27 | 28 | 'allowed_headers' => ['*'], 29 | 30 | 'exposed_headers' => [ 31 | Header::CACHE_CONTROL, 32 | Header::CONTENT_LANGUAGE, 33 | Header::CONTENT_LENGTH, 34 | Header::CONTENT_TYPE, 35 | Header::EXPIRES, 36 | Header::LAST_MODIFIED, 37 | Header::PRAGMA, 38 | Header::RETRY_AFTER, 39 | ], 40 | 41 | 'max_age' => false, 42 | 43 | 'supports_credentials' => false, 44 | 45 | ]; 46 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Database Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here are each of the database connections setup for your application. 26 | | Of course, examples of configuring each database platform that is 27 | | supported by Laravel is shown below to make development simple. 28 | | 29 | | 30 | | All database work in Laravel is done through the PHP PDO facilities 31 | | so make sure you have the driver for your particular database of 32 | | choice installed on your machine before you begin development. 33 | | 34 | */ 35 | 36 | 'connections' => [ 37 | 38 | 'mysql' => [ 39 | 'driver' => 'mysql', 40 | 'url' => env('DATABASE_URL'), 41 | 'host' => env('DB_HOST', '127.0.0.1'), 42 | 'port' => env('DB_PORT', '3306'), 43 | 'database' => env('DB_DATABASE', 'forge'), 44 | 'username' => env('DB_USERNAME', 'forge'), 45 | 'password' => env('DB_PASSWORD', ''), 46 | 'unix_socket' => env('DB_SOCKET', ''), 47 | 'charset' => 'utf8mb4', 48 | 'collation' => 'utf8mb4_unicode_ci', 49 | 'prefix' => '', 50 | 'prefix_indexes' => true, 51 | 'strict' => true, 52 | 'engine' => 'InnoDB', 53 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 54 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 55 | ]) : [], 56 | ], 57 | 58 | ], 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Migration Repository Table 63 | |-------------------------------------------------------------------------- 64 | | 65 | | This table keeps track of all the migrations that have already run for 66 | | your application. Using this information, we can determine which of 67 | | the migrations on disk haven't actually been run in the database. 68 | | 69 | */ 70 | 71 | 'migrations' => 'migrations', 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Redis Databases 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Redis is an open source, fast, and advanced key-value store that also 79 | | provides a richer body of commands than a typical key-value system 80 | | such as APC or Memcached. Laravel makes it easy to dig right in. 81 | | 82 | */ 83 | 84 | 'redis' => [ 85 | 86 | 'client' => env('REDIS_CLIENT', 'phpredis'), 87 | 88 | 'options' => [ 89 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 90 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), 91 | ], 92 | 93 | 'default' => [ 94 | 'url' => env('REDIS_URL'), 95 | 'host' => env('REDIS_HOST', '127.0.0.1'), 96 | 'password' => env('REDIS_PASSWORD', null), 97 | 'port' => env('REDIS_PORT', '6379'), 98 | 'database' => env('REDIS_DB', '0'), 99 | ], 100 | 101 | 'cache' => [ 102 | 'url' => env('REDIS_URL'), 103 | 'host' => env('REDIS_HOST', '127.0.0.1'), 104 | 'password' => env('REDIS_PASSWORD', null), 105 | 'port' => env('REDIS_PORT', '6379'), 106 | 'database' => env('REDIS_DB', '1'), 107 | ], 108 | 109 | ], 110 | 111 | ]; 112 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Cloud Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Many applications store files both locally and in the cloud. For this 24 | | reason, you may specify a default "cloud" driver here. This driver 25 | | will be bound as the Cloud disk implementation in the container. 26 | | 27 | */ 28 | 29 | 'cloud' => env('FILESYSTEM_CLOUD', 's3'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Filesystem Disks 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure as many filesystem "disks" as you wish, and you 37 | | may even configure multiple disks of the same driver. Defaults have 38 | | been setup for each driver as an example of the required options. 39 | | 40 | | Supported Drivers: "local", "ftp", "sftp", "s3", "rackspace" 41 | | 42 | */ 43 | 44 | 'disks' => [ 45 | 46 | 'local' => [ 47 | 'driver' => 'local', 48 | 'root' => storage_path('app'), 49 | ], 50 | 51 | 'public' => [ 52 | 'driver' => 'local', 53 | 'root' => storage_path('app/public'), 54 | 'url' => env('APP_URL') . '/storage', 55 | 'visibility' => 'public', 56 | ], 57 | 58 | 's3' => [ 59 | 'driver' => 's3', 60 | 'key' => env('AWS_ACCESS_KEY_ID'), 61 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 62 | 'region' => env('AWS_DEFAULT_REGION'), 63 | 'bucket' => env('AWS_BUCKET'), 64 | 'url' => env('AWS_URL'), 65 | 'endpoint' => env('AWS_ENDPOINT'), 66 | ], 67 | 68 | ], 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Symbolic Links 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Here you may configure the symbolic links that will be created when the 76 | | `storage:link` Artisan command is executed. The array keys should be 77 | | the locations of the links and the values should be their targets. 78 | | 79 | */ 80 | 81 | 'links' => [ 82 | public_path('storage') => storage_path('app/public'), 83 | ], 84 | 85 | ]; 86 | -------------------------------------------------------------------------------- /config/filterable.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'sort_by' => 'sort_by', 18 | 'sort_desc' => 'desc', 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 1024, 48 | 'threads' => 2, 49 | 'time' => 2, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Log Channels 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Here you may configure the log channels for your application. Out of 28 | | the box, Laravel uses the Monolog PHP logging library. This gives 29 | | you a variety of powerful log handlers / formatters to utilize. 30 | | 31 | | Available Drivers: "single", "daily", "slack", "syslog", 32 | | "errorlog", "monolog", 33 | | "custom", "stack" 34 | | 35 | */ 36 | 37 | 'channels' => [ 38 | 'stack' => [ 39 | 'driver' => 'stack', 40 | 'channels' => ['daily'], 41 | 'ignore_exceptions' => false, 42 | ], 43 | 44 | 'single' => [ 45 | 'driver' => 'single', 46 | 'path' => storage_path('logs/laravel.log'), 47 | 'level' => 'debug', 48 | ], 49 | 50 | 'daily' => [ 51 | 'driver' => 'daily', 52 | 'path' => storage_path('logs/laravel.log'), 53 | 'level' => 'debug', 54 | 'days' => 14, 55 | ], 56 | 57 | 'slack' => [ 58 | 'driver' => 'slack', 59 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 60 | 'username' => 'Laravel Log', 61 | 'emoji' => ':boom:', 62 | 'level' => 'critical', 63 | ], 64 | 65 | 'papertrail' => [ 66 | 'driver' => 'monolog', 67 | 'level' => 'debug', 68 | 'handler' => SyslogUdpHandler::class, 69 | 'handler_with' => [ 70 | 'host' => env('PAPERTRAIL_URL'), 71 | 'port' => env('PAPERTRAIL_PORT'), 72 | ], 73 | ], 74 | 75 | 'stderr' => [ 76 | 'driver' => 'monolog', 77 | 'handler' => StreamHandler::class, 78 | 'formatter' => env('LOG_STDERR_FORMATTER'), 79 | 'with' => [ 80 | 'stream' => 'php://stderr', 81 | ], 82 | ], 83 | 84 | 'syslog' => [ 85 | 'driver' => 'syslog', 86 | 'level' => 'debug', 87 | ], 88 | 89 | 'errorlog' => [ 90 | 'driver' => 'errorlog', 91 | 'level' => 'debug', 92 | ], 93 | 94 | 'null' => [ 95 | 'driver' => 'monolog', 96 | 'handler' => NullHandler::class, 97 | ], 98 | 99 | 'emergency' => [ 100 | 'path' => storage_path('logs/laravel.log'), 101 | ], 102 | ], 103 | 104 | ]; 105 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'smtp'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Mailer Configurations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure all of the mailers used by your application plus 24 | | their respective settings. Several examples have been configured for 25 | | you and you are free to add your own as your application requires. 26 | | 27 | | Laravel supports a variety of mail "transport" drivers to be used while 28 | | sending an e-mail. You will specify which one you are using for your 29 | | mailers below. You are free to add additional mailers as required. 30 | | 31 | | Supported: "smtp", "sendmail", "mailgun", "ses", 32 | | "postmark", "log", "array" 33 | | 34 | */ 35 | 36 | 'mailers' => [ 37 | 'smtp' => [ 38 | 'transport' => 'smtp', 39 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 40 | 'port' => env('MAIL_PORT', 587), 41 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 42 | 'username' => env('MAIL_USERNAME'), 43 | 'password' => env('MAIL_PASSWORD'), 44 | ], 45 | 46 | 'ses' => [ 47 | 'transport' => 'ses', 48 | ], 49 | 50 | 'sendmail' => [ 51 | 'transport' => 'sendmail', 52 | 'path' => '/usr/sbin/sendmail -bs', 53 | ], 54 | 55 | 'mailgun' => [ 56 | 'transport' => 'mailgun', 57 | ], 58 | 59 | 'log' => [ 60 | 'transport' => 'log', 61 | 'channel' => env('MAIL_LOG_CHANNEL'), 62 | ], 63 | 64 | 'array' => [ 65 | 'transport' => 'array', 66 | ], 67 | ], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Global "From" Address 72 | |-------------------------------------------------------------------------- 73 | | 74 | | You may wish for all e-mails sent by your application to be sent from 75 | | the same address. Here, you may specify a name and address that is 76 | | used globally for all e-mails that are sent by your application. 77 | | 78 | */ 79 | 80 | 'from' => [ 81 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 82 | 'name' => env('MAIL_FROM_NAME', 'Example'), 83 | ], 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Markdown Mail Settings 88 | |-------------------------------------------------------------------------- 89 | | 90 | | If you are using Markdown based email rendering, you may configure your 91 | | theme and component paths here, allowing you to customize the design 92 | | of the emails. Or, you may simply stick with the Laravel defaults! 93 | | 94 | */ 95 | 96 | 'markdown' => [ 97 | 'theme' => 'default', 98 | 99 | 'paths' => [ 100 | resource_path('views/vendor/mail'), 101 | ], 102 | ], 103 | 104 | ]; 105 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => 'default', 48 | 'retry_after' => 90, 49 | 'block_for' => 0, 50 | ], 51 | 52 | 'sqs' => [ 53 | 'driver' => 'sqs', 54 | 'key' => env('AWS_ACCESS_KEY_ID'), 55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 58 | 'suffix' => env('SQS_SUFFIX'), 59 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 60 | ], 61 | 62 | 'redis' => [ 63 | 'driver' => 'redis', 64 | 'connection' => 'default', 65 | 'queue' => env('REDIS_QUEUE', 'default'), 66 | 'retry_after' => 90, 67 | 'block_for' => null, 68 | ], 69 | 70 | ], 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Failed Queue Jobs 75 | |-------------------------------------------------------------------------- 76 | | 77 | | These options configure the behavior of failed queue job logging so you 78 | | can control which database and table are used to store the jobs that 79 | | have failed. You may change them to any database / table you wish. 80 | | 81 | */ 82 | 83 | 'failed' => [ 84 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 85 | 'database' => env('DB_CONNECTION', 'mysql'), 86 | 'table' => 'failed_jobs', 87 | ], 88 | 89 | ]; 90 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | 'stripe' => [ 34 | 'model' => App\Models\User::class, 35 | 'key' => env('STRIPE_KEY'), 36 | 'secret' => env('STRIPE_SECRET'), 37 | 'webhook' => [ 38 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 39 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 40 | ], 41 | ], 42 | 43 | ]; 44 | -------------------------------------------------------------------------------- /config/spa.php: -------------------------------------------------------------------------------- 1 | env('SPA_URL', 'http://localhost:8080'), 5 | 'force_url' => env('SPA_FORCE_URL', true), 6 | ]; 7 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Blade View Modification Checking 39 | |-------------------------------------------------------------------------- 40 | | 41 | | On every request the framework will check to see if a view has expired 42 | | to determine if it needs to be recompiled. If you are in production 43 | | and precompiling views this feature may be disabled to save time. 44 | | 45 | */ 46 | 47 | 'expires' => env('VIEW_CHECK_EXPIRATION', true), 48 | 49 | ]; 50 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.sqlite-journal 3 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 19 | 'email' => "{$this->faker->unique()->word}+las@kingscode.nl", 20 | 'password' => '$2y$10$OwXT/21pAv5OSIXyG1caxexYkL0uDJQSadZ4f46Y5AjmfSJtdkphu', 21 | 'remember_token' => Str::random(10), 22 | ]; 23 | } 24 | 25 | /** 26 | * @param array $attributes 27 | * @return \Illuminate\Database\Eloquent\Model|\App\Models\User 28 | */ 29 | public function createOne($attributes = []) 30 | { 31 | return parent::createOne($attributes); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('name'); 16 | $table->string('email')->unique(); 17 | $table->string('password'); 18 | $table->rememberToken(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('users'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 15 | $table->string('token'); 16 | $table->timestamp('created_at')->nullable(); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | Schema::dropIfExists('password_resets'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/2019_05_15_083540_create_user_invitations_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 15 | $table->string('token'); 16 | $table->timestamp('created_at')->nullable(); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | Schema::dropIfExists('user_invitations'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('uuid')->unique(); 16 | $table->text('connection'); 17 | $table->text('queue'); 18 | $table->longText('payload'); 19 | $table->longText('exception'); 20 | $table->timestamp('failed_at')->useCurrent(); 21 | }); 22 | } 23 | 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('failed_jobs'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/2020_01_23_104553_create_user_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->foreignId('user_id'); 16 | $table->string('token')->unique(); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down(): void 22 | { 23 | Schema::dropIfExists('user_tokens'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2020_08_05_061418_create_dispenses_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('key'); 16 | $table->string('token'); 17 | $table->timestamp('expires_at')->nullable(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | public function down(): void 23 | { 24 | Schema::dropIfExists('dispenses'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /database/seeders/UsersTableSeeder.php: -------------------------------------------------------------------------------- 1 | where('email', 'info@kingscode.nl')->doesntExist()) { 16 | UserFactory::new()->createOne([ 17 | 'email' => 'info@kingscode.nl', 18 | ]); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | redis: 5 | image: redis:latest 6 | restart: always 7 | ports: 8 | - "6379:6379" 9 | networks: 10 | - default 11 | 12 | db: 13 | image: mysql:8 14 | restart: always 15 | command: --default-authentication-plugin=mysql_native_password 16 | environment: 17 | MYSQL_ROOT_PASSWORD: secret 18 | MYSQL_DATABASE: application 19 | ports: 20 | - "3306:3306" 21 | volumes: 22 | - mysql-data:/var/lib/mysql 23 | networks: 24 | - default 25 | 26 | adminer: 27 | image: adminer:latest 28 | restart: always 29 | ports: 30 | - "4444:8080" 31 | networks: 32 | - default 33 | 34 | app: 35 | build: 36 | context: . 37 | dockerfile: .docker/Dockerfile 38 | target: dev 39 | restart: always 40 | depends_on: 41 | - db 42 | - redis 43 | ports: 44 | - "80:80" 45 | environment: 46 | - PORT=80 47 | volumes: 48 | - ./:/var/www/ 49 | networks: 50 | - default 51 | 52 | mailhog: 53 | image: mailhog/mailhog 54 | ports: 55 | - "1025:1025" 56 | - "8025:8025" 57 | networks: 58 | - default 59 | 60 | volumes: 61 | web-app: 62 | driver: local 63 | mysql-data: 64 | driver: local 65 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | ]; 19 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least ten characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed you a password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => 'We can\'t find a user with that e-mail address.', 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./app 6 | 7 | 8 | ./app/Http/Responses/ResponseFactory.php 9 | 10 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | ./tests/Feature 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | # 2 | # Header unset X-Powered-By 3 | # Header unset Server 4 | # 5 | # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS 6 | # Header always set X-XSS-Protection "1; mode=block" 7 | # Header always set X-Frame-Options SAMEORIGIN 8 | # Header always set X-Content-Type-Options nosniff 9 | # Header always set Referrer-Policy Same-Origin 10 | # 11 | 12 | 13 | 14 | Options -MultiViews -Indexes 15 | 16 | 17 | RewriteEngine On 18 | 19 | # Handle Authorization Header 20 | RewriteCond %{HTTP:Authorization} . 21 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 22 | 23 | # # Handle HTTP Calls 24 | # RewriteCond %{REMOTE_ADDR} !=127.0.0.1 25 | # RewriteCond %{REMOTE_ADDR} !=::1 26 | # RewriteCond %{HTTPS} off 27 | # RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] 28 | 29 | # Redirect Trailing Slashes If Not A Folder... 30 | RewriteCond %{REQUEST_FILENAME} !-d 31 | RewriteCond %{REQUEST_URI} (.+)/$ 32 | RewriteRule ^ %1 [L,R=301] 33 | 34 | # Handle Front Controller... 35 | RewriteCond %{REQUEST_FILENAME} !-d 36 | RewriteCond %{REQUEST_FILENAME} !-f 37 | RewriteRule ^ index.php [L] 38 | 39 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = tap($kernel->handle( 52 | $request = Request::capture() 53 | ))->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Api Starter 2 | 3 | ![PHPUnit](https://github.com/kingscode/laravel-api-starter/workflows/PHPUnit/badge.svg) 4 | 5 | Our base `laravel/laravel` installation for `vue` front-end applications. 6 | 7 | ## Usage 8 | Use this repository as a template repository in github or clone the repository. 9 | 10 | After install there will be a default user with the following credentials. 11 | ```bash 12 | email: info@kingscode.nl 13 | password: secret 14 | ``` 15 | 16 | ## Installation 17 | ### With Docker 18 | Docker helps a ton by providing us a unison development environment that allows us to quickly install new dependencies and share the configuration of those. 19 | 20 | Begin by pulling the docker containers and booting docker. 21 | ```bash 22 | docker-compose up --build -d 23 | ``` 24 | 25 | Then get into the `app` container. 26 | ```bash 27 | docker exec -it app bash 28 | ``` 29 | 30 | Where you'll run the following commands. 31 | ```bash 32 | cp .env.example .env 33 | composer install 34 | pa key:generate 35 | pa migrate 36 | pa db:seed 37 | ``` 38 | 39 | ### Without Docker 40 | You can also run without Docker, but you'll have to do the walk of atonement. 41 | 42 | Start by setting up your environment and installing dependencies. 43 | 44 | Then run the following to copy the `.env` file and fill it accordingly: 45 | ```bash 46 | cp .env.example .env 47 | ``` 48 | 49 | Then run the following commands to get it all booted up. 50 | ```bash 51 | composer install 52 | php artisan key:generate 53 | php artisan migrate 54 | php artisan db:seed 55 | ``` 56 | -------------------------------------------------------------------------------- /resources/views/mail/registration/already_exists.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | // @formatter:off 3 | @endphp 4 | 5 | @component('mail::message') 6 | # Dear, 7 | 8 | Somebody has tried to create a new account but you already have one. 9 | If you have forgotten your password then you can request a new one. 10 | 11 | @component('mail::button', ['url' => $password_forgotten_url]) 12 | Request new password 13 | @endcomponent 14 | 15 | Yours,
16 | {{ config('app.name') }} 17 | 18 | @slot('subcopy') 19 | If you’re having trouble clicking the "Request new password" button, copy and paste the URL below
20 | into your web browser: [{{ $password_forgotten_url }}]({{ $password_forgotten_url }}) 21 | @endslot 22 | @endcomponent 23 | -------------------------------------------------------------------------------- /resources/views/mail/registration/verify.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | // @formatter:off 3 | @endphp 4 | 5 | @component('mail::message') 6 | # Dear, 7 | 8 | You are receiving this email because you've tried to create an account on [{{ config('app.name') }}]({{ $front_end_url }})
9 | Please click on the button below to verify your account. 10 | 11 | @component('mail::button', ['url' => $verify_url]) 12 | Verify 13 | @endcomponent 14 | 15 | Yours,
16 | {{ config('app.name') }} 17 | 18 | @slot('subcopy') 19 | If you’re having trouble clicking the "Request new password" button, copy and paste the URL below
20 | into your web browser: [{{ $verify_url }}]({{ $verify_url }}) 21 | @endslot 22 | @endcomponent 23 | -------------------------------------------------------------------------------- /resources/views/mail/user/email/cant_update.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | // @formatter:off 3 | 4 | /** @var \App\Models\User $user */ 5 | @endphp 6 | 7 | @component('mail::message') 8 | # Dear {{ $user->getName() }}, 9 | 10 | You are receiving this email because you've requested to change your email address. 11 | 12 | Sadly, you already have an account with this email address in place and we are unable change it. 13 | 14 | Yours,
15 | {{ config('app.name') }} 16 | @endcomponent 17 | -------------------------------------------------------------------------------- /resources/views/mail/user/email/verify_update.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | // @formatter:off 3 | 4 | /** @var \App\Models\User $user */ 5 | @endphp 6 | 7 | @component('mail::message') 8 | # Dear {{ $user->getName() }}, 9 | 10 | You are receiving this email because you've requested to change your email address.
11 | To verify this update click on the button below. 12 | 13 | @component('mail::button', ['url' => $verify_url]) 14 | Verify email 15 | @endcomponent 16 | 17 | Yours,
18 | {{ config('app.name') }} 19 | 20 | @slot('subcopy') 21 | If you’re having trouble clicking the "Activate" button, copy and paste the URL below into your web browser: 22 | [{{ $verify_url }}]({{ $verify_url }}) 23 | @endslot 24 | @endcomponent 25 | -------------------------------------------------------------------------------- /resources/views/mail/user/invitation.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | // @formatter:off 3 | 4 | /** @var \App\Models\User $user */ 5 | @endphp 6 | 7 | @component('mail::message') 8 | # Dear {{ $user->getName() }}, 9 | 10 | Activate your account via the button below. 11 | 12 | @component('mail::button', ['url' => $acceptation_url]) 13 | Activate 14 | @endcomponent 15 | 16 | Yours,
17 | {{ config('app.name') }} 18 | 19 | @slot('subcopy') 20 | If you’re having trouble clicking the "Activate" button, copy and paste the URL below into your web browser: 21 | [{{ $acceptation_url }}]({{ $acceptation_url }}) 22 | @endslot 23 | @endcomponent 24 | -------------------------------------------------------------------------------- /resources/views/mail/user/password_reset.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | // @formatter:off 3 | 4 | /** @var \App\Models\User $user */ 5 | @endphp 6 | 7 | @component('mail::message') 8 | # Dear {{ $user->getName() }}, 9 | 10 | Forgotten your password? Don't worry! It happens to the best of us. 11 | 12 | @component('mail::button', ['url' => $password_reset_url]) 13 | Reset password 14 | @endcomponent 15 | 16 | This email is valid for 15 minutes. 17 | 18 | Yours,
19 | {{ config('app.name') }} 20 | 21 | @slot('subcopy') 22 | If you’re having trouble clicking the "Activate" button, copy and paste the URL below into your web browser: 23 | [{{ $password_reset_url }}]({{ $password_reset_url }}) 24 | @endslot 25 | @endcomponent 26 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | namespace('Api')->group(function (Router $router) { 12 | $router->namespace('Auth')->prefix('auth')->middleware('throttle:spa_login_lock')->group(function (Router $router) { 13 | $router->post('login', 'Login'); 14 | $router->post('dispense', 'Dispense'); 15 | }); 16 | 17 | $router->namespace('Invitation')->prefix('invitation')->middleware('throttle:spa_invitation_lock')->group(function (Router $router) { 18 | $router->post('resend', 'Resend'); 19 | $router->post('accept', 'Accept'); 20 | }); 21 | 22 | $router->namespace('Password')->prefix('password')->middleware('throttle:spa_password_reset_lock')->group(function (Router $router) { 23 | $router->post('reset', 'Reset'); 24 | $router->post('forgotten', 'Forgotten'); 25 | }); 26 | 27 | $router->namespace('Registration')->prefix('registration')->group(function (Router $router) { 28 | $router->post('', 'Store'); 29 | $router->post('verify', 'Verify'); 30 | }); 31 | 32 | $router->middleware('auth:api')->group(function (Router $router) { 33 | $router->namespace('Auth')->prefix('auth')->group(function (Router $router) { 34 | $router->post('logout', 'Logout'); 35 | }); 36 | 37 | $router->namespace('Profile')->prefix('profile')->group(function (Router $router) { 38 | $router->namespace('Email')->prefix('email')->group(function (Router $router) { 39 | $router->post('verify', 'Verify'); 40 | $router->put('', 'Update'); 41 | }); 42 | 43 | $router->namespace('Password')->prefix('password')->group(function (Router $router) { 44 | $router->put('', 'Update'); 45 | }); 46 | 47 | $router->put('', 'Update'); 48 | $router->get('', 'Show'); 49 | }); 50 | 51 | $router->namespace('User')->prefix('user')->group(function (Router $router) { 52 | $router->delete('{user}', 'Destroy'); 53 | $router->put('{user}', 'Update'); 54 | $router->get('{user}', 'Show'); 55 | $router->post('', 'Store'); 56 | $router->get('', 'Index'); 57 | }); 58 | }); 59 | }); 60 | 61 | // @formatter:on 62 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /stubs/console.stub: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->timestamps(); 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /stubs/migration.stub: -------------------------------------------------------------------------------- 1 | get('/'); 14 | 15 | $response->assertStatus(200); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /stubs/test.unit.stub: -------------------------------------------------------------------------------- 1 | assertTrue(true); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /supervisord.pid: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 22 | 23 | return $app; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Auth/LoginTest.php: -------------------------------------------------------------------------------- 1 | createOne([ 17 | 'password' => bcrypt('kingscodedotnl'), 18 | ]); 19 | 20 | $response = $this->json('post', 'auth/login', [ 21 | 'email' => $user->email, 22 | 'password' => 'kingscodedotnl', 23 | ]); 24 | 25 | $response->assertOk(); 26 | 27 | $response->assertJsonStructure([ 28 | 'data' => [ 29 | 'token', 30 | ], 31 | ]); 32 | } 33 | 34 | public function testNonExistentEmail() 35 | { 36 | $response = $this->json('post', 'auth/login', [ 37 | 'email' => 'info@kingscode.nl', 38 | 'password' => 'kingscodedotnl', 39 | ]); 40 | 41 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 42 | 'email', 43 | ]); 44 | } 45 | 46 | public function testWrongPassword() 47 | { 48 | $user = UserFactory::new()->createOne([ 49 | 'password' => bcrypt('kingscodedotnl'), 50 | ]); 51 | 52 | $response = $this->json('post', 'auth/login', [ 53 | 'email' => $user->email, 54 | 'password' => 'pooper', 55 | ]); 56 | 57 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 58 | 'email', 59 | ]); 60 | } 61 | 62 | public function testValidationErrors() 63 | { 64 | $response = $this->json('post', 'auth/login'); 65 | 66 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 67 | 'email', 'password', 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Auth/LogoutTest.php: -------------------------------------------------------------------------------- 1 | createOne([ 17 | 'password' => bcrypt('kingscodedotnl'), 18 | ]); 19 | $user->tokens()->create(['token' => 'yayeet']); 20 | 21 | $response = $this->json('post', 'auth/logout', [], [ 22 | Header::AUTHORIZATION => 'Bearer yayeet', 23 | ]); 24 | 25 | $response->assertOk(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/GuardTest.php: -------------------------------------------------------------------------------- 1 | app->make(Router::class); 23 | 24 | $router->middleware(['api', 'auth:api'])->get('test', function () { 25 | return new JsonResponse(['message' => 'ok']); 26 | }); 27 | } 28 | 29 | public function testUnauthorized() 30 | { 31 | $response = $this->json('get', 'test'); 32 | 33 | $response->assertStatus(Response::HTTP_UNAUTHORIZED); 34 | } 35 | 36 | public function testCorrectToken() 37 | { 38 | $user = UserFactory::new()->createOne(); 39 | $userToken = $user->tokens()->create(['token' => 'yayeet']); 40 | 41 | $now = $userToken->created_at->copy()->addDays(7); 42 | 43 | Carbon::setTestNow($now); 44 | 45 | $response = $this->json('get', 'test', [], [ 46 | Header::AUTHORIZATION => 'Bearer yayeet', 47 | ]); 48 | 49 | $response->assertOk(); 50 | 51 | $this->assertDatabaseHas('user_tokens', [ 52 | 'updated_at' => $now, 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Invitation/AcceptTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 17 | 18 | $token = $this->app->make(PasswordBrokerManager::class)->broker('user-invitations')->createToken($user); 19 | 20 | $response = $this->json('post', 'invitation/accept', [ 21 | 'email' => $user->email, 22 | 'token' => $token, 23 | 'password' => 'kingscodedotnl', 24 | 'password_confirmation' => 'kingscodedotnl', 25 | ]); 26 | 27 | $response->assertOk(); 28 | } 29 | 30 | public function testWithNonExistentToken() 31 | { 32 | $user = UserFactory::new()->createOne(); 33 | 34 | $response = $this->json('post', 'invitation/accept', [ 35 | 'email' => $user->email, 36 | 'token' => 'does-not-exist', 37 | 'password' => 'kingscodedotnl', 38 | 'password_confirmation' => 'kingscodedotnl', 39 | ]); 40 | 41 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 42 | } 43 | 44 | public function testValidationErrors() 45 | { 46 | $response = $this->json('post', 'invitation/accept'); 47 | 48 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 49 | 'token', 'email', 'password', 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Invitation/ResendTest.php: -------------------------------------------------------------------------------- 1 | app->instance(Dispatcher::class, $fake = new NotificationFake()); 18 | 19 | $email = 'info@kingscode.nl'; 20 | 21 | $user = UserFactory::new()->createOne([ 22 | 'email' => $email, 23 | ]); 24 | 25 | $response = $this->json('post', 'invitation/resend', [ 26 | 'email' => $email, 27 | ]); 28 | 29 | $response->assertOk(); 30 | 31 | $fake->assertSentTo($user, Invitation::class); 32 | } 33 | 34 | public function testNotificationNotDispatched() 35 | { 36 | $this->app->instance(Dispatcher::class, $fake = new NotificationFake()); 37 | 38 | $email = 'info@kingscode.nl'; 39 | 40 | $response = $this->json('post', 'invitation/resend', [ 41 | 'email' => $email, 42 | ]); 43 | 44 | $response->assertOk(); 45 | 46 | $fake->assertNothingSent(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Password/ForgottenTest.php: -------------------------------------------------------------------------------- 1 | app->instance(Dispatcher::class, $fake = new NotificationFake()); 19 | 20 | $user = UserFactory::new()->createOne([ 21 | 'email' => 'info@kingscode.nl', 22 | ]); 23 | 24 | $response = $this->json('post', 'password/forgotten', [ 25 | 'email' => 'info@kingscode.nl', 26 | ]); 27 | 28 | $response->assertOk(); 29 | 30 | $fake->assertSentTo($user, PasswordReset::class); 31 | } 32 | 33 | public function testStatusIsOkEvenWhenUserDoesntExist() 34 | { 35 | $response = $this->json('post', 'password/forgotten', [ 36 | 'email' => 'info@kingscode.nl', 37 | ]); 38 | 39 | $response->assertOk(); 40 | } 41 | 42 | public function testValidationErrors() 43 | { 44 | $response = $this->json('post', 'password/forgotten'); 45 | 46 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 47 | 'email', 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Password/ResetTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 17 | 18 | $token = $this->app->make(PasswordBrokerManager::class)->broker('users')->createToken($user); 19 | 20 | $response = $this->json('post', 'password/reset', [ 21 | 'email' => $user->email, 22 | 'token' => $token, 23 | 'password' => 'kingscodedotnl', 24 | 'password_confirmation' => 'kingscodedotnl', 25 | ]); 26 | 27 | $response->assertOk();; 28 | } 29 | 30 | public function testWithNonExistentToken() 31 | { 32 | $user = UserFactory::new()->createOne(); 33 | 34 | $response = $this->json('post', 'password/reset', [ 35 | 'email' => $user->email, 36 | 'token' => 'does-not-exist', 37 | 'password' => 'kingscodedotnl', 38 | 'password_confirmation' => 'kingscodedotnl', 39 | ]); 40 | 41 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 42 | } 43 | 44 | public function testValidationErrors() 45 | { 46 | $response = $this->json('post', 'password/reset'); 47 | 48 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 49 | 'token', 'email', 'password', 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Profile/Email/UpdateTest.php: -------------------------------------------------------------------------------- 1 | app->instance(Dispatcher::class, $fake = new NotificationFake()); 20 | 21 | $email = 'info@kingscode.nl'; 22 | 23 | $user = UserFactory::new()->createOne([ 24 | 'email' => $email, 25 | ]); 26 | 27 | $response = $this->actingAs($user, 'api')->json('put', 'profile/email', [ 28 | 'email' => $email, 29 | ]); 30 | 31 | $response->assertOk(); 32 | 33 | $fake->assertSentTo($user, VerifyUpdate::class); 34 | } 35 | 36 | public function testCantUpdateEmailIsSent() 37 | { 38 | $this->app->instance(Dispatcher::class, $fake = new NotificationFake()); 39 | 40 | $email = 'info@kingscode.nl'; 41 | 42 | UserFactory::new()->createOne([ 43 | 'email' => $email, 44 | ]); 45 | 46 | $user = UserFactory::new()->createOne([ 47 | 'email' => 'yoink@dadoink.nl', 48 | ]); 49 | 50 | $response = $this->actingAs($user, 'api')->json('put', 'profile/email', [ 51 | 'email' => $email, 52 | ]); 53 | 54 | $response->assertOk(); 55 | 56 | $fake->assertSentTo($user, CantUpdate::class); 57 | } 58 | 59 | public function testValidationErrors() 60 | { 61 | $user = UserFactory::new()->createOne(); 62 | 63 | $response = $this->actingAs($user, 'api')->json('put', 'profile/email'); 64 | 65 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 66 | 'email', 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Profile/Email/VerifyTest.php: -------------------------------------------------------------------------------- 1 | dispensary = $this->app->make(EmailDispensary::class); 23 | } 24 | 25 | public function testVerification() 26 | { 27 | $email = 'info@kingscode.nl'; 28 | 29 | $user = UserFactory::new()->createOne(); 30 | 31 | $token = $this->dispensary->dispense($user, $email); 32 | 33 | $response = $this->actingAs($user, 'api')->json('post', 'profile/email/verify', [ 34 | 'email' => $email, 35 | 'token' => $token, 36 | ]); 37 | 38 | $response->assertOk(); 39 | } 40 | 41 | public function testBadRequestWhenWrongTokenPassed() 42 | { 43 | $email = 'info@kingscode.nl'; 44 | 45 | $user = UserFactory::new()->createOne(); 46 | 47 | $this->dispensary->dispense($user, $email); 48 | 49 | $response = $this->actingAs($user, 'api')->json('post', 'profile/email/verify', [ 50 | 'email' => $email, 51 | 'token' => 'zigzagzog', 52 | ]); 53 | 54 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 55 | } 56 | 57 | public function testBadRequestTokenExpired() 58 | { 59 | $email = 'info@kingscode.nl'; 60 | 61 | $user = UserFactory::new()->createOne(); 62 | 63 | $token = $this->dispensary->dispense($user, $email); 64 | 65 | /** @var Repository $repository */ 66 | $repository = $this->app->make(Repository::class); 67 | 68 | $repository->clear(); 69 | 70 | $response = $this->actingAs($user, 'api')->json('post', 'profile/email/verify', [ 71 | 'email' => $email, 72 | 'token' => $token, 73 | ]); 74 | 75 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 76 | } 77 | 78 | public function testValidationErrors() 79 | { 80 | $user = UserFactory::new()->createOne(); 81 | 82 | $response = $this->actingAs($user, 'api')->json('post', 'profile/email/verify'); 83 | 84 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 85 | 'email', 'token', 86 | ]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Profile/Password/UpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 18 | 19 | $response = $this->actingAs($user, 'api')->json('put', 'profile/password', [ 20 | 'password' => 'kingscodedotnl', 21 | 'password_confirmation' => 'kingscodedotnl', 22 | 'current_password' => 'secret', 23 | ]); 24 | 25 | $response->assertOk(); 26 | } 27 | 28 | public function testCurrentPasswordIncorrect() 29 | { 30 | $user = UserFactory::new()->createOne(); 31 | 32 | $response = $this->actingAs($user, 'api')->json('put', 'profile/password', [ 33 | 'password' => 'kingscodedotnl', 34 | 'password_confirmation' => 'kingscodedotnl', 35 | 'current_password' => 'secretiswrong', 36 | ]); 37 | 38 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 39 | 'current_password', 40 | ]); 41 | } 42 | 43 | public function testValidationErrors() 44 | { 45 | $user = UserFactory::new()->createOne(); 46 | 47 | $response = $this->actingAs($user, 'api')->json('put', 'profile/password'); 48 | 49 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 50 | 'password', 'current_password', 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Profile/ShowTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 15 | 16 | $response = $this->actingAs($user, 'api')->json('get', 'profile'); 17 | 18 | $response->assertOk()->assertJson([ 19 | 'data' => [ 20 | 'id' => $user->getKey(), 21 | 'name' => $user->name, 22 | 'email' => $user->email, 23 | ], 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Profile/UpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne([ 16 | 'email' => 'info@kingscode.nl', 17 | ]); 18 | 19 | $response = $this->actingAs($user, 'api')->json('put', 'profile', [ 20 | 'name' => 'King', 21 | ]); 22 | 23 | $response->assertOk(); 24 | 25 | $this->assertDatabaseHas('users', [ 26 | 'name' => 'King', 27 | ]); 28 | } 29 | 30 | public function testValidationErrors() 31 | { 32 | $user = UserFactory::new()->createOne(); 33 | 34 | $response = $this->actingAs($user, 'api')->json('put', 'profile'); 35 | 36 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 37 | 'name', 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Registration/StoreTest.php: -------------------------------------------------------------------------------- 1 | json('post', 'registration', [ 22 | 'name' => 'Kings Code', 23 | 'email' => 'info@kingscode.nl', 24 | ]); 25 | 26 | $response->assertOk(); 27 | 28 | $this->mailer->assertQueued(Verify::class, function (Verify $verify) { 29 | return $verify->hasTo('info@kingscode.nl'); 30 | }); 31 | 32 | $this->assertDatabaseHas('users', [ 33 | 'name' => 'Kings Code', 34 | 'email' => 'info@kingscode.nl', 35 | ]); 36 | } 37 | 38 | public function testEmailAlreadyExists() 39 | { 40 | UserFactory::new()->createOne([ 41 | 'email' => 'info@kingscode.nl', 42 | ]); 43 | 44 | $response = $this->json('post', 'registration', [ 45 | 'name' => 'Kings Code', 46 | 'email' => 'info@kingscode.nl', 47 | ]); 48 | 49 | $response->assertOk(); 50 | 51 | $this->mailer->assertQueued(AlreadyExists::class, function (AlreadyExists $verify) { 52 | return $verify->hasTo('info@kingscode.nl'); 53 | }); 54 | } 55 | 56 | public function testValidationErrors() 57 | { 58 | $response = $this->json('post', 'registration'); 59 | 60 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 61 | 'name', 'email', 62 | ]); 63 | } 64 | 65 | protected function setUp(): void 66 | { 67 | parent::setUp(); 68 | 69 | $this->mailer = Mail::fake(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/Registration/VerifyTest.php: -------------------------------------------------------------------------------- 1 | dispensary = $this->app->make(RegistrationDispensary::class); 23 | } 24 | 25 | public function testVerification() 26 | { 27 | $email = 'info@kingscode.nl'; 28 | 29 | $user = UserFactory::new()->createOne([ 30 | 'email' => $email, 31 | ]); 32 | 33 | $token = $this->dispensary->dispense($user); 34 | 35 | $response = $this->actingAs($user, 'api')->json('post', 'registration/verify', [ 36 | 'email' => $email, 37 | 'token' => $token, 38 | 'password' => 'secretatleast10charpassword', 39 | 'password_confirmation' => 'secretatleast10charpassword', 40 | ]); 41 | 42 | $response->assertOk(); 43 | } 44 | 45 | public function testBadRequestWhenWrongEmail() 46 | { 47 | $user = UserFactory::new()->createOne([ 48 | 'email' => 'wrong@kingscode.nl', 49 | ]); 50 | 51 | $token = $this->dispensary->dispense($user); 52 | 53 | $response = $this->actingAs($user, 'api')->json('post', 'registration/verify', [ 54 | 'email' => 'info@kingscode.nl', 55 | 'token' => $token, 56 | 'password' => 'secretatleast10charpassword', 57 | 'password_confirmation' => 'secretatleast10charpassword', 58 | ]); 59 | 60 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 61 | } 62 | 63 | public function testBadRequestWhenWrongTokenPassed() 64 | { 65 | $email = 'info@kingscode.nl'; 66 | 67 | $user = UserFactory::new()->createOne([ 68 | 'email' => $email, 69 | ]); 70 | 71 | $this->dispensary->dispense($user); 72 | 73 | $response = $this->actingAs($user, 'api')->json('post', 'registration/verify', [ 74 | 'email' => $email, 75 | 'token' => 'zigzagzog', 76 | 'password' => 'secretatleast10charpassword', 77 | 'password_confirmation' => 'secretatleast10charpassword', 78 | ]); 79 | 80 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 81 | } 82 | 83 | public function testBadRequestTokenExpired() 84 | { 85 | $email = 'info@kingscode.nl'; 86 | 87 | $user = UserFactory::new()->createOne([ 88 | 'email' => $email, 89 | ]); 90 | 91 | $token = $this->dispensary->dispense($user); 92 | 93 | /** @var Repository $repository */ 94 | $repository = $this->app->make(Repository::class); 95 | 96 | $repository->clear(); 97 | 98 | $response = $this->actingAs($user, 'api')->json('post', 'registration/verify', [ 99 | 'email' => $email, 100 | 'token' => $token, 101 | 'password' => 'secretatleast10charpassword', 102 | 'password_confirmation' => 'secretatleast10charpassword', 103 | ]); 104 | 105 | $response->assertStatus(Response::HTTP_BAD_REQUEST); 106 | } 107 | 108 | public function testValidationErrors() 109 | { 110 | $response = $this->json('post', 'registration/verify'); 111 | 112 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 113 | 'email', 'token', 'password', 114 | ]); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/User/DestroyTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | $user2 = UserFactory::new()->createOne(); 17 | 18 | $response = $this->actingAs($user1, 'api')->json('delete', "user/{$user2->getKey()}"); 19 | 20 | $response->assertOk(); 21 | 22 | $this->assertDatabaseMissing('users', [ 23 | 'id' => $user2->getKey(), 24 | ]); 25 | } 26 | 27 | public function testCantDeleteYourself() 28 | { 29 | $user = UserFactory::new()->createOne(); 30 | 31 | $response = $this->actingAs($user, 'api')->json('delete', "user/{$user->getKey()}"); 32 | 33 | $response->assertStatus(Response::HTTP_CONFLICT); 34 | 35 | $this->assertDatabaseHas('users', [ 36 | 'id' => $user->getKey(), 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/User/IndexTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 15 | 16 | $response = $this->actingAs($user, 'api')->json('get', 'user'); 17 | 18 | $response->assertOk()->assertExactJson([ 19 | 'data' => [ 20 | [ 21 | 'id' => $user->getKey(), 22 | 'name' => $user->name, 23 | 'email' => $user->email, 24 | ], 25 | ], 26 | 'meta' => [ 27 | 'current_page' => 1, 28 | 'last_page' => 1, 29 | 'from' => 1, 30 | 'to' => 1, 31 | 'total' => 1, 32 | 'per_page' => 15, 33 | ], 34 | ]); 35 | } 36 | 37 | public function testSearchingForName() 38 | { 39 | $user1 = UserFactory::new()->createOne([ 40 | 'name' => 'aaa', 41 | ]); 42 | $user2 = UserFactory::new()->createOne([ 43 | 'name' => 'bbb', 44 | ]); 45 | 46 | $response = $this->actingAs($user1, 'api')->json('get', 'user?search=bbb'); 47 | 48 | $response->assertOk(); 49 | 50 | $this->assertSame($user2->getKey(), $response->json('data.0.id')); 51 | } 52 | 53 | public function testSearchingForEmail() 54 | { 55 | $user1 = UserFactory::new()->createOne([ 56 | 'email' => 'info@kingscode.nl', 57 | ]); 58 | $user2 = UserFactory::new()->createOne([ 59 | 'email' => 'support@kingscode.nl', 60 | ]); 61 | 62 | $response = $this->actingAs($user1, 'api')->json('get', 'user?search=support'); 63 | 64 | $response->assertOk(); 65 | 66 | $this->assertSame($user2->getKey(), $response->json('data.0.id')); 67 | } 68 | 69 | public function testSortingByNameAsc() 70 | { 71 | $user2 = UserFactory::new()->createOne([ 72 | 'name' => 'bbb', 73 | ]); 74 | $user1 = UserFactory::new()->createOne([ 75 | 'name' => 'aaa', 76 | ]); 77 | 78 | $response = $this->actingAs($user1, 'api')->json('get', 'user?sort_by=name&desc=0'); 79 | 80 | $response->assertOk(); 81 | 82 | $this->assertSame($user1->getKey(), $response->json('data.0.id')); 83 | } 84 | 85 | public function testSortingByNameDesc() 86 | { 87 | $user2 = UserFactory::new()->createOne([ 88 | 'name' => 'bbb', 89 | ]); 90 | $user1 = UserFactory::new()->createOne([ 91 | 'name' => 'aaa', 92 | ]); 93 | 94 | $response = $this->actingAs($user1, 'api')->json('get', 'user?sort_by=name&desc=1'); 95 | 96 | $response->assertOk(); 97 | 98 | $this->assertSame($user2->getKey(), $response->json('data.0.id')); 99 | } 100 | 101 | public function testSortingByEmailAsc() 102 | { 103 | $user1 = UserFactory::new()->createOne([ 104 | 'email' => 'a@kingscode.nl', 105 | ]); 106 | $user2 = UserFactory::new()->createOne([ 107 | 'email' => 'b@kingscode.nl', 108 | ]); 109 | 110 | $response = $this->actingAs($user1, 'api')->json('get', 'user?sort_by=email&desc=0'); 111 | 112 | $response->assertOk(); 113 | 114 | $this->assertSame($user1->getKey(), $response->json('data.0.id')); 115 | } 116 | 117 | public function testSortingByEmailDesc() 118 | { 119 | $user1 = UserFactory::new()->createOne([ 120 | 'email' => 'a@kingscode.nl', 121 | ]); 122 | $user2 = UserFactory::new()->createOne([ 123 | 'email' => 'b@kingscode.nl', 124 | ]); 125 | 126 | $response = $this->actingAs($user1, 'api')->json('get', 'user?sort_by=email&desc=1'); 127 | 128 | $response->assertOk(); 129 | 130 | $this->assertSame($user2->getKey(), $response->json('data.0.id')); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/User/ShowTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 15 | 16 | $response = $this->actingAs($user, 'api')->json('get', 'user/' . $user->getKey()); 17 | 18 | $response->assertOk()->assertJson([ 19 | 'data' => [ 20 | 'id' => $user->getKey(), 21 | 'name' => $user->name, 22 | 'email' => $user->email, 23 | ], 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/User/StoreTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | 17 | $response = $this->actingAs($user, 'api')->json('post', 'user', [ 18 | 'name' => 'Kings Code', 19 | 'email' => 'info@kingscode.nl', 20 | ]); 21 | 22 | $response->assertCreated(); 23 | 24 | $this->assertDatabaseHas('users', [ 25 | 'name' => 'Kings Code', 26 | 'email' => 'info@kingscode.nl', 27 | ]); 28 | } 29 | 30 | public function testValidationErrors() 31 | { 32 | $user = UserFactory::new()->createOne(); 33 | 34 | $response = $this->actingAs($user, 'api')->json('post', 'user'); 35 | 36 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 37 | 'name', 'email', 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Feature/Http/Api/User/UpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | 17 | $response = $this->actingAs($user, 'api')->json('put', 'user/' . $user->getKey(), [ 18 | 'name' => 'Kings Code', 19 | 'email' => 'info@kingscode.nl', 20 | ]); 21 | 22 | $response->assertOk(); 23 | 24 | $this->assertDatabaseHas('users', [ 25 | 'id' => $user->getKey(), 26 | 'name' => 'Kings Code', 27 | 'email' => 'info@kingscode.nl', 28 | ]); 29 | } 30 | 31 | public function testValidationErrors() 32 | { 33 | $user = UserFactory::new()->createOne(); 34 | 35 | $response = $this->actingAs($user, 'api')->json('put', 'user/' . $user->getKey()); 36 | 37 | $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)->assertJsonValidationErrors([ 38 | 'name', 'email', 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Feature/Models/UserTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 15 | $token = $user->tokens()->create(['token' => 'yayeet']); 16 | 17 | $user->delete(); 18 | 19 | $this->assertDatabaseMissing('user_tokens', [ 20 | 'id' => $token->getKey(), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | app->make(RateLimiter::class); 23 | 24 | $rateLimiter->for('test', function (Request $request) { 25 | return new Limit($request->ip(), 1, 1); 26 | }); 27 | 28 | /** @var Router $router */ 29 | $router = $this->app->make(Router::class); 30 | 31 | $router->middleware('throttle:test')->get('test', function () { 32 | return new Response('', 200); 33 | }); 34 | } 35 | 36 | public function testWhenDisabled() 37 | { 38 | /** @var \Illuminate\Contracts\Config\Repository $config */ 39 | $config = $this->app->make(Repository::class); 40 | 41 | $config->set('app.throttling', false); 42 | 43 | $response = $this->call('get', 'test'); 44 | 45 | $response->assertOk(); 46 | 47 | $response = $this->call('get', 'test'); 48 | 49 | $response->assertOk(); 50 | } 51 | 52 | public function testWhenEnabled() 53 | { 54 | /** @var \Illuminate\Contracts\Config\Repository $config */ 55 | $config = $this->app->make(Repository::class); 56 | 57 | $config->set('app.throttling', true); 58 | 59 | $response = $this->call('get', 'test'); 60 | 61 | $response->assertOk(); 62 | 63 | $response = $this->call('get', 'test'); 64 | 65 | $response->assertStatus(Response::HTTP_TOO_MANY_REQUESTS); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Unit/Mail/Registration/AlreadyExistsTest.php: -------------------------------------------------------------------------------- 1 | render(); 17 | 18 | $this->assertArrayHasKey('front_end_url', $mailable->viewData); 19 | $this->assertArrayHasKey('password_forgotten_url', $mailable->viewData); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Mail/Registration/VerifyTest.php: -------------------------------------------------------------------------------- 1 | render(); 16 | 17 | $this->assertArrayHasKey('front_end_url', $mailable->viewData); 18 | $this->assertArrayHasKey('verify_url', $mailable->viewData); 19 | $this->assertStringContainsString('info@kingscode.nl', $mailable->viewData['verify_url']); 20 | $this->assertStringContainsString('yayeeeeeeet', $mailable->viewData['verify_url']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Unit/Mail/User/Email/CantUpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | 17 | $mailable = (new CantUpdate($user)); 18 | $mailable->render(); 19 | 20 | $this->assertArrayHasKey('user', $mailable->viewData); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Unit/Mail/User/Email/VerifyUpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | 17 | $mailable = (new VerifyUpdate($user, 'token', 'info@kingscode.nl')); 18 | $mailable->render(); 19 | 20 | $this->assertArrayHasKey('verify_url', $mailable->viewData); 21 | $this->assertArrayHasKey('user', $mailable->viewData); 22 | $this->assertStringContainsString('info@kingscode.nl', $mailable->viewData['verify_url']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Mail/User/InvitationTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | 17 | $mailable = (new Invitation($user, 'info@kingscode.nl')); 18 | $mailable->render(); 19 | 20 | $this->assertArrayHasKey('acceptation_url', $mailable->viewData); 21 | $this->assertArrayHasKey('user', $mailable->viewData); 22 | $this->assertStringContainsString('info@kingscode.nl', $mailable->viewData['acceptation_url']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Mail/User/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 16 | 17 | $mailable = (new PasswordReset($user, 'info@kingscode.nl')); 18 | $mailable->render(); 19 | 20 | $this->assertArrayHasKey('password_reset_url', $mailable->viewData); 21 | $this->assertArrayHasKey('user', $mailable->viewData); 22 | $this->assertStringContainsString('info@kingscode.nl', $mailable->viewData['password_reset_url']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/User/Email/CantUpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 20 | 21 | $this->assertInstanceOf(CantUpdateMail::class, $notification->toMail($user)); 22 | } 23 | 24 | public function testViaReturnsMailChannel() 25 | { 26 | $notification = new CantUpdate(); 27 | 28 | $this->assertTrue(in_array('mail', $notification->via())); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/User/Email/VerifyUpdateTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 20 | 21 | $this->assertInstanceOf(VerifyUpdateMail::class, $notification->toMail($user)); 22 | } 23 | 24 | public function testViaReturnsMailChannel() 25 | { 26 | $notification = new VerifyUpdate('token', 'info@kingscode.nl'); 27 | 28 | $this->assertTrue(in_array('mail', $notification->via())); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/User/InvitationTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 20 | 21 | $this->assertInstanceOf(InvitationMail::class, $notification->toMail($user)); 22 | } 23 | 24 | public function testViaReturnsMailChannel() 25 | { 26 | $notification = new Invitation('token'); 27 | 28 | $this->assertTrue(in_array('mail', $notification->via())); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/User/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | createOne(); 19 | 20 | $this->assertInstanceOf(PasswordResetMail::class, $notification->toMail($user)); 21 | } 22 | 23 | public function testViaReturnsMailChannel() 24 | { 25 | $notification = new PasswordReset('token'); 26 | 27 | $this->assertTrue(in_array('mail', $notification->via())); 28 | } 29 | } 30 | --------------------------------------------------------------------------------