Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: tests

on:
push:
pull_request:

jobs:
laravel-13:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: curl, mbstring, sqlite, xml, zip
coverage: none

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Run tests
run: vendor/bin/phpunit
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/node_modules
/vendor
/.idea
/.phpunit.cache
composer.lock
npm-debug.log
yarn-error.log
Expand Down
25 changes: 25 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,28 @@ php artisan vendor:publish --tag=wink-assets --force
```

In addition to this, make sure you render your posts and pages using `$post->content` and `$page->content` instead of `->body`.

## Laravel 11, 12, and 13

This release requires PHP 8.3+ and Laravel 11, 12, or 13.

`Wink\WinkAuthor` implements `getAuthPasswordName()` so the Laravel 11 authentication contract can resolve the password column. Route files import the `Route` facade directly, because a fresh Laravel 11+ application does not register global facade aliases.

### Published configuration

Laravel 11 and newer store the `local` filesystem disk in `storage/app/private`. Wink now defaults to the `public` disk so `php artisan storage:link` can serve uploads. If you already published `config/wink.php`, update these values:

```php
'storage_disk' => env('WINK_STORAGE_DISK', 'public'),

'storage_path' => env('WINK_STORAGE_PATH', 'wink/images'),
```

Files uploaded with the previous defaults on Laravel 10 (`local` disk, path `public/wink/images`) already live in `storage/app/public/wink/images`, which is the same directory as the new defaults. If a Laravel 11+ app stored uploads under `storage/app/private/public/wink/images`, move that directory to `storage/app/public/wink/images`.

After updating, run:

```sh
php artisan wink:migrate
php artisan vendor:publish --tag=wink-assets --force
```
17 changes: 15 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@
}
],
"require": {
"php": "^7.3||^7.4||^8.0",
"laravel/framework": "^7.0||^8.0||^9.0|^10.0"
"php": "^8.3",
"laravel/framework": "^11.0||^12.0||^13.0",
"league/commonmark": "^2.7"
},
"require-dev": {
"orchestra/testbench": "^11.0",
"phpunit/phpunit": "^11.5.50||^12.5.8"
},
"autoload": {
"psr-4": {
"Wink\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Wink\\Tests\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
Expand All @@ -28,6 +38,9 @@
]
}
},
"scripts": {
"test": "phpunit"
},
"config": {
"sort-packages": true
},
Expand Down
8 changes: 6 additions & 2 deletions config/wink.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@
| any of the disks defined in your config/filesystems.php file. You may
| also configure the path where the files should be stored.
|
| The public disk keeps uploads under storage/app/public so that
| `php artisan storage:link` can serve them. Laravel 11 and newer point
| the local disk at storage/app/private, which is not web-accessible.
|
*/

'storage_disk' => env('WINK_STORAGE_DISK', 'local'),
'storage_disk' => env('WINK_STORAGE_DISK', 'public'),

'storage_path' => env('WINK_STORAGE_PATH', 'public/wink/images'),
'storage_path' => env('WINK_STORAGE_PATH', 'wink/images'),

/*
|--------------------------------------------------------------------------
Expand Down
16 changes: 16 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="Wink">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_KEY" value="base64:YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="/>
</php>
</phpunit>
20 changes: 18 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,28 @@ Wink is used to manage the [official Laravel blog](https://blog.laravel.com), [d

Dark & Light modes available so everyone is happy 😁

## Requirements

- PHP 8.3, 8.4, or 8.5
- Laravel 11, 12, or 13

## Installation

Wink uses a separate database connection and authentication system so that you don't have to modify any of your project code.

To install Wink, run these commands in the root of your Laravel app:
Packagist `themsaid/wink` stops at Laravel 10. Install this fork from GitHub:

```json
"repositories": [
{
"type": "vcs",
"url": "https://github.com/drscript/wink"
}
]
```

```sh
composer require themsaid/wink
composer require themsaid/wink:dev-1.x
php artisan wink:install
php artisan storage:link
```
Expand All @@ -28,6 +42,8 @@ php artisan wink:migrate

Head to `yourproject.test/wink` and use the provided email and password to log in.

Uploaded images use the `public` disk and are stored in `storage/app/public/wink/images`. `storage:link` serves that directory. On Laravel 11 and newer the `local` disk points at `storage/app/private`, so leave `storage_disk` set to `public` unless you intend to use a private or S3 disk.

## Uploading to S3

If you want to upload images to S3, update the `storage_disk` attribute in your `wink.php` configuration file to s3. Make sure your S3 disk is correctly configured in your `filesystems.php` configuration file.
Expand Down
3 changes: 2 additions & 1 deletion src/Console/MigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public function handle()

$this->call('migrate', [
'--database' => config('wink.database_connection'),
'--path' => 'vendor/themsaid/wink/src/Migrations',
'--path' => realpath(__DIR__.'/../Migrations'),
'--realpath' => true,
'--force' => $this->option('force') ?? true,
]);

Expand Down
3 changes: 2 additions & 1 deletion src/Http/Controllers/ForgotPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Wink\Http\Controllers;

use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Throwable;
Expand Down Expand Up @@ -69,7 +70,7 @@ public function showNewPassword($token)

cache()->forget('password.reset.'.$authorId);

$author->password = \Hash::make($password = Str::random());
$author->password = Hash::make($password = Str::random());

$author->save();

Expand Down
1 change: 1 addition & 0 deletions src/Http/routes.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Illuminate\Support\Facades\Route;
use Wink\Http\Controllers\ImageUploadsController;
use Wink\Http\Controllers\LoginController;
use Wink\Http\Controllers\PagesController;
Expand Down
10 changes: 10 additions & 0 deletions src/WinkAuthor.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ public function getAuthIdentifier()
return $this->{$this->getAuthIdentifierName()};
}

/**
* Get the name of the password attribute for the user.
*
* @return string
*/
public function getAuthPasswordName()
{
return 'password';
}

/**
* Get the password for the user.
*
Expand Down
2 changes: 1 addition & 1 deletion src/WinkPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public function getContentAttribute()
'allow_unsafe_links' => false,
]);

return new HtmlString($converter->convertToHtml($this->body));
return new HtmlString((string) $converter->convert((string) $this->body));
}

/**
Expand Down
52 changes: 33 additions & 19 deletions src/WinkServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,40 @@ private function registerRoutes()
{
$middlewareGroup = config('wink.middleware_group');

Route::middleware($middlewareGroup)
->as('wink.')
->domain(config('wink.domain'))
->prefix(config('wink.path'))
->group(function () {
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('auth.login');
Route::post('/login', [LoginController::class, 'login'])->name('auth.attempt');

Route::get('/password/forgot', [ForgotPasswordController::class, 'showResetRequestForm'])->name('password.forgot');
Route::post('/password/forgot', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ForgotPasswordController::class, 'showNewPassword'])->name('password.reset');
});

Route::middleware([$middlewareGroup, Authenticate::class])
$this->routeGroup($middlewareGroup)->group(function () {
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('auth.login');
Route::post('/login', [LoginController::class, 'login'])->name('auth.attempt');

Route::get('/password/forgot', [ForgotPasswordController::class, 'showResetRequestForm'])->name('password.forgot');
Route::post('/password/forgot', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ForgotPasswordController::class, 'showNewPassword'])->name('password.reset');
});

$this->routeGroup([$middlewareGroup, Authenticate::class])->group(function () {
$this->loadRoutesFrom(__DIR__.'/Http/routes.php');
});
}

/**
* Start a Wink route group.
*
* An empty domain is omitted so routes stay host-agnostic. Laravel 13
* prioritizes routes that declare a domain during matching.
*
* @param string|array $middleware
* @return \Illuminate\Routing\RouteRegistrar
*/
private function routeGroup($middleware)
{
$routes = Route::middleware($middleware)
->as('wink.')
->domain(config('wink.domain'))
->prefix(config('wink.path'))
->group(function () {
$this->loadRoutesFrom(__DIR__.'/Http/routes.php');
});
->prefix(config('wink.path'));

if ($domain = config('wink.domain')) {
$routes->domain($domain);
}

return $routes;
}

/**
Expand Down
71 changes: 71 additions & 0 deletions tests/AuthenticationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Wink\Tests;

use Illuminate\Support\Facades\Mail;
use Wink\Mail\ResetPasswordEmail;

class AuthenticationTest extends TestCase
{
public function test_guests_are_sent_to_the_login_screen(): void
{
$this->get('/wink')->assertRedirect(route('wink.auth.login'));
$this->get('/wink/login')->assertOk()->assertSee('Log In');
$this->getJson('/wink/api/posts')->assertUnauthorized();
}

public function test_authors_can_log_in_and_out(): void
{
$this->author();

$this->post('/wink/login', [
'email' => 'admin@mail.com',
'password' => 'secret',
])->assertRedirect('/wink');

$this->get('/wink')->assertOk()->assertSee('Regina Phalange', false);

$this->get('/wink/logout')->assertRedirect(route('wink.auth.login'));
$this->get('/wink/api/posts')->assertRedirect(route('wink.auth.login'));
}

public function test_invalid_credentials_are_rejected(): void
{
$this->author();

$this->from('/wink/login')->post('/wink/login', [
'email' => 'admin@mail.com',
'password' => 'wrong',
])->assertRedirect('/wink/login')->assertSessionHasErrors('email');
}

public function test_password_reset_issues_a_new_password(): void
{
$author = $this->author();

Mail::fake();

$this->post('/wink/password/forgot', [
'email' => $author->email,
])->assertRedirect(route('wink.password.forgot'));

$token = null;

Mail::assertSent(ResetPasswordEmail::class, function (ResetPasswordEmail $mail) use (&$token) {
$token = $mail->token;

return $mail->hasTo('admin@mail.com');
});

$this->get('/wink/password/reset/'.$token)
->assertOk()
->assertSee('Copy your new password');

$author->refresh();

$this->assertFalse(\Illuminate\Support\Facades\Hash::check('secret', $author->password));

$this->get('/wink/password/reset/'.$token)
->assertRedirect(route('wink.password.forgot'));
}
}
22 changes: 22 additions & 0 deletions tests/DomainRoutingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Wink\Tests;

class DomainRoutingTest extends TestCase
{
protected function defineEnvironment($app)
{
parent::defineEnvironment($app);

$app['config']->set('wink.domain', 'blog.test');
}

public function test_routes_are_limited_to_the_configured_domain(): void
{
$login = app('router')->getRoutes()->getByName('wink.auth.login');
$posts = app('router')->getRoutes()->getByName('wink.posts.index');

$this->assertSame('blog.test', $login->getDomain());
$this->assertSame('blog.test', $posts->getDomain());
}
}
Loading
Loading