diff --git a/app/Helpers.php b/app/Helpers.php
index a9353610..bc5fc4ab 100644
--- a/app/Helpers.php
+++ b/app/Helpers.php
@@ -86,3 +86,12 @@ function gravatar(string $email, int $size = 192): string
{
return sprintf("https://www.gravatar.com/avatar/%s?s=$size&d=robohash", md5($email));
}
+
+/**
+ * A quick check to determine if a mailer is configured.
+ * This is not bulletproof but should work in most cases.
+ */
+function mailer_configured(): bool
+{
+ return config('mail.default') && !in_array(config('mail.default'), ['log', 'array'], true);
+}
diff --git a/app/Http/Controllers/API/ForgotPasswordController.php b/app/Http/Controllers/API/ForgotPasswordController.php
new file mode 100644
index 00000000..9cfb85a9
--- /dev/null
+++ b/app/Http/Controllers/API/ForgotPasswordController.php
@@ -0,0 +1,20 @@
+trySendResetPasswordLink($request->email)
+ ? response()->noContent()
+ : response('', Response::HTTP_NOT_FOUND);
+ }
+}
diff --git a/app/Http/Controllers/API/ProfileController.php b/app/Http/Controllers/API/ProfileController.php
index 654f5272..1cdde1e5 100644
--- a/app/Http/Controllers/API/ProfileController.php
+++ b/app/Http/Controllers/API/ProfileController.php
@@ -9,6 +9,7 @@ use App\Models\User;
use App\Services\TokenManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Hashing\Hasher;
+use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
class ProfileController extends Controller
@@ -28,9 +29,7 @@ class ProfileController extends Controller
public function update(ProfileUpdateRequest $request)
{
- if (config('koel.misc.demo')) {
- return response()->noContent();
- }
+ static::disableInDemo(Response::HTTP_NO_CONTENT);
throw_unless(
$this->hash->check($request->current_password, $this->user->password),
diff --git a/app/Http/Controllers/API/ResetPasswordController.php b/app/Http/Controllers/API/ResetPasswordController.php
new file mode 100644
index 00000000..834e9d36
--- /dev/null
+++ b/app/Http/Controllers/API/ResetPasswordController.php
@@ -0,0 +1,20 @@
+tryResetPasswordUsingBroker($request->email, $request->password, $request->token)
+ ? response()->noContent()
+ : response('', Response::HTTP_UNPROCESSABLE_ENTITY);
+ }
+}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 345847e2..98733dd6 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
@@ -12,4 +13,9 @@ abstract class Controller extends BaseController
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
+
+ protected static function disableInDemo(int $code = Response::HTTP_FORBIDDEN): void
+ {
+ abort_if(config('koel.misc.demo'), $code);
+ }
}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index e06fd441..355bf514 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -8,6 +8,7 @@ use App\Http\Middleware\ForceHttps;
use App\Http\Middleware\ObjectStorageAuthenticate;
use App\Http\Middleware\ThrottleRequests;
use App\Http\Middleware\TrimStrings;
+use App\Http\Middleware\TrustHosts;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
@@ -26,6 +27,7 @@ class Kernel extends HttpKernel
ValidatePostSize::class,
TrimStrings::class,
ForceHttps::class,
+ TrustHosts::class,
];
/**
diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php
new file mode 100644
index 00000000..85321dba
--- /dev/null
+++ b/app/Http/Middleware/TrustHosts.php
@@ -0,0 +1,18 @@
+
+ */
+ public function hosts(): array
+ {
+ return [
+ $this->allSubdomainsOfApplicationUrl(),
+ ];
+ }
+}
diff --git a/app/Http/Requests/API/ForgotPasswordRequest.php b/app/Http/Requests/API/ForgotPasswordRequest.php
new file mode 100644
index 00000000..74877061
--- /dev/null
+++ b/app/Http/Requests/API/ForgotPasswordRequest.php
@@ -0,0 +1,17 @@
+ */
+ public function rules(): array
+ {
+ return [
+ 'email' => 'required|email',
+ ];
+ }
+}
diff --git a/app/Http/Requests/API/ResetPasswordRequest.php b/app/Http/Requests/API/ResetPasswordRequest.php
new file mode 100644
index 00000000..9afce849
--- /dev/null
+++ b/app/Http/Requests/API/ResetPasswordRequest.php
@@ -0,0 +1,23 @@
+ */
+ public function rules(): array
+ {
+ return [
+ 'token' => 'required',
+ 'email' => 'required|email',
+ 'password' => ['sometimes', Password::defaults()],
+ ];
+ }
+}
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php
index 46411a12..6f063777 100644
--- a/app/Providers/AuthServiceProvider.php
+++ b/app/Providers/AuthServiceProvider.php
@@ -4,6 +4,7 @@ namespace App\Providers;
use App\Models\User;
use App\Services\TokenManager;
+use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -25,6 +26,12 @@ class AuthServiceProvider extends ServiceProvider
});
$this->setPasswordDefaultRules();
+
+ ResetPassword::createUrlUsing(static function (User $user, string $token): string {
+ $payload = base64_encode($user->getEmailForPasswordReset() . "|$token");
+
+ return url("/#/reset-password/$payload");
+ });
}
private function setPasswordDefaultRules(): void
diff --git a/app/Services/AuthenticationService.php b/app/Services/AuthenticationService.php
index fd106009..3c5d154e 100644
--- a/app/Services/AuthenticationService.php
+++ b/app/Services/AuthenticationService.php
@@ -6,14 +6,18 @@ use App\Exceptions\InvalidCredentialsException;
use App\Models\User;
use App\Repositories\UserRepository;
use App\Values\CompositeToken;
+use Illuminate\Auth\Events\PasswordReset;
+use Illuminate\Auth\Passwords\PasswordBroker;
use Illuminate\Hashing\HashManager;
+use Illuminate\Support\Facades\Password;
class AuthenticationService
{
public function __construct(
private UserRepository $userRepository,
private TokenManager $tokenManager,
- private HashManager $hash
+ private HashManager $hash,
+ private PasswordBroker $passwordBroker
) {
}
@@ -38,4 +42,27 @@ class AuthenticationService
{
$this->tokenManager->deleteCompositionToken($token);
}
+
+ public function trySendResetPasswordLink(string $email): bool
+ {
+ return $this->passwordBroker->sendResetLink(['email' => $email]) === Password::RESET_LINK_SENT;
+ }
+
+ public function tryResetPasswordUsingBroker(string $email, string $password, string $token): bool
+ {
+ $credentials = [
+ 'email' => $email,
+ 'password' => $password,
+ 'password_confirmation' => $password,
+ 'token' => $token,
+ ];
+
+ $status = $this->passwordBroker->reset($credentials, function (User $user, string $password): void {
+ $user->password = $this->hash->make($password);
+ $user->save();
+ event(new PasswordReset($user));
+ });
+
+ return $status === Password::PASSWORD_RESET;
+ }
}
diff --git a/config/mail.php b/config/mail.php
index 0e457422..534395a3 100644
--- a/config/mail.php
+++ b/config/mail.php
@@ -1,42 +1,85 @@
env('MAIL_DRIVER', 'smtp'),
+
+ 'default' => env('MAIL_MAILER', 'smtp'),
+
/*
|--------------------------------------------------------------------------
- | SMTP Host Address
+ | Mailer Configurations
|--------------------------------------------------------------------------
|
- | Here you may provide the host address of the SMTP server used by your
- | applications. A default option is provided that is compatible with
- | the Mailgun mail service which will provide reliable deliveries.
+ | Here you may configure all of the mailers used by your application plus
+ | their respective settings. Several examples have been configured for
+ | you and you are free to add your own as your application requires.
+ |
+ | Laravel supports a variety of mail "transport" drivers to be used while
+ | sending an e-mail. You will specify which one you are using for your
+ | mailers below. You are free to add additional mailers as required.
+ |
+ | Supported: "smtp", "sendmail", "mailgun", "ses",
+ | "postmark", "log", "array", "failover"
|
*/
- 'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
- /*
- |--------------------------------------------------------------------------
- | SMTP Host Port
- |--------------------------------------------------------------------------
- |
- | This is the SMTP port used by your application to deliver e-mails to
- | users of the application. Like the host we have set this value to
- | stay compatible with the Mailgun e-mail application by default.
- |
- */
- 'port' => env('MAIL_PORT', 587),
+
+ 'mailers' => [
+ 'smtp' => [
+ 'transport' => 'smtp',
+ 'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
+ 'port' => env('MAIL_PORT', 587),
+ 'encryption' => env('MAIL_ENCRYPTION', 'tls'),
+ 'username' => env('MAIL_USERNAME'),
+ 'password' => env('MAIL_PASSWORD'),
+ 'timeout' => null,
+ 'local_domain' => env('MAIL_EHLO_DOMAIN'),
+ ],
+
+ 'ses' => [
+ 'transport' => 'ses',
+ ],
+
+ 'mailgun' => [
+ 'transport' => 'mailgun',
+ ],
+
+ 'postmark' => [
+ 'transport' => 'postmark',
+ ],
+
+ 'sendmail' => [
+ 'transport' => 'sendmail',
+ 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
+ ],
+
+ 'log' => [
+ 'transport' => 'log',
+ 'channel' => env('MAIL_LOG_CHANNEL'),
+ ],
+
+ 'array' => [
+ 'transport' => 'array',
+ ],
+
+ 'failover' => [
+ 'transport' => 'failover',
+ 'mailers' => [
+ 'smtp',
+ 'log',
+ ],
+ ],
+ ],
+
/*
|--------------------------------------------------------------------------
| Global "From" Address
@@ -47,44 +90,12 @@ return [
| used globally for all e-mails that are sent by your application.
|
*/
+
'from' => [
- 'address' => env('MAIL_FROM_ADDRESS', 'noreply@koel.local'),
- 'name' => env('MAIL_FROM_NAME', 'Koel'),
+ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
+ 'name' => env('MAIL_FROM_NAME', 'Example'),
],
- /*
- |--------------------------------------------------------------------------
- | E-Mail Encryption Protocol
- |--------------------------------------------------------------------------
- |
- | Here you may specify the encryption protocol that should be used when
- | the application send e-mail messages. A sensible default using the
- | transport layer security protocol should provide great security.
- |
- */
- 'encryption' => env('MAIL_ENCRYPTION', 'tls'),
- /*
- |--------------------------------------------------------------------------
- | SMTP Server Username
- |--------------------------------------------------------------------------
- |
- | If your SMTP server requires a username for authentication, you should
- | set it here. This will get used to authenticate with your server on
- | connection. You may also set the "password" value below this one.
- |
- */
- 'username' => env('MAIL_USERNAME'),
- 'password' => env('MAIL_PASSWORD'),
- /*
- |--------------------------------------------------------------------------
- | Sendmail System Path
- |--------------------------------------------------------------------------
- |
- | When using the "sendmail" driver to send e-mails, we will need to know
- | the path to where Sendmail lives on this server. A default path has
- | been provided here, which will work well on most of your systems.
- |
- */
- 'sendmail' => '/usr/sbin/sendmail -bs',
+
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
@@ -95,10 +106,13 @@ return [
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
+
'markdown' => [
'theme' => 'default',
+
'paths' => [
resource_path('views/vendor/mail'),
],
],
+
];
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 442ad6e1..aace5c10 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -48,7 +48,7 @@