diff --git a/.env.example b/.env.example index c574f0a631..e2e0b18720 100644 --- a/.env.example +++ b/.env.example @@ -178,25 +178,6 @@ MANDRILL_SECRET= SPARKPOST_SECRET= MAILERSEND_API_KEY= -# -# Ntfy notification settings. -# defaults to "https://ntfy.sh", but needs a topic or it won't work. -# authentication is recommended but not required. -# -NTFY_SERVER= -NTFY_TOPIC= -NTFY_AUTH_ENABLED=false -NTFY_AUTH_USERNAME= -NTFY_AUTH_PASSWORD= - -# -# Pushover notification Application/API Token and User token. -# Used if you want to receive notifications over pushover. -# Both must be configured for this channel to work. -# -PUSHOVER_APP_TOKEN= -PUSHOVER_USER_TOKEN= - # Firefly III can send you the following messages. SEND_ERROR_MESSAGE=true diff --git a/app/Events/Test/TestNotificationChannel.php b/app/Events/Test/TestNotificationChannel.php index 5ad625e64a..ae919f805c 100644 --- a/app/Events/Test/TestNotificationChannel.php +++ b/app/Events/Test/TestNotificationChannel.php @@ -23,23 +23,23 @@ declare(strict_types=1); namespace FireflyIII\Events\Test; -use FireflyIII\User; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use Illuminate\Queue\SerializesModels; class TestNotificationChannel { use SerializesModels; - public User $user; - public string $channel; + public OwnerNotifiable $owner; + public string $channel; /** * Create a new event instance. */ - public function __construct(string $channel, User $user) + public function __construct(string $channel, OwnerNotifiable $owner) { - app('log')->debug(sprintf('Triggered TestNotificationChannel("%s") for user #%d (%s)', $channel, $user->id, $user->email)); - $this->user = $user; + app('log')->debug(sprintf('Triggered TestNotificationChannel("%s")', $channel)); + $this->owner = $owner; $this->channel = $channel; } } diff --git a/app/Handlers/Events/AdminEventHandler.php b/app/Handlers/Events/AdminEventHandler.php index 88c5ab4a08..c10d278af5 100644 --- a/app/Handlers/Events/AdminEventHandler.php +++ b/app/Handlers/Events/AdminEventHandler.php @@ -117,14 +117,8 @@ class AdminEventHandler */ public function sendTestNotification(TestNotificationChannel $event): void { - Log::debug(sprintf('Now in sendTestNotification(#%d, "%s")', $event->user->id, $event->channel)); - /** @var UserRepositoryInterface $repository */ - $repository = app(UserRepositoryInterface::class); + Log::debug(sprintf('Now in sendTestNotification("%s")', $event->channel)); - if (!$repository->hasRole($event->user, 'owner')) { - Log::error(sprintf('User #%d is not an owner.', $event->user->id)); - return; - } switch($event->channel) { case 'email': $class = TestNotificationEmail::class; @@ -145,7 +139,7 @@ class AdminEventHandler Log::debug(sprintf('Will send %s as a notification.', $class)); try { - Notification::send($event->user, new $class($event->user->email)); + Notification::send($event->owner, new $class($event->owner)); } catch (\Exception $e) { // @phpstan-ignore-line $message = $e->getMessage(); if (str_contains($message, 'Bcc')) { diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php index d1fc043742..b64444d5f4 100644 --- a/app/Http/Controllers/Admin/NotificationController.php +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -26,6 +26,7 @@ namespace FireflyIII\Http\Controllers\Admin; use FireflyIII\Events\Test\TestNotificationChannel; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\NotificationRequest; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -40,12 +41,21 @@ class NotificationController extends Controller $mainTitleIcon = 'fa-hand-spock-o'; $subTitle = (string) trans('firefly.title_owner_notifications'); $subTitleIcon = 'envelope-o'; - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - $channels = config('notifications.channels'); + + // notification settings: + $slackUrl = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; + $pushoverAppToken = app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data; + + $ntfyServer = app('fireflyconfig')->getEncrypted('ntfy_server', 'https://ntfy.sh')->data; + $ntfyTopic = app('fireflyconfig')->getEncrypted('ntfy_topic', '')->data; + $ntfyAuth = app('fireflyconfig')->get('ntfy_auth', false)->data; + $ntfyUser = app('fireflyconfig')->getEncrypted('ntfy_user', '')->data; + $ntfyPass = app('fireflyconfig')->getEncrypted('ntfy_pass', '')->data; + + $channels = config('notifications.channels'); $forcedAvailability = []; - - // admin notification settings: $notifications = []; foreach (config('notifications.notifications.owner') as $key => $info) { @@ -60,18 +70,24 @@ class NotificationController extends Controller } // validate presence of of Ntfy settings. - if('' === (string)config('ntfy-notification-channel.topic')) { + if ('' === $ntfyTopic) { Log::warning('No topic name for Ntfy, channel is disabled.'); $forcedAvailability['ntfy'] = false; } // validate pushover - if('' === (string)config('services.pushover.token') || '' === (string)config('services.pushover.user_token')) { + if ('' === $pushoverAppToken || '' === $pushoverUserToken) { Log::warning('No Pushover token, channel is disabled.'); $forcedAvailability['pushover'] = false; } - return view('admin.notifications.index', compact('title', 'subTitle', 'forcedAvailability', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl', 'notifications')); + return view('admin.notifications.index', + compact( + 'title', 'subTitle', 'forcedAvailability', 'mainTitleIcon', 'subTitleIcon', 'channels', + 'slackUrl', 'notifications', + 'pushoverAppToken', 'pushoverUserToken', + 'ntfyServer', 'ntfyTopic', 'ntfyAuth', 'ntfyUser', 'ntfyPass' + )); } public function postIndex(NotificationRequest $request): RedirectResponse @@ -83,12 +99,17 @@ class NotificationController extends Controller app('fireflyconfig')->set(sprintf('notification_%s', $key), $all[$key]); } } - if ('' === $all['slack_url']) { - app('fireflyconfig')->delete('slack_webhook_url'); - } - if ('' !== $all['slack_url']) { - app('fireflyconfig')->set('slack_webhook_url', $all['slack_url']); + $variables = ['slack_webhook_url', 'pushover_app_token', 'pushover_user_token', 'ntfy_server', 'ntfy_topic', 'ntfy_user', 'ntfy_pass']; + foreach ($variables as $variable) { + if ('' === $all[$variable]) { + app('fireflyconfig')->delete($variable); + } + if ('' !== $all[$variable]) { + app('fireflyconfig')->setEncrypted($variable, $all[$variable]); + } } + app('fireflyconfig')->set('ntfy_auth', $all['ntfy_auth'] ?? false); + session()->flash('success', (string) trans('firefly.notification_settings_saved')); @@ -109,10 +130,9 @@ class NotificationController extends Controller case 'slack': case 'pushover': case 'ntfy': - /** @var User $user */ - $user = auth()->user(); + $owner = new OwnerNotifiable(); app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel)); - event(new TestNotificationChannel($channel, $user)); + event(new TestNotificationChannel($channel, $owner)); session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel])); } diff --git a/app/Http/Requests/NotificationRequest.php b/app/Http/Requests/NotificationRequest.php index 09a2d93fe9..8b027ec1b2 100644 --- a/app/Http/Requests/NotificationRequest.php +++ b/app/Http/Requests/NotificationRequest.php @@ -23,8 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; -use FireflyIII\Rules\Admin\IsValidDiscordUrl; -use FireflyIII\Rules\Admin\IsValidSlackUrl; +use FireflyIII\Rules\Admin\IsValidSlackOrDiscordUrl; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; @@ -44,7 +43,16 @@ class NotificationRequest extends FormRequest } $return[$key] = $value; } - $return['slack_url'] = $this->convertString('slack_url'); + $return['slack_webhook_url'] = $this->convertString('slack_webhook_url'); + + $return['pushover_app_token'] = $this->convertString('pushover_app_token'); + $return['pushover_user_token'] = $this->convertString('pushover_user_token'); + + $return['ntfy_server'] = $this->convertString('ntfy_server'); + $return['ntfy_topic'] = $this->convertString('ntfy_topic'); + $return['ntfy_auth'] = $this->convertBoolean($this->get('ntfy_auth')); + $return['ntfy_user'] = $this->convertString('ntfy_user'); + $return['ntfy_pass'] = $this->convertString('ntfy_pass'); return $return; } @@ -54,7 +62,10 @@ class NotificationRequest extends FormRequest public function rules(): array { $rules = [ - 'slack_url' => ['nullable', 'url', 'min:1', new IsValidSlackUrl()], + 'slack_webhook_url' => ['nullable', 'url', 'min:1', new IsValidSlackOrDiscordUrl()], + 'ntfy_server' => ['nullable', 'url', 'min:1'], + 'ntfy_user' => ['required_with:ntfy_pass,ntfy_auth', 'nullable', 'string', 'min:1'], + 'ntfy_pass' => ['required_with:ntfy_user,ntfy_auth', 'nullable', 'string', 'min:1'], ]; foreach (config('notifications.notifications.owner') as $key => $info) { $rules[sprintf('notification_%s', $key)] = 'in:0,1'; diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php index 15d97d1aa2..bec827d92b 100644 --- a/app/Notifications/Admin/UserInvitation.php +++ b/app/Notifications/Admin/UserInvitation.php @@ -107,6 +107,9 @@ class UserInvitation extends Notification */ public function via($notifiable) { + + + $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; if (UrlValidator::isValidWebhookURL($slackUrl)) { return ['mail', 'slack']; diff --git a/app/Notifications/Notifiables/OwnerNotifiable.php b/app/Notifications/Notifiables/OwnerNotifiable.php new file mode 100644 index 0000000000..fb71610a37 --- /dev/null +++ b/app/Notifications/Notifiables/OwnerNotifiable.php @@ -0,0 +1,70 @@ +getEncrypted('slack_webhook_url', '')->data; + if (is_array($res)) { + $res = ''; + } + return (string) $res; + } + + public function routeNotificationForPushover() + { + $pushoverAppToken = (string) app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = (string) app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data; + return PushoverReceiver::withUserKey($pushoverUserToken) + ->withApplicationToken($pushoverAppToken); + } + + /** + * Get the notification routing information for the given driver. + * + * @param string $driver + * @param null|Notification $notification + * + * @return mixed + */ + public function routeNotificationFor($driver, $notification = null) + { + $method = 'routeNotificationFor' . Str::studly($driver); + if (method_exists($this, $method)) { + return $this->{$method}($notification); // @phpstan-ignore-line + } + + return match ($driver) { + 'mail' => (string) config('firefly.site_owner'), + default => null, + }; + } +} diff --git a/app/Notifications/ReturnsAvailableChannels.php b/app/Notifications/ReturnsAvailableChannels.php new file mode 100644 index 0000000000..2fa3c1b8eb --- /dev/null +++ b/app/Notifications/ReturnsAvailableChannels.php @@ -0,0 +1,46 @@ +get('slack_webhook_url', '')->data; + if (UrlValidator::isValidWebhookURL($slackUrl)) { + $channels[] = 'slack'; + } + // only the owner can get notifications over + } + + + + return $channels; + } + +} diff --git a/app/Notifications/ReturnsSettings.php b/app/Notifications/ReturnsSettings.php new file mode 100644 index 0000000000..ce49807fcf --- /dev/null +++ b/app/Notifications/ReturnsSettings.php @@ -0,0 +1,60 @@ + 'https://ntfy.sh', + 'ntfy_topic' => '', + 'ntfy_auth' => false, + 'ntfy_user' => '', + 'ntfy_pass' => '', + + ]; + if ('owner' === $type) { + $settings['ntfy_server'] = FireflyConfig::getEncrypted('ntfy_server', 'https://ntfy.sh')->data; + $settings['ntfy_topic'] = FireflyConfig::getEncrypted('ntfy_topic', '')->data; + $settings['ntfy_auth'] = FireflyConfig::get('ntfy_auth', false)->data; + $settings['ntfy_user'] = FireflyConfig::getEncrypted('ntfy_user', '')->data; + $settings['ntfy_pass'] = FireflyConfig::getEncrypted('ntfy_pass', '')->data; + } + return $settings; + } + +} diff --git a/app/Notifications/Test/TestNotificationEmail.php b/app/Notifications/Test/TestNotificationEmail.php index d29be7c357..a46ae9725c 100644 --- a/app/Notifications/Test/TestNotificationEmail.php +++ b/app/Notifications/Test/TestNotificationEmail.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Test; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -35,26 +36,26 @@ class TestNotificationEmail extends Notification { use Queueable; - private string $address; + private OwnerNotifiable $owner; /** * Create a new notification instance. */ - public function __construct(string $address) + public function __construct(OwnerNotifiable $owner) { - $this->address = $address; + $this->owner = $owner; } /** * Get the array representation of the notification. * - * @param mixed $notifiable + * @param OwnerNotifiable $notifiable * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * * @return array */ - public function toArray($notifiable) + public function toArray(OwnerNotifiable $notifiable) { return [ ]; @@ -69,23 +70,14 @@ class TestNotificationEmail extends Notification * * @return MailMessage */ - public function toMail($notifiable) + public function toMail(OwnerNotifiable $notifiable) { + $address = (string) config('firefly.site_owner'); return (new MailMessage()) - ->markdown('emails.admin-test', ['email' => $this->address]) + ->markdown('emails.admin-test', ['email' => $address]) ->subject((string) trans('email.admin_test_subject')); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - */ - public function toSlack($notifiable) {} - /** * Get the notification's delivery channels. * @@ -95,7 +87,7 @@ class TestNotificationEmail extends Notification * * @return array */ - public function via($notifiable) + public function via(OwnerNotifiable $notifiable) { return ['mail']; } diff --git a/app/Notifications/Test/TestNotificationNtfy.php b/app/Notifications/Test/TestNotificationNtfy.php index 6131ae692d..434bee6909 100644 --- a/app/Notifications/Test/TestNotificationNtfy.php +++ b/app/Notifications/Test/TestNotificationNtfy.php @@ -24,9 +24,10 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Test; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\User; use Illuminate\Bus\Queueable; -use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; use Ntfy\Message; use Wijourdil\NtfyNotificationChannel\Channels\NtfyChannel; @@ -40,14 +41,14 @@ class TestNotificationNtfy extends Notification { use Queueable; - private string $address; + public OwnerNotifiable $owner; /** * Create a new notification instance. */ - public function __construct(string $address) + public function __construct(OwnerNotifiable $owner) { - $this->address = $address; + $this->owner = $owner; } /** @@ -66,51 +67,34 @@ class TestNotificationNtfy extends Notification } - public function toNtfy(mixed $notifiable): Message + public function toNtfy(OwnerNotifiable $notifiable): Message { + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + + // overrule config. + config(['ntfy-notification-channel.server' => $settings['ntfy_server']]); + config(['ntfy-notification-channel.topic' => $settings['ntfy_topic']]); + + if ($settings['ntfy_auth']) { + // overrule auth as well. + config(['ntfy-notification-channel.authentication.enabled' => true]); + config(['ntfy-notification-channel.authentication.username' => $settings['ntfy_user']]); + config(['ntfy-notification-channel.authentication.password' => $settings['ntfy_pass']]); + } + $message = new Message(); - $message->topic(config('ntfy-notification-channel.topic')); - $message->title((string)trans('email.admin_test_subject')); - $message->body((string)trans('email.admin_test_message', ['channel' => 'ntfy'])); - $message->tags(['white_check_mark', 'ok_hand']); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.admin_test_subject')); + $message->body((string) trans('email.admin_test_message', ['channel' => 'ntfy'])); + $message->tags(['white_check_mark']); return $message; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return MailMessage */ - public function toMail($notifiable) - { - } - - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - */ - public function toSlack($notifiable) { - } - - /** - * Get the notification's delivery channels. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param mixed $notifiable - * - * @return array - */ - public function via($notifiable) + public function via(OwnerNotifiable $notifiable) { return [NtfyChannel::class]; } diff --git a/app/Notifications/Test/TestNotificationPushover.php b/app/Notifications/Test/TestNotificationPushover.php index 4c2ea8d0e5..a17da167c0 100644 --- a/app/Notifications/Test/TestNotificationPushover.php +++ b/app/Notifications/Test/TestNotificationPushover.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Test; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -42,73 +44,45 @@ class TestNotificationPushover extends Notification { use Queueable; - private string $address; + private OwnerNotifiable $owner; /** * Create a new notification instance. */ - public function __construct(string $address) + public function __construct(OwnerNotifiable $owner) { - $this->address = $address; + $this->owner = $owner; } /** * Get the array representation of the notification. * - * @param mixed $notifiable + * @param OwnerNotifiable $notifiable * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * * @return array */ - public function toArray($notifiable) + public function toArray(OwnerNotifiable $notifiable) { return [ ]; } - public function toPushover(mixed $notifiable): PushoverMessage + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage { Log::debug('Now in toPushover()'); + return PushoverMessage::create((string)trans('email.admin_test_message', ['channel' => 'Pushover'])) ->title((string)trans('email.admin_test_subject')); } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return MailMessage - */ - public function toMail($notifiable) - { - } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * */ - public function toSlack($notifiable) { - } - - /** - * Get the notification's delivery channels. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param mixed $notifiable - * - * @return array - */ - public function via($notifiable) + public function via(OwnerNotifiable $notifiable) { return [PushoverChannel::class]; } diff --git a/app/Notifications/Test/TestNotificationSlack.php b/app/Notifications/Test/TestNotificationSlack.php index 05f035848d..a98db18790 100644 --- a/app/Notifications/Test/TestNotificationSlack.php +++ b/app/Notifications/Test/TestNotificationSlack.php @@ -24,11 +24,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Test; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\User; use Illuminate\Bus\Queueable; -use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; //use Illuminate\Notifications\Slack\SlackMessage; +use Illuminate\Support\Facades\Log; + +use Illuminate\Notifications\Messages\SlackMessage; /** * Class TestNotification @@ -37,64 +40,49 @@ class TestNotificationSlack extends Notification { use Queueable; - private string $address; + private OwnerNotifiable $owner; /** * Create a new notification instance. */ - public function __construct(string $address) + public function __construct(OwnerNotifiable $owner) { - $this->address = $address; + $this->owner =$owner; } /** * Get the array representation of the notification. * - * @param mixed $notifiable + * @param OwnerNotifiable $notifiable * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * * @return array */ - public function toArray($notifiable) + public function toArray(OwnerNotifiable $notifiable) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return MailMessage - */ - public function toMail($notifiable) - { - } - /** * Get the Slack representation of the notification. * - * @param mixed $notifiable + * @param OwnerNotifiable $notifiable * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * */ - public function toSlack($notifiable) { - + public function toSlack(OwnerNotifiable $notifiable) + { // since it's an admin notification, grab the URL from fireflyconfig - $url = app('fireflyconfig')->get('slack_webhook_url', '')->data; - -// return (new SlackMessage) -// ->text((string)trans('email.admin_test_subject')) -// ->to($url); - return (new SlackMessage()) - ->content((string)trans('email.admin_test_subject')) - ->to($url); + $url = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; + if ('' !== $url) { + return new SlackMessage()->content((string)trans('email.admin_test_subject'))->to($url); + //return new SlackMessage()->text((string) trans('email.admin_test_subject'))->to($url); + } + Log::error('Empty slack URL, cannot send notification.'); } /** @@ -102,11 +90,11 @@ class TestNotificationSlack extends Notification * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @param mixed $notifiable + * @param OwnerNotifiable $notifiable * * @return array */ - public function via($notifiable) + public function via(OwnerNotifiable $notifiable) { return ['slack']; } diff --git a/app/Rules/Admin/IsValidSlackOrDiscordUrl.php b/app/Rules/Admin/IsValidSlackOrDiscordUrl.php new file mode 100644 index 0000000000..c03e513a59 --- /dev/null +++ b/app/Rules/Admin/IsValidSlackOrDiscordUrl.php @@ -0,0 +1,32 @@ +translate(); + $message = sprintf('IsValidSlackUrl: "%s" is not a discord or slack URL.', substr($value, 0, 255)); + Log::debug($message); + Log::channel('audit')->info($message); + } + } +} diff --git a/app/Rules/Admin/IsValidSlackUrl.php b/app/Rules/Admin/IsValidSlackUrl.php index 57cd1675a1..8eddfa18c2 100644 --- a/app/Rules/Admin/IsValidSlackUrl.php +++ b/app/Rules/Admin/IsValidSlackUrl.php @@ -24,7 +24,7 @@ class IsValidSlackUrl implements ValidationRule if(!str_starts_with($value, 'https://hooks.slack.com/services/')) { $fail('validation.active_url')->translate(); - $message = sprintf('IsValidSlackUrl: "%s" is not a discord URL.', substr($value, 0, 255)); + $message = sprintf('IsValidSlackUrl: "%s" is not a slack URL.', substr($value, 0, 255)); Log::debug($message); Log::channel('audit')->info($message); } diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 23a583f4ee..22719d612d 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -289,6 +289,26 @@ class ExpandedForm return $html; } + /** + * @throws FireflyException + */ + public function passwordWithValue(string $name, string $value, ?array $options = null): string + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + + try { + $html = view('form.password', compact('classes', 'value','name', 'label', 'options'))->render(); + } catch (\Throwable $e) { + app('log')->debug(sprintf('Could not render passwordWithValue(): %s', $e->getMessage())); + $html = 'Could not render passwordWithValue.'; + + throw new FireflyException($html, 0, $e); + } + + return $html; + } /** * Function to render a percentage. diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php index c4078dbfd0..abcc00d650 100644 --- a/app/Support/FireflyConfig.php +++ b/app/Support/FireflyConfig.php @@ -25,7 +25,10 @@ namespace FireflyIII\Support; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Configuration; +use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Contracts\Encryption\EncryptException; use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\Log; /** * Class FireflyConfig. @@ -34,7 +37,7 @@ class FireflyConfig { public function delete(string $name): void { - $fullName = 'ff-config-'.$name; + $fullName = 'ff-config-' . $name; if (\Cache::has($fullName)) { \Cache::forget($fullName); } @@ -46,6 +49,25 @@ class FireflyConfig return 1 === Configuration::where('name', $name)->count(); } + public function getEncrypted(string $name, $default = null): ?Configuration + { + $result = $this->get($name, $default); + if (null === $result) { + return null; + } + if ('' === $result->data) { + Log::warning(sprintf('Empty encrypted preference found: "%s"', $name)); + return $result; + } + try { + $result->data = decrypt($result->data); + } catch (DecryptException $e) { + Log::error(sprintf('Could not decrypt preference "%s": %s', $name, $e->getMessage())); + return $result; + } + return $result; + } + /** * @param null|bool|int|string $default * @@ -53,7 +75,7 @@ class FireflyConfig */ public function get(string $name, $default = null): ?Configuration { - $fullName = 'ff-config-'.$name; + $fullName = 'ff-config-' . $name; if (\Cache::has($fullName)) { return \Cache::get($fullName); } @@ -61,7 +83,7 @@ class FireflyConfig try { /** @var null|Configuration $config */ $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); - } catch (\Exception|QueryException $e) { + } catch (\Exception | QueryException $e) { throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e); } @@ -78,10 +100,18 @@ class FireflyConfig return $this->set($name, $default); } - /** - * @param mixed $value - */ - public function set(string $name, $value): Configuration + public function setEncrypted(string $name, mixed $value): Configuration + { + try { + $encrypted = encrypt($value); + } catch (EncryptException $e) { + Log::error(sprintf('Could not encrypt preference "%s": %s', $name, $e->getMessage())); + throw new FireflyException(sprintf('Could not encrypt preference "%s". Cowardly refuse to continue.', $name)); + } + return $this->set($name, $encrypted); + } + + public function set(string $name, mixed $value): Configuration { try { $config = Configuration::whereName($name)->whereNull('deleted_at')->first(); @@ -99,13 +129,13 @@ class FireflyConfig $item->name = $name; $item->data = $value; $item->save(); - \Cache::forget('ff-config-'.$name); + \Cache::forget('ff-config-' . $name); return $item; } $config->data = $value; $config->save(); - \Cache::forget('ff-config-'.$name); + \Cache::forget('ff-config-' . $name); return $config; } diff --git a/app/User.php b/app/User.php index c11762991d..a5165facac 100644 --- a/app/User.php +++ b/app/User.php @@ -36,7 +36,6 @@ use FireflyIII\Models\Category; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\ObjectGroup; -use FireflyIII\Models\PiggyBank; use FireflyIII\Models\Preference; use FireflyIII\Models\Recurrence; use FireflyIII\Models\Role; @@ -54,7 +53,6 @@ use FireflyIII\Notifications\Admin\UserInvitation; use FireflyIII\Notifications\Admin\UserRegistration; use FireflyIII\Notifications\Admin\VersionCheckResult; use FireflyIII\Notifications\Test\TestNotificationDiscord; -use FireflyIII\Notifications\Test\TestNotificationSlack; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -65,7 +63,6 @@ use Illuminate\Notifications\Notification; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Laravel\Passport\HasApiTokens; -use Laravel\Passport\Token; use NotificationChannels\Pushover\PushoverReceiver; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -93,7 +90,7 @@ class User extends Authenticatable public static function routeBinder(string $value): self { if (auth()->check()) { - $userId = (int)$value; + $userId = (int) $value; $user = self::find($userId); if (null !== $user) { return $user; @@ -102,12 +99,6 @@ class User extends Authenticatable throw new NotFoundHttpException(); } - public function routeNotificationForPushover() - { - return PushoverReceiver::withUserKey((string) config('services.pushover.user_token')) - ->withApplicationToken((string) config('services.pushover.token')); - //return (string) config('services.pushover.token'); - } /** * Link to accounts. @@ -192,7 +183,7 @@ class User extends Authenticatable */ public function getAdministrationId(): int { - $groupId = (int)$this->user_group_id; + $groupId = (int) $this->user_group_id; if (0 === $groupId) { throw new FireflyException('User has no administration ID.'); } @@ -269,38 +260,38 @@ class User extends Authenticatable app('log')->debug(sprintf('in hasAnyRoleInGroup(%s)', implode(', ', $roles))); /** @var Collection $dbRoles */ - $dbRoles = UserRole::whereIn('title', $roles)->get(); + $dbRoles = UserRole::whereIn('title', $roles)->get(); if (0 === $dbRoles->count()) { app('log')->error(sprintf('Could not find role(s): %s. Probably migration mishap.', implode(', ', $roles))); return false; } - $dbRolesIds = $dbRoles->pluck('id')->toArray(); - $dbRolesTitles = $dbRoles->pluck('title')->toArray(); + $dbRolesIds = $dbRoles->pluck('id')->toArray(); + $dbRolesTitles = $dbRoles->pluck('title')->toArray(); /** @var Collection $groupMemberships */ $groupMemberships = $this->groupMemberships()->whereIn('user_role_id', $dbRolesIds)->where('user_group_id', $userGroup->id)->get(); if (0 === $groupMemberships->count()) { app('log')->error(sprintf( - 'User #%d "%s" does not have roles %s in user group #%d "%s"', - $this->id, - $this->email, - implode(', ', $roles), - $userGroup->id, - $userGroup->title - )); + 'User #%d "%s" does not have roles %s in user group #%d "%s"', + $this->id, + $this->email, + implode(', ', $roles), + $userGroup->id, + $userGroup->title + )); return false; } foreach ($groupMemberships as $membership) { app('log')->debug(sprintf( - 'User #%d "%s" has role "%s" in user group #%d "%s"', - $this->id, - $this->email, - $membership->userRole->title, - $userGroup->id, - $userGroup->title - )); + 'User #%d "%s" has role "%s" in user group #%d "%s"', + $this->id, + $this->email, + $membership->userRole->title, + $userGroup->id, + $userGroup->title + )); if (in_array($membership->userRole->title, $dbRolesTitles, true)) { app('log')->debug(sprintf('Return true, found role "%s"', $membership->userRole->title)); @@ -308,13 +299,13 @@ class User extends Authenticatable } } app('log')->error(sprintf( - 'User #%d "%s" does not have roles %s in user group #%d "%s"', - $this->id, - $this->email, - implode(', ', $roles), - $userGroup->id, - $userGroup->title - )); + 'User #%d "%s" does not have roles %s in user group #%d "%s"', + $this->id, + $this->email, + implode(', ', $roles), + $userGroup->id, + $userGroup->title + )); return false; } @@ -366,13 +357,13 @@ class User extends Authenticatable */ public function routeNotificationFor($driver, $notification = null) { - $method = 'routeNotificationFor'.Str::studly($driver); + $method = 'routeNotificationFor' . Str::studly($driver); if (method_exists($this, $method)) { return $this->{$method}($notification); // @phpstan-ignore-line } - $email = $this->email; + $email = $this->email; // see if user has alternative email address: - $pref = app('preferences')->getForUser($this, 'remote_guard_alt_email'); + $pref = app('preferences')->getForUser($this, 'remote_guard_alt_email'); if (null !== $pref) { $email = $pref->data; } @@ -382,7 +373,6 @@ class User extends Authenticatable } return match ($driver) { - 'database' => $this->notifications(), 'mail' => $email, default => null, }; @@ -404,30 +394,41 @@ class User extends Authenticatable return $this->belongsToMany(Role::class); } + public function routeNotificationForPushover(Notification $notification) + { + // this check does not validate if the user is owner, Should be done by notification itself. + $appToken = (string) app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data; + $userToken = (string) app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data; + + if (property_exists($notification, 'type') && $notification->type === 'owner') { + return PushoverReceiver::withUserKey($userToken) + ->withApplicationToken($appToken); + } + + throw new FireflyException('No pushover token found.'); +// return PushoverReceiver::withUserKey((string) config('services.pushover.user_token')) +// ->withApplicationToken((string) config('services.pushover.token')); + //return (string) config('services.pushover.token'); + } + + /** * Route notifications for the Slack channel. */ public function routeNotificationForSlack(Notification $notification): ?string { // this check does not validate if the user is owner, Should be done by notification itself. - $res = app('fireflyconfig')->get('slack_webhook_url', '')->data; + $res = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; if (is_array($res)) { $res = ''; } - $res = (string)$res; + $res = (string) $res; - // not the best way to do this, but alas. - - if ($notification instanceof TestNotificationSlack) { + if (property_exists($notification, 'type') && $notification->type === 'owner') { return $res; } - if ($notification instanceof TestNotificationDiscord) { - $res = app('fireflyconfig')->get('discord_webhook_url', '')->data; - if (is_array($res)) { - $res = ''; - } - return (string)$res; - } + + // not the best way to do this, but alas. if ($notification instanceof UserInvitation) { return $res; } @@ -437,12 +438,12 @@ class User extends Authenticatable if ($notification instanceof VersionCheckResult) { return $res; } - $pref = app('preferences')->getForUser($this, 'slack_webhook_url', '')->data; + $pref = app('preferences')->getEncryptedForUser($this, 'slack_webhook_url', '')->data; if (is_array($pref)) { return ''; } - return (string)$pref; + return (string) $pref; } /** diff --git a/config/notifications.php b/config/notifications.php index de0fb4864a..65b53836dd 100644 --- a/config/notifications.php +++ b/config/notifications.php @@ -24,8 +24,8 @@ return [ 'channels' => [ 'email' => ['enabled' => true, 'ui_configurable' => 0,], 'slack' => ['enabled' => true, 'ui_configurable' => 1,], - 'ntfy' => ['enabled' => true, 'ui_configurable' => 0,], - 'pushover' => ['enabled' => true, 'ui_configurable' => 0,], + 'ntfy' => ['enabled' => true, 'ui_configurable' => 1,], + 'pushover' => ['enabled' => true, 'ui_configurable' => 1,], 'gotify' => ['enabled' => false, 'ui_configurable' => 0,], 'pushbullet' => ['enabled' => false, 'ui_configurable' => 0,], ], diff --git a/config/ntfy-notification-channel.php b/config/ntfy-notification-channel.php index e1ca641b7f..b65d4c35b0 100644 --- a/config/ntfy-notification-channel.php +++ b/config/ntfy-notification-channel.php @@ -3,12 +3,12 @@ // config for Wijourdil/NtfyNotificationChannel return [ - 'server' => env('NTFY_SERVER', 'https://ntfy.sh'), - 'topic' => env('NTFY_TOPIC', ''), + 'server' => 'https://ntfy.sh', + 'topic' => '', 'authentication' => [ - 'enabled' => (bool) env('NTFY_AUTH_ENABLED', false), - 'username' => env('NTFY_AUTH_USERNAME', ''), - 'password' => env('NTFY_AUTH_PASSWORD', ''), + 'enabled' => false, + 'username' => '', + 'password' => '', ], ]; diff --git a/config/twigbridge.php b/config/twigbridge.php index 60d1e1023d..a3a6558817 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -183,6 +183,7 @@ return [ 'file', 'staticText', 'password', + 'passwordWithValue', 'nonSelectableAmount', 'number', 'amountNoCurrency', diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 6b9e486dc7..ef4cf2575e 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2497,8 +2497,9 @@ return [ 'notification_settings' => 'Settings for notifications', 'notification_settings_saved' => 'The notification settings have been saved', 'available_channels_title' => 'Available channels', - 'available_channels_expl' => 'These channels are available to send notifications over. To test your confiuration, use the buttons below. Please note that the buttons have no spam control.', + 'available_channels_expl' => 'These channels are available to send notifications over. To test your configuration, use the buttons below. Please note that the buttons have no spam control.', 'notification_channel_name_email' => 'Email', + 'slack_discord_double' => 'The Slack notification channel can also send notifications to Discord.', 'notification_channel_name_slack' => 'Slack', 'notification_channel_name_ntfy' => 'Ntfy.sh', 'notification_channel_name_pushover' => 'Pushover', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index a83bbd764e..41778b2239 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -264,5 +264,12 @@ return [ 'webhook_delivery' => 'Delivery', 'webhook_response' => 'Response', 'webhook_trigger' => 'Trigger', + 'pushover_app_token' => 'Pushover app token', + 'pushover_user_token' => 'Pushover user token', + 'ntfy_server' => 'Ntfy server', + 'ntfy_topic' => 'Ntfy topic', + 'ntfy_auth' => 'Ntfy authentication enabled', + 'ntfy_user' => 'Ntfy username', + 'ntfy_pass' => 'Ntfy password', ]; // Ignore this comment diff --git a/resources/views/admin/notifications/index.twig b/resources/views/admin/notifications/index.twig index 76e372cf1e..d569f22c49 100644 --- a/resources/views/admin/notifications/index.twig +++ b/resources/views/admin/notifications/index.twig @@ -24,7 +24,16 @@ {% endfor %}

{{ 'channel_settings'|_ }}

- {{ ExpandedForm.text('slack_url', slackUrl, {'label' : 'slack_url_label'|_}) }} + {{ ExpandedForm.text('slack_webhook_url', slackUrl, {'label' : 'slack_url_label'|_, helpText: trans('firefly.slack_discord_double')}) }} + + {{ ExpandedForm.text('pushover_app_token', pushoverAppToken, {}) }} + {{ ExpandedForm.text('pushover_user_token', pushoverUserToken, {}) }} + + {{ ExpandedForm.text('ntfy_server', ntfyServer, {}) }} + {{ ExpandedForm.text('ntfy_topic', ntfyTopic, {}) }} + {{ ExpandedForm.checkbox('ntfy_auth','1', ntfyAuth, {}) }} + {{ ExpandedForm.text('ntfy_user', ntfyUser, {}) }} + {{ ExpandedForm.passwordWithValue('ntfy_pass', ntfyPass, {}) }}