diff --git a/app/Events/Admin/InvitationCreated.php b/app/Events/Admin/InvitationCreated.php
new file mode 100644
index 0000000000..a91fc89e4c
--- /dev/null
+++ b/app/Events/Admin/InvitationCreated.php
@@ -0,0 +1,49 @@
+.
+ */
+
+namespace FireflyIII\Events\Admin;
+
+use FireflyIII\Events\Event;
+use FireflyIII\Models\InvitedUser;
+use FireflyIII\Models\TransactionGroup;
+use Illuminate\Queue\SerializesModels;
+
+/**
+ * Class InvitationCreated
+ */
+class InvitationCreated extends Event
+{
+ use SerializesModels;
+
+ public InvitedUser $invitee;
+
+ public TransactionGroup $transactionGroup;
+
+ /**
+ * Create a new event instance.
+ *
+ * @param InvitedUser $invitee
+ */
+ public function __construct(InvitedUser $invitee)
+ {
+ $this->invitee = $invitee;
+ }
+}
diff --git a/app/Handlers/Events/AdminEventHandler.php b/app/Handlers/Events/AdminEventHandler.php
index 4abc4605c9..9a2c40b0ab 100644
--- a/app/Handlers/Events/AdminEventHandler.php
+++ b/app/Handlers/Events/AdminEventHandler.php
@@ -22,9 +22,11 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
+use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\AdminRequestedTestMessage;
use FireflyIII\Events\NewVersionAvailable;
use FireflyIII\Notifications\Admin\TestNotification;
+use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\FireflyConfig;
@@ -57,6 +59,27 @@ class AdminEventHandler
Notification::send($event->user, new TestNotification($event->user->email));
}
+ /**
+ * @param InvitationCreated $event
+ * @return void
+ */
+ public function sendInvitationNotification(InvitationCreated $event): void
+ {
+ $sendMail = FireflyConfig::get('notification_invite_created', true)->data;
+ if (false === $sendMail) {
+ return;
+ }
+
+ /** @var UserRepositoryInterface $repository */
+ $repository = app(UserRepositoryInterface::class);
+ $all = $repository->all();
+ foreach ($all as $user) {
+ if ($repository->hasRole($user, 'owner')) {
+ Notification::send($user, new UserInvitation($event->invitee));
+ }
+ }
+ }
+
/**
* Send new version message to admin.
*
diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php
index cde875d4d7..8fcb945cac 100644
--- a/app/Http/Controllers/Admin/UserController.php
+++ b/app/Http/Controllers/Admin/UserController.php
@@ -22,14 +22,17 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Admin;
+use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
+use FireflyIII\Http\Requests\InviteUserFormRequest;
use FireflyIII\Http\Requests\UserFormRequest;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
use Log;
@@ -142,9 +145,17 @@ class UserController extends Controller
*/
public function index()
{
- $subTitle = (string) trans('firefly.user_administration');
- $subTitleIcon = 'fa-users';
- $users = $this->repository->all();
+ $subTitle = (string) trans('firefly.user_administration');
+ $subTitleIcon = 'fa-users';
+ $users = $this->repository->all();
+ $singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
+ $allowInvites = false;
+ if (!$this->externalIdentity && $singleUserMode) {
+ // also registration enabled.
+ $allowInvites = true;
+ }
+
+ $invitedUsers = $this->repository->getInvitedUsers();
// add meta stuff.
$users->each(
@@ -154,7 +165,7 @@ class UserController extends Controller
}
);
- return view('admin.users.index', compact('subTitle', 'subTitleIcon', 'users'));
+ return view('admin.users.index', compact('subTitle', 'subTitleIcon', 'users', 'allowInvites', 'invitedUsers'));
}
/**
@@ -185,6 +196,22 @@ class UserController extends Controller
);
}
+ /**
+ * @param InviteUserFormRequest $request
+ * @return RedirectResponse
+ */
+ public function invite(InviteUserFormRequest $request): RedirectResponse
+ {
+ $address = (string) $request->get('invited_user');
+ $invitee = $this->repository->inviteUser(auth()->user(), $address);
+ session()->flash('info', trans('firefly.user_is_invited', ['address' => $address]));
+
+ // event!
+ event(new InvitationCreated($invitee));
+
+ return redirect(route('admin.users'));
+ }
+
/**
* Update single user.
*
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index c7a5511215..3722b19669 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -25,6 +25,7 @@ namespace FireflyIII\Http\Controllers\Auth;
use FireflyIII\Events\RegisteredUser;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
+use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Http\Controllers\CreateStuff;
use FireflyIII\User;
use Illuminate\Contracts\Foundation\Application;
@@ -88,11 +89,15 @@ class RegisterController extends Controller
public function register(Request $request)
{
$allowRegistration = $this->allowedToRegister();
+ $inviteCode = (string) $request->get('invite_code');
+ $repository = app(UserRepositoryInterface::class);
+ $validCode = $repository->validateInviteCode($inviteCode);
- if (false === $allowRegistration) {
+ if (false === $allowRegistration && false === $validCode) {
throw new FireflyException('Registration is currently not available :(');
}
+
$this->validator($request->all())->validate();
$user = $this->createUser($request->all());
Log::info(sprintf('Registered new user %s', $user->email));
@@ -104,6 +109,10 @@ class RegisterController extends Controller
$this->registered($request, $user);
+ if ($validCode) {
+ $repository->redeemCode($inviteCode);
+ }
+
return redirect($this->redirectPath());
}
@@ -157,4 +166,39 @@ class RegisterController extends Controller
return view('auth.register', compact('isDemoSite', 'email', 'pageTitle'));
}
+
+ /**
+ * Show the application registration form if the invitation code is valid.
+ *
+ * @param Request $request
+ *
+ * @return Factory|View
+ * @throws ContainerExceptionInterface
+ * @throws FireflyException
+ * @throws NotFoundExceptionInterface
+ */
+ public function showInviteForm(Request $request, string $code)
+ {
+ $isDemoSite = app('fireflyconfig')->get('is_demo_site', config('firefly.configuration.is_demo_site'))->data;
+ $pageTitle = (string) trans('firefly.register_page_title');
+ $repository = app(UserRepositoryInterface::class);
+ $allowRegistration = $this->allowedToRegister();
+ $inviteCode = $code;
+ $validCode = $repository->validateInviteCode($inviteCode);
+
+ if (true === $allowRegistration) {
+ $message = 'You do not need an invite code on this installation.';
+
+ return view('error', compact('message'));
+ }
+ if(false === $validCode) {
+ $message = 'Invalid code.';
+
+ return view('error', compact('message'));
+ }
+
+ $email = $request->old('email');
+
+ return view('auth.register', compact('isDemoSite', 'email', 'pageTitle', 'inviteCode'));
+ }
}
diff --git a/app/Http/Requests/InviteUserFormRequest.php b/app/Http/Requests/InviteUserFormRequest.php
new file mode 100644
index 0000000000..fb603b74ee
--- /dev/null
+++ b/app/Http/Requests/InviteUserFormRequest.php
@@ -0,0 +1,43 @@
+.
+ */
+
+namespace FireflyIII\Http\Requests;
+
+use FireflyIII\Support\Request\ChecksLogin;
+use FireflyIII\Support\Request\ConvertsDataTypes;
+use Illuminate\Foundation\Http\FormRequest;
+
+class InviteUserFormRequest extends FormRequest
+{
+ use ConvertsDataTypes, ChecksLogin;
+
+ /**
+ * Rules for this request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ return [
+ 'invited_user' => 'required|email|unique:invited_users,email',
+ ];
+ }
+}
diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php
new file mode 100644
index 0000000000..22e0c7bf15
--- /dev/null
+++ b/app/Models/InvitedUser.php
@@ -0,0 +1,49 @@
+.
+ */
+
+namespace FireflyIII\Models;
+
+use FireflyIII\User;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * Class InvitedUser
+ */
+class InvitedUser extends Model
+{
+ protected $fillable = ['user_id', 'email', 'invite_code', 'expires', 'redeemed'];
+
+ protected $casts
+ = [
+ 'expires' => 'datetime',
+ 'redeemed' => 'boolean',
+ ];
+
+ /**
+ * @codeCoverageIgnore
+ * @return BelongsTo
+ */
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php
index 34a9e93cc4..47e234bf38 100644
--- a/app/Notifications/Admin/UserInvitation.php
+++ b/app/Notifications/Admin/UserInvitation.php
@@ -22,7 +22,76 @@ declare(strict_types=1);
namespace FireflyIII\Notifications\Admin;
-class UserInvitation
-{
+use FireflyIII\Models\InvitedUser;
+use Illuminate\Bus\Queueable;
+use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Notifications\Messages\SlackMessage;
+use Illuminate\Notifications\Notification;
+/**
+ * Class UserInvitation
+ */
+class UserInvitation extends Notification
+{
+ use Queueable;
+
+ private InvitedUser $invitee;
+
+ /**
+ * Create a new notification instance.
+ *
+ * @return void
+ */
+ public function __construct(InvitedUser $invitee)
+ {
+ $this->invitee = $invitee;
+ }
+
+ /**
+ * Get the notification's delivery channels.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function via($notifiable)
+ {
+ return ['mail', 'slack'];
+ }
+
+ /**
+ * Get the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ return (new MailMessage)
+ ->markdown('emails.invitation-created', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email])
+ ->subject((string) trans('email.invitation_created_subject'));
+ }
+
+ /**
+ * Get the Slack representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return SlackMessage
+ */
+ public function toSlack($notifiable)
+ {
+ return (new SlackMessage)->content((string) trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]));
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ //
+ ];
+ }
}
diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index 99738ab1ce..4811817068 100644
--- a/app/Providers/EventServiceProvider.php
+++ b/app/Providers/EventServiceProvider.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Providers;
use Exception;
use FireflyIII\Events\ActuallyLoggedIn;
+use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\AdminRequestedTestMessage;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\DetectedNewIPAddress;
@@ -113,6 +114,11 @@ class EventServiceProvider extends ServiceProvider
NewVersionAvailable::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion',
],
+ InvitationCreated::class => [
+ 'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
+ //'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
+ ],
+
// is a Transaction Journal related event.
StoredTransactionGroup::class => [
'FireflyIII\Handlers\Events\StoredGroupEventHandler@processRules',
diff --git a/app/Repositories/User/UserRepository.php b/app/Repositories/User/UserRepository.php
index d841a6bc46..4d6c60dc95 100644
--- a/app/Repositories/User/UserRepository.php
+++ b/app/Repositories/User/UserRepository.php
@@ -22,9 +22,11 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\User;
+use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\BudgetLimit;
+use FireflyIII\Models\InvitedUser;
use FireflyIII\Models\Role;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
@@ -103,22 +105,6 @@ class UserRepository implements UserRepositoryInterface
return true;
}
- /**
- * @return int
- */
- public function count(): int
- {
- return $this->all()->count();
- }
-
- /**
- * @return Collection
- */
- public function all(): Collection
- {
- return User::orderBy('id', 'DESC')->get(['users.*']);
- }
-
/**
* @param string $name
* @param string $displayName
@@ -164,6 +150,22 @@ class UserRepository implements UserRepositoryInterface
}
}
+ /**
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->all()->count();
+ }
+
+ /**
+ * @return Collection
+ */
+ public function all(): Collection
+ {
+ return User::orderBy('id', 'DESC')->get(['users.*']);
+ }
+
/**
* @param int $userId
*
@@ -265,6 +267,24 @@ class UserRepository implements UserRepositoryInterface
return false;
}
+ /**
+ * @inheritDoc
+ */
+ public function inviteUser(User $user, string $email): InvitedUser
+ {
+ $now = Carbon::now();
+ $now->addDays(2);
+ $invitee = new InvitedUser;
+ $invitee->user()->associate($user);
+ $invitee->invite_code = Str::random(64);
+ $invitee->email = $email;
+ $invitee->redeemed = false;
+ $invitee->expires = $now;
+ $invitee->save();
+
+ return $invitee;
+ }
+
/**
* Set MFA code.
*
@@ -416,4 +436,34 @@ class UserRepository implements UserRepositoryInterface
{
return Role::where('name', $role)->first();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function getInvitedUsers(): Collection
+ {
+ return InvitedUser::with('user')->get();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validateInviteCode(string $code): bool
+ {
+ $now = Carbon::now();
+ $invitee = InvitedUser::where('invite_code', $code)->where('expires', '<=', $now)->where('redeemed', 0)->first();
+ return null !== $invitee;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function redeemCode(string $code): void
+ {
+ $obj = InvitedUser::where('invite_code', $code)->where('redeemed', 0)->first();
+ if ($obj) {
+ $obj->redeemed = true;
+ $obj->save();
+ }
+ }
}
diff --git a/app/Repositories/User/UserRepositoryInterface.php b/app/Repositories/User/UserRepositoryInterface.php
index 410162b2e4..cf5a22d59d 100644
--- a/app/Repositories/User/UserRepositoryInterface.php
+++ b/app/Repositories/User/UserRepositoryInterface.php
@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\User;
+use FireflyIII\Models\InvitedUser;
use FireflyIII\Models\Role;
use FireflyIII\User;
use Illuminate\Support\Collection;
@@ -159,6 +160,30 @@ interface UserRepositoryInterface
*/
public function hasRole(User $user, string $role): bool;
+ /**
+ * @param User $user
+ * @param string $email
+ * @return InvitedUser
+ */
+ public function inviteUser(User $user, string $email): InvitedUser;
+
+ /**
+ * @return Collection
+ */
+ public function getInvitedUsers(): Collection;
+
+ /**
+ * @param string $code
+ * @return bool
+ */
+ public function validateInviteCode(string $code): bool;
+
+ /**
+ * @param string $code
+ * @return void
+ */
+ public function redeemCode(string $code): void;
+
/**
* Remove any role the user has.
*
diff --git a/app/User.php b/app/User.php
index 67e4801820..2479143db7 100644
--- a/app/User.php
+++ b/app/User.php
@@ -49,6 +49,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\Webhook;
use FireflyIII\Notifications\Admin\TestNotification;
+use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Admin\UserRegistration;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use Illuminate\Database\Eloquent\Builder;
@@ -599,6 +600,9 @@ class User extends Authenticatable
if ($notification instanceof VersionCheckResult) {
return app('fireflyconfig')->get('slack_webhook_url', '')->data;
}
+ if ($notification instanceof UserInvitation) {
+ return app('fireflyconfig')->get('slack_webhook_url', '')->data;
+ }
return app('preferences')->getForUser($this, 'slack_webhook_url', '')->data;
}
diff --git a/config/firefly.php b/config/firefly.php
index c2f00c93cc..0aaa5f6d5b 100644
--- a/config/firefly.php
+++ b/config/firefly.php
@@ -150,7 +150,7 @@ return [
// notifications
'available_notifications' => ['bill_reminder', 'new_access_token', 'transaction_creation', 'user_login'],
- 'admin_notifications' => ['admin_new_reg', 'user_new_reg', 'new_version'],
+ 'admin_notifications' => ['admin_new_reg', 'user_new_reg', 'new_version', 'invite_created', 'invite_redeemed'],
// enabled languages
'languages' => [
@@ -486,7 +486,7 @@ return [
'update_piggy' => UpdatePiggybank::class,
'delete_transaction' => DeleteTransaction::class,
'append_descr_to_notes' => AppendDescriptionToNotes::class,
- 'append_notes_to_descr' => AppendNotesToDescription::class,
+ 'append_notes_to_descr' => AppendNotesToDescription::class,
'move_descr_to_notes' => MoveDescriptionToNotes::class,
'move_notes_to_descr' => MoveNotesToDescription::class,
],
diff --git a/database/migrations/2022_10_01_074908_invited_users.php b/database/migrations/2022_10_01_074908_invited_users.php
new file mode 100644
index 0000000000..00af7e9129
--- /dev/null
+++ b/database/migrations/2022_10_01_074908_invited_users.php
@@ -0,0 +1,37 @@
+id();
+ $table->timestamps();
+ $table->integer('user_id', false, true);
+ $table->string('email', 255);
+ $table->string('invite_code', 64);
+ $table->dateTime('expires');
+ $table->boolean('redeemed');
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('invited_users');
+ }
+};
diff --git a/frontend/package.json b/frontend/package.json
index e3bf318613..b5f3877fe7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,14 +11,14 @@
},
"dependencies": {
"@popperjs/core": "^2.11.2",
- "@quasar/extras": "^1.14.3",
+ "@quasar/extras": "^1.15.4",
"apexcharts": "^3.32.1",
"axios": "^0.21.1",
"axios-cache-adapter": "^2.7.3",
"core-js": "^3.6.5",
"date-fns": "^2.28.0",
"pinia": "^2.0.14",
- "quasar": "^2.7.5",
+ "quasar": "^2.8.4",
"vue": "3",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.0",
@@ -26,7 +26,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.14",
- "@quasar/app-webpack": "^3.5.7",
+ "@quasar/app-webpack": "^3.6.1",
"@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 854e3ed2b6..30ea0efcd8 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1165,10 +1165,10 @@
resolved "https://registry.yarnpkg.com/@positron/stack-trace/-/stack-trace-1.0.0.tgz#14fcc712a530038ef9be1ce6952315a839f466a8"
integrity sha1-FPzHEqUwA475vhzmlSMVqDn0Zqg=
-"@quasar/app-webpack@^3.5.7":
- version "3.5.7"
- resolved "https://registry.yarnpkg.com/@quasar/app-webpack/-/app-webpack-3.5.7.tgz#1c314288dfa8fd0b4167fd46d74d3e90c21d0ce1"
- integrity sha512-VvOOzvYjJa1ibl/w+dcvnBP9r97dtVHqaf3N7tTj7QaT667QX/BN6wCx2qKh8KOFODvKVs8vt/Nvvw02pWn7YA==
+"@quasar/app-webpack@^3.6.1":
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/@quasar/app-webpack/-/app-webpack-3.6.1.tgz#f2189316e931b77df9c3c63bf21f6108c3111a2a"
+ integrity sha512-dwHs34fMXPaAY6iO4B0Qs6zexIS8kv9kI9LuYDEiU9uHndn6roH0Wsnk6MjiHk/4zPqFAKIVn6zF6FdE5nD9nA==
dependencies:
"@quasar/babel-preset-app" "2.0.1"
"@quasar/fastclick" "1.1.5"
@@ -1215,7 +1215,7 @@
ouch "^2.0.1"
postcss "^8.4.4"
postcss-loader "6.2.1"
- postcss-rtlcss "3.6.3"
+ postcss-rtlcss "3.7.2"
pretty-error "4.0.0"
register-service-worker "1.7.2"
sass "1.32.12"
@@ -1231,7 +1231,7 @@
webpack "^5.58.1"
webpack-bundle-analyzer "4.5.0"
webpack-chain "6.5.1"
- webpack-dev-server "4.9.2"
+ webpack-dev-server "4.9.3"
webpack-merge "5.8.0"
webpack-node-externals "3.0.0"
@@ -1261,10 +1261,10 @@
core-js "^3.6.5"
core-js-compat "^3.6.5"
-"@quasar/extras@^1.14.3":
- version "1.14.3"
- resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.14.3.tgz#2a4d7a2f773a789ca43e3a02d5b797c9d2888884"
- integrity sha512-OHyR/pfW0R8E5DvnsV1wg9ISnLL/yXHyOMZsqPY3gUtmfdF2634x2osdVg4YpBYW29vIQeV5feGWGIx8nuprdA==
+"@quasar/extras@^1.15.4":
+ version "1.15.4"
+ resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.15.4.tgz#c3c78416c2c39e4d4e791e8bd4dd6ff4b7e14b14"
+ integrity sha512-GGURiH/K/IZM41RD9hcNG0Zly63sFGFZ97Q+doVMFSGBqNySfVNsb3WFSovOyL5K/Lfnb/sjzslroVIUoDVTKw==
"@quasar/fastclick@1.1.5":
version "1.1.5"
@@ -2481,10 +2481,10 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-connect-history-api-fallback@^1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
- integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
+connect-history-api-fallback@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8"
+ integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
content-disposition@0.5.4:
version "0.5.4"
@@ -5158,10 +5158,10 @@ postcss-reduce-transforms@^5.1.0:
dependencies:
postcss-value-parser "^4.2.0"
-postcss-rtlcss@3.6.3:
- version "3.6.3"
- resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-3.6.3.tgz#aabd1122a5b082157ea06d606c441002c1060030"
- integrity sha512-jJlS7gM5JPH8n/hcHqqekK8wusdFEFYi79mBvAK2GWvl3aehOFgj9vEMwFzUTJrrErakYTgiQ+uuGAzdL98g0g==
+postcss-rtlcss@3.7.2:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-3.7.2.tgz#0a06cbfd74aec36ad1fe6c6fd1ec74069c62ce45"
+ integrity sha512-GurrGedCKvOTe1QrifI+XpDKXA3bJky1v8KiOa/TYYHs1bfJOxI53GIRvVSqLJLly7e1WcNMz8KMESTN01vbZQ==
dependencies:
rtlcss "^3.5.0"
@@ -5259,10 +5259,10 @@ qs@6.9.7:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
-quasar@^2.7.5:
- version "2.7.5"
- resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.7.5.tgz#a3feb5d50647313c4d6e1451223c158e10792902"
- integrity sha512-DWI0S+bXASfMSPrB8c/LVsXpA4dF7cBUbaJlcrM+1ioTNBHtiudma2Nhk2SDd5bzk9AYVHh5A8JCZuKqQAXt7g==
+quasar@^2.8.4:
+ version "2.8.4"
+ resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.8.4.tgz#d32d7f0c1c4f313ee45f8f3d72028f3085727172"
+ integrity sha512-bygg0GgSwQyrUJJTaHmYV50nVrz779QsNeH/cg2R/SHOQ4UmJI2FBA1hxU/nlpJ6DbmezNab1COa5ID57PvKfw==
queue-microtask@^1.2.2:
version "1.2.3"
@@ -6404,10 +6404,10 @@ webpack-dev-middleware@^5.3.1:
range-parser "^1.2.1"
schema-utils "^4.0.0"
-webpack-dev-server@4.9.2:
- version "4.9.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.2.tgz#c188db28c7bff12f87deda2a5595679ebbc3c9bc"
- integrity sha512-H95Ns95dP24ZsEzO6G9iT+PNw4Q7ltll1GfJHV4fKphuHWgKFzGHWi4alTlTnpk1SPPk41X+l2RB7rLfIhnB9Q==
+webpack-dev-server@4.9.3:
+ version "4.9.3"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz#2360a5d6d532acb5410a668417ad549ee3b8a3c9"
+ integrity sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==
dependencies:
"@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5"
@@ -6421,7 +6421,7 @@ webpack-dev-server@4.9.2:
chokidar "^3.5.3"
colorette "^2.0.10"
compression "^1.7.4"
- connect-history-api-fallback "^1.6.0"
+ connect-history-api-fallback "^2.0.0"
default-gateway "^6.0.3"
express "^4.17.3"
graceful-fs "^4.2.6"
diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php
index f796c7b82d..a8d96e80b4 100644
--- a/resources/lang/en_US/email.php
+++ b/resources/lang/en_US/email.php
@@ -33,6 +33,10 @@ return [
'admin_test_subject' => 'A test message from your Firefly III installation',
'admin_test_body' => 'This is a test message from your Firefly III instance. It was sent to :email.',
+ // invite
+ 'invitation_created_subject' => 'An invitation has been created',
+ 'invitation_created_body' => 'Admin user ":email" created a user invitation which can be used by whoever is behind email address ":invitee". The invite will be valid for 48hrs.',
+
// new IP
'login_from_new_ip' => 'New login on Firefly III',
'slack_login_from_new_ip' => 'New Firefly III login from IP :ip (:host)',
diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php
index 73888a04d8..b3321b00c7 100644
--- a/resources/lang/en_US/firefly.php
+++ b/resources/lang/en_US/firefly.php
@@ -2221,62 +2221,70 @@ return [
'updated_tag' => 'Updated tag ":tag"',
'created_tag' => 'Tag ":tag" has been created!',
- 'transaction_journal_information' => 'Transaction information',
- 'transaction_journal_meta' => 'Meta information',
- 'transaction_journal_more' => 'More information',
- 'basic_journal_information' => 'Basic transaction information',
- 'transaction_journal_extra' => 'Extra information',
- 'att_part_of_journal' => 'Stored under ":journal"',
- 'total_amount' => 'Total amount',
- 'number_of_decimals' => 'Number of decimals',
+ 'transaction_journal_information' => 'Transaction information',
+ 'transaction_journal_meta' => 'Meta information',
+ 'transaction_journal_more' => 'More information',
+ 'basic_journal_information' => 'Basic transaction information',
+ 'transaction_journal_extra' => 'Extra information',
+ 'att_part_of_journal' => 'Stored under ":journal"',
+ 'total_amount' => 'Total amount',
+ 'number_of_decimals' => 'Number of decimals',
// administration
- 'administration' => 'Administration',
- 'user_administration' => 'User administration',
- 'list_all_users' => 'All users',
- 'all_users' => 'All users',
- 'instance_configuration' => 'Configuration',
- 'firefly_instance_configuration' => 'Configuration options for Firefly III',
- 'setting_single_user_mode' => 'Single user mode',
- 'setting_single_user_mode_explain' => 'By default, Firefly III only accepts one (1) registration: you. This is a security measure, preventing others from using your instance unless you allow them to. Future registrations are blocked. When you uncheck this box, others can use your instance as well, assuming they can reach it (when it is connected to the internet).',
- 'store_configuration' => 'Store configuration',
- 'single_user_administration' => 'User administration for :email',
- 'edit_user' => 'Edit user :email',
- 'hidden_fields_preferences' => 'You can enable more transaction options in your preferences.',
- 'user_data_information' => 'User data',
- 'user_information' => 'User information',
- 'total_size' => 'total size',
- 'budget_or_budgets' => ':count budget|:count budgets',
- 'budgets_with_limits' => ':count budget with configured amount|:count budgets with configured amount',
- 'nr_of_rules_in_total_groups' => ':count_rules rule(s) in :count_groups rule group(s)',
- 'tag_or_tags' => ':count tag|:count tags',
- 'configuration_updated' => 'The configuration has been updated',
- 'setting_is_demo_site' => 'Demo site',
- 'setting_is_demo_site_explain' => 'If you check this box, this installation will behave as if it is the demo site, which can have weird side effects.',
- 'block_code_bounced' => 'Email message(s) bounced',
- 'block_code_expired' => 'Demo account expired',
- 'no_block_code' => 'No reason for block or user not blocked',
- 'block_code_email_changed' => 'User has not yet confirmed new email address',
- 'admin_update_email' => 'Contrary to the profile page, the user will NOT be notified their email address has changed!',
- 'update_user' => 'Update user',
- 'updated_user' => 'User data has been changed.',
- 'delete_user' => 'Delete user :email',
- 'user_deleted' => 'The user has been deleted',
- 'send_test_email' => 'Send test email message',
- 'send_test_email_text' => 'To see if your installation is capable of sending email or posting Slack messages, please press this button. You will not see an error here (if any), the log files will reflect any errors. You can press this button as many times as you like. There is no spam control. The message will be sent to :email
and should arrive shortly.',
- 'send_message' => 'Send message',
- 'send_test_triggered' => 'Test was triggered. Check your inbox and the log files.',
- 'give_admin_careful' => 'Users who are given admin rights can take away yours. Be careful.',
- 'admin_maintanance_title' => 'Maintenance',
- 'admin_maintanance_expl' => 'Some nifty buttons for Firefly III maintenance',
- 'admin_maintenance_clear_cache' => 'Clear cache',
- 'admin_notifications' => 'Admin notifications',
- 'admin_notifications_expl' => 'The following notifications can be enabled or disabled by the administrator. If you want to get these messages over Slack as well, set the "incoming webhook" URL.',
- 'admin_notification_check_user_new_reg' => 'User gets post-registration welcome message',
- 'admin_notification_check_admin_new_reg' => 'Administrator(s) get new user registration notification',
- 'admin_notification_check_new_version' => 'A new version is available',
- 'save_notification_settings' => 'Save settings',
- 'notification_settings_saved' => 'The notification settings have been saved',
+ 'invite_new_user_title' => 'Invite new user',
+ 'invite_new_user_text' => 'As an administrator, you can invite users to register on your Firefly III administration. Using the direct link you can share with them, they will be able to register an account. The invited user and their invite link will appear in the table below. You are free to share the invitation link with them.',
+ 'invited_user_mail' => 'Email address',
+ 'invite_user' => 'Invite user',
+ 'user_is_invited' => 'Email address ":address" was invited to Firefly III',
+ 'administration' => 'Administration',
+ 'code_already_used' => 'Invite code has been used',
+ 'user_administration' => 'User administration',
+ 'list_all_users' => 'All users',
+ 'all_users' => 'All users',
+ 'instance_configuration' => 'Configuration',
+ 'firefly_instance_configuration' => 'Configuration options for Firefly III',
+ 'setting_single_user_mode' => 'Single user mode',
+ 'setting_single_user_mode_explain' => 'By default, Firefly III only accepts one (1) registration: you. This is a security measure, preventing others from using your instance unless you allow them to. Future registrations are blocked. When you uncheck this box, others can use your instance as well, assuming they can reach it (when it is connected to the internet).',
+ 'store_configuration' => 'Store configuration',
+ 'single_user_administration' => 'User administration for :email',
+ 'edit_user' => 'Edit user :email',
+ 'hidden_fields_preferences' => 'You can enable more transaction options in your preferences.',
+ 'user_data_information' => 'User data',
+ 'user_information' => 'User information',
+ 'total_size' => 'total size',
+ 'budget_or_budgets' => ':count budget|:count budgets',
+ 'budgets_with_limits' => ':count budget with configured amount|:count budgets with configured amount',
+ 'nr_of_rules_in_total_groups' => ':count_rules rule(s) in :count_groups rule group(s)',
+ 'tag_or_tags' => ':count tag|:count tags',
+ 'configuration_updated' => 'The configuration has been updated',
+ 'setting_is_demo_site' => 'Demo site',
+ 'setting_is_demo_site_explain' => 'If you check this box, this installation will behave as if it is the demo site, which can have weird side effects.',
+ 'block_code_bounced' => 'Email message(s) bounced',
+ 'block_code_expired' => 'Demo account expired',
+ 'no_block_code' => 'No reason for block or user not blocked',
+ 'block_code_email_changed' => 'User has not yet confirmed new email address',
+ 'admin_update_email' => 'Contrary to the profile page, the user will NOT be notified their email address has changed!',
+ 'update_user' => 'Update user',
+ 'updated_user' => 'User data has been changed.',
+ 'delete_user' => 'Delete user :email',
+ 'user_deleted' => 'The user has been deleted',
+ 'send_test_email' => 'Send test email message',
+ 'send_test_email_text' => 'To see if your installation is capable of sending email or posting Slack messages, please press this button. You will not see an error here (if any), the log files will reflect any errors. You can press this button as many times as you like. There is no spam control. The message will be sent to :email
and should arrive shortly.',
+ 'send_message' => 'Send message',
+ 'send_test_triggered' => 'Test was triggered. Check your inbox and the log files.',
+ 'give_admin_careful' => 'Users who are given admin rights can take away yours. Be careful.',
+ 'admin_maintanance_title' => 'Maintenance',
+ 'admin_maintanance_expl' => 'Some nifty buttons for Firefly III maintenance',
+ 'admin_maintenance_clear_cache' => 'Clear cache',
+ 'admin_notifications' => 'Admin notifications',
+ 'admin_notifications_expl' => 'The following notifications can be enabled or disabled by the administrator. If you want to get these messages over Slack as well, set the "incoming webhook" URL.',
+ 'admin_notification_check_user_new_reg' => 'User gets post-registration welcome message',
+ 'admin_notification_check_admin_new_reg' => 'Administrator(s) get new user registration notification',
+ 'admin_notification_check_new_version' => 'A new version is available',
+ 'admin_notification_check_invite_created' => 'A user is invited to Firefly III',
+ 'admin_notification_check_invite_redeemed' => 'A user invitation is redeemed',
+ 'save_notification_settings' => 'Save settings',
+ 'notification_settings_saved' => 'The notification settings have been saved',
'split_transaction_title' => 'Description of the split transaction',
diff --git a/resources/lang/en_US/list.php b/resources/lang/en_US/list.php
index d0f58a6904..98b6061df9 100644
--- a/resources/lang/en_US/list.php
+++ b/resources/lang/en_US/list.php
@@ -43,6 +43,10 @@ return [
'lastActivity' => 'Last activity',
'balanceDiff' => 'Balance difference',
'other_meta_data' => 'Other meta data',
+ 'invited_at' => 'Invited at',
+ 'expires' => 'Invitation expires',
+ 'invited_by' => 'Invited by',
+ 'invite_link' => 'Invite link',
'account_type' => 'Account type',
'created_at' => 'Created at',
'account' => 'Account',
diff --git a/resources/views/admin/users/index.twig b/resources/views/admin/users/index.twig
index 426fba58d5..d2bc9d5b7a 100644
--- a/resources/views/admin/users/index.twig
+++ b/resources/views/admin/users/index.twig
@@ -4,6 +4,31 @@
{{ Breadcrumbs.render }}
{% endblock %}
{% block content %}
+ {% if allowInvites %}
+
+ | {{ trans('list.email') }} | +{{ trans('list.invited_at') }} | +{{ trans('list.expires') }} | +{{ trans('list.invited_by') }} | +{{ trans('list.invite_link') }} | +
---|---|---|---|---|---|
+ + | ++ {{ invitee.email }} + | ++ {{ invitee.created_at.isoFormat(monthAndDayFormat) }} + {{ invitee.created_at.format('H:i') }} + | ++ {{ invitee.expires.isoFormat(monthAndDayFormat) }} + {{ invitee.expires.format('H:i') }} + | ++ {{ invitee.user.email }} + | +
+ {% if invitee.redeemed %}
+ |
+