diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Controllers/AuthController.php | 2 | ||||
-rw-r--r-- | src/Controllers/PasswordResetController.php | 167 | ||||
-rw-r--r-- | src/Helpers/Translation/TranslationServiceProvider.php | 6 | ||||
-rw-r--r-- | src/Http/Exceptions/HttpNotFound.php | 23 | ||||
-rw-r--r-- | src/Http/Response.php | 27 | ||||
-rw-r--r-- | src/Http/Validation/Rules/Between.php | 10 | ||||
-rw-r--r-- | src/Http/Validation/Rules/Max.php | 10 | ||||
-rw-r--r-- | src/Http/Validation/Rules/Min.php | 10 | ||||
-rw-r--r-- | src/Http/Validation/Rules/StringInputLength.php | 44 | ||||
-rw-r--r-- | src/Mail/EngelsystemMailer.php | 59 | ||||
-rw-r--r-- | src/Middleware/LegacyMiddleware.php | 6 | ||||
-rw-r--r-- | src/Renderer/Twig/Extensions/Legacy.php | 1 |
12 files changed, 342 insertions, 23 deletions
diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index c69c2377..7892064b 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -88,7 +88,7 @@ class AuthController extends BaseController $user = $this->auth->authenticate($data['login'], $data['password']); if (!$user instanceof User) { - $this->session->set('errors', $this->session->get('errors', []) + ['auth.not-found']); + $this->session->set('errors', array_merge($this->session->get('errors', []), ['auth.not-found'])); return $this->showLogin(); } diff --git a/src/Controllers/PasswordResetController.php b/src/Controllers/PasswordResetController.php new file mode 100644 index 00000000..505ed8eb --- /dev/null +++ b/src/Controllers/PasswordResetController.php @@ -0,0 +1,167 @@ +<?php + +namespace Engelsystem\Controllers; + +use Engelsystem\Http\Exceptions\HttpNotFound; +use Engelsystem\Http\Request; +use Engelsystem\Http\Response; +use Engelsystem\Mail\EngelsystemMailer; +use Engelsystem\Models\User\PasswordReset; +use Engelsystem\Models\User\User; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +class PasswordResetController extends BaseController +{ + /** @var LoggerInterface */ + protected $log; + + /** @var EngelsystemMailer */ + protected $mail; + + /** @var Response */ + protected $response; + + /** @var SessionInterface */ + protected $session; + + /** @var array */ + protected $permissions = [ + 'reset' => 'login', + 'postReset' => 'login', + 'resetPassword' => 'login', + 'postResetPassword' => 'login', + ]; + + /** + * @param Response $response + * @param SessionInterface $session + * @param EngelsystemMailer $mail + * @param LoggerInterface $log + */ + public function __construct( + Response $response, + SessionInterface $session, + EngelsystemMailer $mail, + LoggerInterface $log + ) { + $this->log = $log; + $this->mail = $mail; + $this->response = $response; + $this->session = $session; + } + + /** + * @return Response + */ + public function reset(): Response + { + return $this->showView('pages/password/reset'); + } + + /** + * @param Request $request + * @return Response + */ + public function postReset(Request $request): Response + { + $data = $this->validate($request, [ + 'email' => 'required|email', + ]); + + /** @var User $user */ + $user = User::whereEmail($data['email'])->first(); + if ($user) { + $reset = PasswordReset::findOrNew($user->id); + $reset->user_id = $user->id; + $reset->token = md5(random_bytes(64)); + $reset->save(); + + $this->log->info( + sprintf('Password recovery for %s (%u)', $user->name, $user->id), + ['user' => $user->toJson()] + ); + + $this->mail->sendViewTranslated( + $user, + 'Password recovery', + 'emails/password-reset', + ['username' => $user->name, 'reset' => $reset] + ); + } + + return $this->showView('pages/password/reset-success', ['type' => 'email']); + } + + /** + * @param Request $request + * @return Response + */ + public function resetPassword(Request $request): Response + { + $this->requireToken($request); + + return $this->showView('pages/password/reset-form'); + } + + /** + * @param Request $request + * @return Response + */ + public function postResetPassword(Request $request): Response + { + $reset = $this->requireToken($request); + + $data = $this->validate($request, [ + 'password' => 'required|min:' . config('min_password_length'), + 'password_confirmation' => 'required', + ]); + + if ($data['password'] !== $data['password_confirmation']) { + $this->session->set('errors', + array_merge($this->session->get('errors', []), ['validation.password.confirmed'])); + + return $this->showView('pages/password/reset-form'); + } + + auth()->setPassword($reset->user, $data['password']); + $reset->delete(); + + return $this->showView('pages/password/reset-success', ['type' => 'reset']); + } + + /** + * @param string $view + * @param array $data + * @return Response + */ + protected function showView($view = 'pages/password/reset', $data = []): Response + { + $errors = Collection::make(Arr::flatten($this->session->get('errors', []))); + $this->session->remove('errors'); + + return $this->response->withView( + $view, + array_merge_recursive(['errors' => $errors], $data) + ); + } + + /** + * @param Request $request + * @return PasswordReset + */ + protected function requireToken(Request $request): PasswordReset + { + $token = $request->getAttribute('token'); + /** @var PasswordReset|null $reset */ + $reset = PasswordReset::whereToken($token)->first(); + + if (!$reset) { + throw new HttpNotFound(); + } + + return $reset; + } +} diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php index 62247000..6df9b0fe 100644 --- a/src/Helpers/Translation/TranslationServiceProvider.php +++ b/src/Helpers/Translation/TranslationServiceProvider.php @@ -41,8 +41,10 @@ class TranslationServiceProvider extends ServiceProvider 'localeChangeCallback' => [$this, 'setLocale'], ] ); - $this->app->instance(Translator::class, $translator); - $this->app->instance('translator', $translator); + $this->app->singleton(Translator::class, function () use ($translator) { + return $translator; + }); + $this->app->alias(Translator::class, 'translator'); } /** diff --git a/src/Http/Exceptions/HttpNotFound.php b/src/Http/Exceptions/HttpNotFound.php new file mode 100644 index 00000000..324adaf9 --- /dev/null +++ b/src/Http/Exceptions/HttpNotFound.php @@ -0,0 +1,23 @@ +<?php + +namespace Engelsystem\Http\Exceptions; + +use Throwable; + +class HttpNotFound extends HttpException +{ + /** + * @param string $message + * @param array $headers + * @param int $code + * @param Throwable|null $previous + */ + public function __construct( + string $message = '', + array $headers = [], + int $code = 0, + Throwable $previous = null + ) { + parent::__construct(404, $message, $headers, $code, $previous); + } +} diff --git a/src/Http/Response.php b/src/Http/Response.php index 1a7c8209..a6b4ab74 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -3,6 +3,7 @@ namespace Engelsystem\Http; use Engelsystem\Renderer\Renderer; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -11,21 +12,21 @@ class Response extends SymfonyResponse implements ResponseInterface use MessageTrait; /** @var Renderer */ - protected $view; + protected $renderer; /** * @param string $content * @param int $status * @param array $headers - * @param Renderer $view + * @param Renderer $renderer */ public function __construct( $content = '', int $status = 200, array $headers = [], - Renderer $view = null + Renderer $renderer = null ) { - $this->view = $view; + $this->renderer = $renderer; parent::__construct($content, $status, $headers); } @@ -47,7 +48,7 @@ class Response extends SymfonyResponse implements ResponseInterface * provided status code; if none is provided, implementations MAY * use the defaults as suggested in the HTTP specification. * @return static - * @throws \InvalidArgumentException For invalid status code arguments. + * @throws InvalidArgumentException For invalid status code arguments. */ public function withStatus($code, $reasonPhrase = '') { @@ -107,12 +108,12 @@ class Response extends SymfonyResponse implements ResponseInterface */ public function withView($view, $data = [], $status = 200, $headers = []) { - if (!$this->view instanceof Renderer) { - throw new \InvalidArgumentException('Renderer not defined'); + if (!$this->renderer instanceof Renderer) { + throw new InvalidArgumentException('Renderer not defined'); } $new = clone $this; - $new->setContent($this->view->render($view, $data)); + $new->setContent($this->renderer->render($view, $data)); $new->setStatusCode($status, ($status == $this->getStatusCode() ? $this->statusText : null)); foreach ($headers as $key => $values) { @@ -144,4 +145,14 @@ class Response extends SymfonyResponse implements ResponseInterface return $response; } + + /** + * Set the renderer to use + * + * @param Renderer $renderer + */ + public function setRenderer(Renderer $renderer) + { + $this->renderer = $renderer; + } } diff --git a/src/Http/Validation/Rules/Between.php b/src/Http/Validation/Rules/Between.php new file mode 100644 index 00000000..106a93ac --- /dev/null +++ b/src/Http/Validation/Rules/Between.php @@ -0,0 +1,10 @@ +<?php + +namespace Engelsystem\Http\Validation\Rules; + +use Respect\Validation\Rules\Between as RespectBetween; + +class Between extends RespectBetween +{ + use StringInputLength; +} diff --git a/src/Http/Validation/Rules/Max.php b/src/Http/Validation/Rules/Max.php new file mode 100644 index 00000000..b1b2cfa3 --- /dev/null +++ b/src/Http/Validation/Rules/Max.php @@ -0,0 +1,10 @@ +<?php + +namespace Engelsystem\Http\Validation\Rules; + +use Respect\Validation\Rules\Max as RespectMax; + +class Max extends RespectMax +{ + use StringInputLength; +} diff --git a/src/Http/Validation/Rules/Min.php b/src/Http/Validation/Rules/Min.php new file mode 100644 index 00000000..ab8d4e1a --- /dev/null +++ b/src/Http/Validation/Rules/Min.php @@ -0,0 +1,10 @@ +<?php + +namespace Engelsystem\Http\Validation\Rules; + +use Respect\Validation\Rules\Min as RespectMin; + +class Min extends RespectMin +{ + use StringInputLength; +} diff --git a/src/Http/Validation/Rules/StringInputLength.php b/src/Http/Validation/Rules/StringInputLength.php new file mode 100644 index 00000000..7b5c248b --- /dev/null +++ b/src/Http/Validation/Rules/StringInputLength.php @@ -0,0 +1,44 @@ +<?php + +namespace Engelsystem\Http\Validation\Rules; + +use DateTime; +use Illuminate\Support\Str; +use Throwable; + +trait StringInputLength +{ + /** + * Use the input length of a string + * + * @param mixed $input + * @return bool + */ + public function validate($input): bool + { + if ( + is_string($input) + && !is_numeric($input) + && !$this->isDateTime($input) + ) { + $input = Str::length($input); + } + + return parent::validate($input); + } + + /** + * @param mixed $input + * @return bool + */ + protected function isDateTime($input): bool + { + try { + new DateTime($input); + } catch (Throwable $e) { + return false; + } + + return true; + } +} diff --git a/src/Mail/EngelsystemMailer.php b/src/Mail/EngelsystemMailer.php index 81660681..87915d67 100644 --- a/src/Mail/EngelsystemMailer.php +++ b/src/Mail/EngelsystemMailer.php @@ -2,6 +2,8 @@ namespace Engelsystem\Mail; +use Engelsystem\Helpers\Translation\Translator; +use Engelsystem\Models\User\User; use Engelsystem\Renderer\Renderer; use Swift_Mailer as SwiftMailer; @@ -10,30 +12,75 @@ class EngelsystemMailer extends Mailer /** @var Renderer|null */ protected $view; + /** @var Translator|null */ + protected $translation; + /** @var string */ protected $subjectPrefix = null; /** * @param SwiftMailer $mailer * @param Renderer $view + * @param Translator $translation */ - public function __construct(SwiftMailer $mailer, Renderer $view = null) + public function __construct(SwiftMailer $mailer, Renderer $view = null, Translator $translation = null) { parent::__construct($mailer); + $this->translation = $translation; $this->view = $view; } /** + * @param string|string[]|User $to + * @param string $subject + * @param string $template + * @param array $data + * @param string|null $locale + * @return int + */ + public function sendViewTranslated( + $to, + string $subject, + string $template, + array $data = [], + ?string $locale = null + ): int { + if ($to instanceof User) { + $locale = $locale ?: $to->settings->language; + $to = $to->contact->email ? $to->contact->email : $to->email; + } + + $activeLocale = null; + if ( + $locale + && $this->translation + && isset($this->translation->getLocales()[$locale]) + ) { + $activeLocale = $this->translation->getLocale(); + $this->translation->setLocale($locale); + } + + $subject = $this->translation ? $this->translation->translate($subject) : $subject; + $sentMails = $this->sendView($to, $subject, $template, $data); + + if ($activeLocale) { + $this->translation->setLocale($activeLocale); + } + + return $sentMails; + } + + /** * Send a template * - * @param string $to - * @param string $subject - * @param string $template - * @param array $data + * @param string|string[] $to + * @param string $subject + * @param string $template + * @param array $data * @return int */ - public function sendView($to, $subject, $template, $data = []): int + public function sendView($to, string $subject, string $template, array $data = []): int { $body = $this->view->render($template, $data); diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 27a15faa..11508e1c 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -26,7 +26,6 @@ class LegacyMiddleware implements MiddlewareInterface 'shifts_json_export', 'users', 'user_driver_licenses', - 'user_password_recovery', 'user_worklog', ]; @@ -112,11 +111,6 @@ class LegacyMiddleware implements MiddlewareInterface case 'shifts_json_export': require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); shifts_json_export_controller(); - case 'user_password_recovery': - require_once realpath(__DIR__ . '/../../includes/controller/users_controller.php'); - $title = user_password_recovery_title(); - $content = user_password_recovery_controller(); - return [$title, $content]; case 'public_dashboard': return public_dashboard_controller(); case 'angeltypes': diff --git a/src/Renderer/Twig/Extensions/Legacy.php b/src/Renderer/Twig/Extensions/Legacy.php index 79de32cb..55c095fc 100644 --- a/src/Renderer/Twig/Extensions/Legacy.php +++ b/src/Renderer/Twig/Extensions/Legacy.php @@ -32,6 +32,7 @@ class Legacy extends TwigExtension new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml), new TwigFunction('menuUserSubmenu', 'make_user_submenu', $isSafeHtml), new TwigFunction('page', [$this, 'getPage']), + new TwigFunction('msg', 'msg', $isSafeHtml), ]; } |