mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-22 03:56:42 +00:00
Add ability to invite users
This commit is contained in:
49
app/Events/Admin/InvitationCreated.php
Normal file
49
app/Events/Admin/InvitationCreated.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/*
|
||||
* InvitationCreated.php
|
||||
* Copyright (c) 2022 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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.
|
||||
*
|
||||
|
@@ -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;
|
||||
@@ -145,6 +148,14 @@ class UserController extends Controller
|
||||
$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.
|
||||
*
|
||||
|
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
43
app/Http/Requests/InviteUserFormRequest.php
Normal file
43
app/Http/Requests/InviteUserFormRequest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* InviteUserFormRequest.php
|
||||
* Copyright (c) 2022 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
49
app/Models/InvitedUser.php
Normal file
49
app/Models/InvitedUser.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/*
|
||||
* InvitedUser.php
|
||||
* Copyright (c) 2022 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -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 [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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' => [
|
||||
|
37
database/migrations/2022_10_01_074908_invited_users.php
Normal file
37
database/migrations/2022_10_01_074908_invited_users.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invited_users', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
@@ -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",
|
||||
|
@@ -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"
|
||||
|
@@ -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)',
|
||||
|
@@ -2231,7 +2231,13 @@ return [
|
||||
'number_of_decimals' => 'Number of decimals',
|
||||
|
||||
// administration
|
||||
'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',
|
||||
@@ -2275,6 +2281,8 @@ return [
|
||||
'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',
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -4,6 +4,31 @@
|
||||
{{ Breadcrumbs.render }}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% if allowInvites %}
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<form action="{{ route('admin.users.invite') }}" method="post">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ 'invite_new_user_title'|_ }}</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
{{ 'invite_new_user_text'|_ }}
|
||||
</p>
|
||||
{{ ExpandedForm.text('invited_user',null, {'type': 'email', 'label' : 'invited_user_mail'|_}) }}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn pull-right btn-success">
|
||||
{{ ('invite_user')|_ }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="box box-primary">
|
||||
@@ -29,8 +54,10 @@
|
||||
<tr>
|
||||
<td class="hidden-xs" data-value="{{ user.id }}">
|
||||
<div class="btn-group btn-group-xs">
|
||||
<a class="btn btn-default" href="{{ route('admin.users.edit',user.id) }}"><span class="fa fa-pencil"></span></a>
|
||||
<a class="btn btn-danger" href="{{ route('admin.users.delete',user.id) }}"><span class="fa fa-trash"></span></a>
|
||||
<a class="btn btn-default" href="{{ route('admin.users.edit',user.id) }}"><span
|
||||
class="fa fa-pencil"></span></a>
|
||||
<a class="btn btn-danger" href="{{ route('admin.users.delete',user.id) }}"><span
|
||||
class="fa fa-trash"></span></a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden-xs" data-value="{{ user.id }}">#{{ user.id }}</td>
|
||||
@@ -60,9 +87,11 @@
|
||||
</td>
|
||||
<td data-value="{% if user.blocked %}1{% else %}0{% endif %}">
|
||||
{% if user.blocked == 1 %}
|
||||
<small class="text-danger"><span class="fa fa-fw fa-check" title="{{ 'yes'|_ }}"></span></small>
|
||||
<small class="text-danger"><span class="fa fa-fw fa-check"
|
||||
title="{{ 'yes'|_ }}"></span></small>
|
||||
{% else %}
|
||||
<small class="text-success"><span class="fa fa-fw fa-times" title="{{ 'no'|_ }}"></span></small>
|
||||
<small class="text-success"><span class="fa fa-fw fa-times"
|
||||
title="{{ 'no'|_ }}"></span></small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="hidden-xs">
|
||||
@@ -84,10 +113,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if invitedUsers.count > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ 'all_invited_users'|_ }}</h3>
|
||||
</div>
|
||||
<div class="box-body no-padding">
|
||||
<table class="table table-responsive table-condensed sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-defaultsign="_19" class="hidden-xs" colspan="1"> </th>
|
||||
<th data-defaultsign="az">{{ trans('list.email') }}</th>
|
||||
<th data-defaultsign="month" class="hidden-xs">{{ trans('list.invited_at') }}</th>
|
||||
<th data-defaultsign="month" class="hidden-xs">{{ trans('list.expires') }}</th>
|
||||
<th class="hidden-xs">{{ trans('list.invited_by') }}</th>
|
||||
<th>{{ trans('list.invite_link') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invitee in invitedUsers %}
|
||||
<tr>
|
||||
<td class="hidden-xs" data-value="{{ user.id }}">
|
||||
<div class="btn-group btn-group-xs">
|
||||
<a class="btn btn-danger" href="{{ route('admin.users.delete-invite', invitee.id) }}"><span
|
||||
class="fa fa-trash"></span></a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ invitee.email }}
|
||||
</td>
|
||||
<td class="hidden-xs">
|
||||
{{ invitee.created_at.isoFormat(monthAndDayFormat) }}
|
||||
{{ invitee.created_at.format('H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ invitee.expires.isoFormat(monthAndDayFormat) }}
|
||||
{{ invitee.expires.format('H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ invitee.user.email }}
|
||||
</td>
|
||||
<td>
|
||||
{% if invitee.redeemed %}
|
||||
<em><s>{{ 'code_already_used'|_ }}</s></em>
|
||||
{% else %}
|
||||
<input type="text" class="form-control" readonly value="{{ route('invite', [invitee.invite_code]) }}">
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="v1/css/bootstrap-sortable.css?v={{ FF_VERSION }}" type="text/css" media="all" nonce="{{ JS_NONCE }}">
|
||||
<link rel="stylesheet" href="v1/css/bootstrap-sortable.css?v={{ FF_VERSION }}" type="text/css" media="all"
|
||||
nonce="{{ JS_NONCE }}">
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="v1/js/lib/bootstrap-sortable.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
|
||||
<script type="text/javascript" src="v1/js/lib/bootstrap-sortable.js?v={{ FF_VERSION }}"
|
||||
nonce="{{ JS_NONCE }}"></script>
|
||||
{% endblock %}
|
||||
|
@@ -23,6 +23,7 @@
|
||||
|
||||
<form action="{{ route('register') }}" method="post">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="invite_code" value="{{ inviteCode }}">
|
||||
<div class="input-group mb-3">
|
||||
<input type="email" name="email" value="{{ email }}" class="form-control"
|
||||
placeholder="{{ trans('form.email') }}"/>
|
||||
|
3
resources/views/emails/invitation-created.blade.php
Normal file
3
resources/views/emails/invitation-created.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
@component('mail::message')
|
||||
{{ trans('email.invitation_created_body', ['email' => $email,'invitee' => $invitee]) }}
|
||||
@endcomponent
|
@@ -58,6 +58,7 @@ Route::group(
|
||||
|
||||
// Registration Routes...
|
||||
Route::get('register', ['uses' => 'Auth\RegisterController@showRegistrationForm', 'as' => 'register']);
|
||||
Route::get('invitee/{code}', ['uses' => 'Auth\RegisterController@showInviteForm', 'as' => 'invite']);
|
||||
Route::post('register', 'Auth\RegisterController@register');
|
||||
|
||||
// Password Reset Routes...
|
||||
@@ -1101,6 +1102,10 @@ Route::group(
|
||||
Route::post('users/update/{user}', ['uses' => 'UserController@update', 'as' => 'users.update']);
|
||||
Route::post('users/destroy/{user}', ['uses' => 'UserController@destroy', 'as' => 'users.destroy']);
|
||||
|
||||
// invitee management
|
||||
Route::get('users/delete_invite/{invitedUser}', ['uses' => 'UserController@deleteInvite', 'as' => 'users.delete-invite']);
|
||||
Route::post('users/invite', ['uses' => 'UserController@invite', 'as' => 'users.invite']);
|
||||
|
||||
// journal links manager
|
||||
Route::get('links', ['uses' => 'LinkController@index', 'as' => 'links.index']);
|
||||
Route::get('links/create', ['uses' => 'LinkController@create', 'as' => 'links.create']);
|
||||
|
Reference in New Issue
Block a user