فهرست منبع

Merge pull request #3975 from pixelfed/staging

Add Admin Invites
daniel 2 سال پیش
والد
کامیت
5f8ff9d280

+ 2 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@
 - Manually verify email address (php artisan user:verifyemail) ([682f5f0f](https://github.com/pixelfed/pixelfed/commit/682f5f0f))
 - Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36))
 - Optional home feed caching ([3328b367](https://github.com/pixelfed/pixelfed/commit/3328b367))
+- Admin Invites ([b73ca9a1](https://github.com/pixelfed/pixelfed/commit/b73ca9a1))
 
 ### Updates
 - Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
@@ -59,6 +60,7 @@
 - Update MediaS3GarbageCollector command, disable logging by default and optimize huge invocations ([a14af93b](https://github.com/pixelfed/pixelfed/commit/a14af93b))
 - Update MediaStorageService, clear MediaService and StatusService caches after localToCloud ([de56b0f0](https://github.com/pixelfed/pixelfed/commit/de56b0f0))
 - Add CloudMediaMigrate command to migrate older local media to cloud storage ([382d00d9](https://github.com/pixelfed/pixelfed/commit/382d00d9))
+- Update MediaS3GarbageCollector command, handle thumbnail deletion ([95bbcc38](https://github.com/pixelfed/pixelfed/commit/95bbcc38))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)

+ 179 - 0
app/Console/Commands/AdminInviteCommand.php

@@ -0,0 +1,179 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\AdminInvite;
+use Illuminate\Support\Str;
+
+class AdminInviteCommand extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'admin:invite';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Create an invite link';
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $this->info('       ____  _           ______         __  ');
+        $this->info('      / __ \(_)  _____  / / __/__  ____/ /  ');
+        $this->info('     / /_/ / / |/_/ _ \/ / /_/ _ \/ __  /   ');
+        $this->info('    / ____/ />  </  __/ / __/  __/ /_/ /    ');
+        $this->info('   /_/   /_/_/|_|\___/_/_/  \___/\__,_/     ');
+        $this->info(' ');
+        $this->info('    Pixelfed Admin Inviter');
+        $this->line(' ');
+        $this->info('    Manage user registration invite links');
+        $this->line(' ');
+
+        $action = $this->choice(
+            'Select an action',
+            [
+                'Create invite',
+                'View invites',
+                'Expire invite',
+                'Cancel'
+            ],
+            3
+        );
+
+        switch($action) {
+            case 'Create invite':
+                return $this->create();
+            break;
+
+            case 'View invites':
+                return $this->view();
+            break;
+
+            case 'Expire invite':
+                return $this->expire();
+            break;
+
+            case 'Cancel':
+                return;
+            break;
+        }
+    }
+
+    protected function create()
+    {
+        $this->info('Create Invite');
+        $this->line('=============');
+        $this->info('Set an optional invite name (only visible to admins)');
+        $name = $this->ask('Invite Name (optional)', 'Untitled Invite');
+
+        $this->info('Set an optional invite description (only visible to admins)');
+        $description = $this->ask('Invite Description (optional)');
+
+        $this->info('Set an optional message to invitees (visible to all)');
+        $message = $this->ask('Invite Message (optional)', 'You\'ve been invited to join');
+
+        $this->info('Set maximum # of invite uses, use 0 for unlimited');
+        $max_uses = $this->ask('Max uses', 1);
+
+        $shouldExpire = $this->choice(
+            'Set an invite expiry date?',
+            [
+                'No - invite never expires',
+                'Yes - expire after 24 hours',
+                'Custom - let me pick an expiry date'
+            ],
+            0
+        );
+        switch($shouldExpire) {
+            case 'No - invite never expires':
+                $expires = null;
+            break;
+
+            case 'Yes - expire after 24 hours':
+                $expires = now()->addHours(24);
+            break;
+
+            case 'Custom - let me pick an expiry date':
+                $this->info('Set custom expiry date in days');
+                $customExpiry = $this->ask('Custom Expiry', 14);
+                $expires = now()->addDays($customExpiry);
+            break;
+        }
+
+        $this->info('Skip email verification for invitees?');
+        $skipEmailVerification = $this->choice('Skip email verification', ['No', 'Yes'], 0);
+
+        $invite = new AdminInvite;
+        $invite->name = $name;
+        $invite->description = $description;
+        $invite->message = $message;
+        $invite->max_uses = $max_uses;
+        $invite->skip_email_verification = $skipEmailVerification === 'Yes';
+        $invite->expires_at = $expires;
+        $invite->invite_code = Str::uuid() . Str::random(random_int(1,6));
+        $invite->save();
+
+        $this->info('####################');
+        $this->info('# Invite Generated!');
+        $this->line(' ');
+        $this->info($invite->url());
+        $this->line(' ');
+        return Command::SUCCESS;
+    }
+
+    protected function view()
+    {
+        $this->info('View Invites');
+        $this->line('=============');
+        if(AdminInvite::count() == 0) {
+            $this->line(' ');
+            $this->error('No invites found!');
+            return;
+        }
+        $this->table(
+            ['Invite Code', 'Uses Left', 'Expires'],
+            AdminInvite::all(['invite_code', 'max_uses', 'uses', 'expires_at'])->map(function($invite) {
+                return [
+                    'invite_code' => $invite->invite_code,
+                    'uses_left' => $invite->max_uses ? ($invite->max_uses - $invite->uses) : '∞',
+                    'expires_at' => $invite->expires_at ? $invite->expires_at->diffForHumans() : 'never'
+                ];
+            })->toArray()
+        );
+    }
+
+    protected function expire()
+    {
+        $token = $this->anticipate('Enter invite code to expire', function($val) {
+            if(!$val || empty($val)) {
+                return [];
+            }
+            return AdminInvite::where('invite_code', 'like', '%' . $val . '%')->pluck('invite_code')->toArray();
+        });
+
+        if(!$token || empty($token)) {
+            $this->error('Invalid invite code');
+            return;
+        }
+        $invite = AdminInvite::whereInviteCode($token)->first();
+        if(!$invite) {
+            $this->error('Invalid invite code');
+            return;
+        }
+        $invite->max_uses = 1;
+        $invite->expires_at = now()->subHours(2);
+        $invite->save();
+        $this->info('Expired the following invite: ' . $invite->url());
+    }
+}

+ 191 - 0
app/Http/Controllers/AdminInviteController.php

@@ -0,0 +1,191 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Models\AdminInvite;
+use App\Profile;
+use App\User;
+use App\Util\Lexer\RestrictedNames;
+use Illuminate\Foundation\Auth\RegistersUsers;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Auth\Events\Registered;
+use App\Services\EmailService;
+use App\Http\Controllers\Auth\RegisterController;
+
+class AdminInviteController extends Controller
+{
+    public function __construct()
+    {
+        abort_if(!config('instance.admin_invites.enabled'), 404);
+    }
+
+    public function index(Request $request, $code)
+    {
+        if($request->user()) {
+            return redirect('/');
+        }
+        return view('invite.admin_invite', compact('code'));
+    }
+
+    public function apiVerifyCheck(Request $request)
+    {
+        $this->validate($request, [
+            'token' => 'required',
+        ]);
+
+        $invite = AdminInvite::whereInviteCode($request->input('token'))->first();
+        abort_if(!$invite, 404);
+        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
+        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+        $res = [
+            'message' => $invite->message,
+            'max_uses' => $invite->max_uses,
+            'sev' => $invite->skip_email_verification
+        ];
+        return response()->json($res);
+    }
+
+    public function apiUsernameCheck(Request $request)
+    {
+        $this->validate($request, [
+            'token' => 'required',
+            'username' => 'required'
+        ]);
+
+        $invite = AdminInvite::whereInviteCode($request->input('token'))->first();
+        abort_if(!$invite, 404);
+        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
+        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+
+        $usernameRules = [
+            'required',
+            'min:2',
+            'max:15',
+            'unique:users',
+            function ($attribute, $value, $fail) {
+                $dash = substr_count($value, '-');
+                $underscore = substr_count($value, '_');
+                $period = substr_count($value, '.');
+
+                if(ends_with($value, ['.php', '.js', '.css'])) {
+                    return $fail('Username is invalid.');
+                }
+
+                if(($dash + $underscore + $period) > 1) {
+                    return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+                }
+
+                if (!ctype_alnum($value[0])) {
+                    return $fail('Username is invalid. Must start with a letter or number.');
+                }
+
+                if (!ctype_alnum($value[strlen($value) - 1])) {
+                    return $fail('Username is invalid. Must end with a letter or number.');
+                }
+
+                $val = str_replace(['_', '.', '-'], '', $value);
+                if(!ctype_alnum($val)) {
+                    return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+                }
+
+                $restricted = RestrictedNames::get();
+                if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+                    return $fail('Username cannot be used.');
+                }
+            },
+        ];
+
+        $rules = ['username' => $usernameRules];
+        $validator = Validator::make($request->all(), $rules);
+
+        if($validator->fails()) {
+            return response()->json($validator->errors(), 400);
+        }
+
+        return response()->json([]);
+    }
+
+    public function apiEmailCheck(Request $request)
+    {
+        $this->validate($request, [
+            'token' => 'required',
+            'email' => 'required'
+        ]);
+
+        $invite = AdminInvite::whereInviteCode($request->input('token'))->first();
+        abort_if(!$invite, 404);
+        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
+        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+
+        $emailRules = [
+            'required',
+            'string',
+            'email',
+            'max:255',
+            'unique:users',
+            function ($attribute, $value, $fail) {
+                $banned = EmailService::isBanned($value);
+                if($banned) {
+                    return $fail('Email is invalid.');
+                }
+            },
+        ];
+
+        $rules = ['email' => $emailRules];
+        $validator = Validator::make($request->all(), $rules);
+
+        if($validator->fails()) {
+            return response()->json($validator->errors(), 400);
+        }
+
+        return response()->json([]);
+    }
+
+    public function apiRegister(Request $request)
+    {
+        $this->validate($request, [
+            'token' => 'required',
+            'username' => 'required',
+            'name' => 'nullable',
+            'email' => 'required|email',
+            'password' => 'required',
+            'password_confirm' => 'required'
+        ]);
+
+        $invite = AdminInvite::whereInviteCode($request->input('token'))->firstOrFail();
+        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite expired');
+        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+
+        $invite->uses = $invite->uses + 1;
+
+        event(new Registered($user = User::create([
+            'name'     => $request->input('name') ?? $request->input('username'),
+            'username' => $request->input('username'),
+            'email'    => $request->input('email'),
+            'password' => Hash::make($request->input('password')),
+        ])));
+        $invite->used_by = array_merge($invite->used_by ?? [], [[
+            'user_id' => $user->id,
+            'username' => $user->username
+        ]]);
+        $invite->save();
+
+        if($invite->skip_email_verification) {
+            $user->email_verified_at = now();
+            $user->save();
+        }
+
+        if(Auth::attempt([
+            'email' => $request->input('email'),
+            'password' => $request->input('password')
+        ])) {
+            $request->session()->regenerate();
+            return redirect()->intended('/');
+        } else {
+            return response()->json([], 400);
+        }
+    }
+}

+ 21 - 0
app/Models/AdminInvite.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class AdminInvite extends Model
+{
+    use HasFactory;
+
+    protected $casts = [
+        'used_by' => 'array',
+        'expires_at' => 'datetime',
+    ];
+
+    public function url()
+    {
+        return url('/auth/invite/a/' . $this->invite_code);
+    }
+}

+ 173 - 169
composer.lock

@@ -114,16 +114,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.253.0",
+            "version": "3.254.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "1fc9d166dd8ee7c2a187cf8f3ed9342863208865"
+                "reference": "9e07cddf9be6ab241c241344ca2e9cf33e32a22e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1fc9d166dd8ee7c2a187cf8f3ed9342863208865",
-                "reference": "1fc9d166dd8ee7c2a187cf8f3ed9342863208865",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9e07cddf9be6ab241c241344ca2e9cf33e32a22e",
+                "reference": "9e07cddf9be6ab241c241344ca2e9cf33e32a22e",
                 "shasum": ""
             },
             "require": {
@@ -202,9 +202,9 @@
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.253.0"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.254.0"
             },
-            "time": "2022-12-12T19:23:54+00:00"
+            "time": "2022-12-19T19:23:23+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -262,16 +262,16 @@
         },
         {
             "name": "beyondcode/laravel-websockets",
-            "version": "1.13.1",
+            "version": "1.13.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/beyondcode/laravel-websockets.git",
-                "reference": "f0649b65fb5562d20eff66f61716ef98717e228a"
+                "reference": "50f8a1e77227a2d2302d45b99185d68a1c1c6866"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/beyondcode/laravel-websockets/zipball/f0649b65fb5562d20eff66f61716ef98717e228a",
-                "reference": "f0649b65fb5562d20eff66f61716ef98717e228a",
+                "url": "https://api.github.com/repos/beyondcode/laravel-websockets/zipball/50f8a1e77227a2d2302d45b99185d68a1c1c6866",
+                "reference": "50f8a1e77227a2d2302d45b99185d68a1c1c6866",
                 "shasum": ""
             },
             "require": {
@@ -338,9 +338,9 @@
             ],
             "support": {
                 "issues": "https://github.com/beyondcode/laravel-websockets/issues",
-                "source": "https://github.com/beyondcode/laravel-websockets/tree/1.13.1"
+                "source": "https://github.com/beyondcode/laravel-websockets/tree/1.13.2"
             },
-            "time": "2022-03-03T08:41:47+00:00"
+            "time": "2022-10-19T18:15:42+00:00"
         },
         {
             "name": "brick/math",
@@ -1576,16 +1576,16 @@
         },
         {
             "name": "firebase/php-jwt",
-            "version": "v6.3.1",
+            "version": "v6.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/firebase/php-jwt.git",
-                "reference": "ddfaddcb520488b42bca3a75e17e9dd53c3667da"
+                "reference": "ea7dda77098b96e666c5ef382452f94841e439cd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/firebase/php-jwt/zipball/ddfaddcb520488b42bca3a75e17e9dd53c3667da",
-                "reference": "ddfaddcb520488b42bca3a75e17e9dd53c3667da",
+                "url": "https://api.github.com/repos/firebase/php-jwt/zipball/ea7dda77098b96e666c5ef382452f94841e439cd",
+                "reference": "ea7dda77098b96e666c5ef382452f94841e439cd",
                 "shasum": ""
             },
             "require": {
@@ -1632,9 +1632,9 @@
             ],
             "support": {
                 "issues": "https://github.com/firebase/php-jwt/issues",
-                "source": "https://github.com/firebase/php-jwt/tree/v6.3.1"
+                "source": "https://github.com/firebase/php-jwt/tree/v6.3.2"
             },
-            "time": "2022-11-01T21:20:08+00:00"
+            "time": "2022-12-19T17:10:46+00:00"
         },
         {
             "name": "fruitcake/laravel-cors",
@@ -2400,16 +2400,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v9.43.0",
+            "version": "v9.44.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "011f2e1d49a11c22519a7899b46ddf3bc5b0f40b"
+                "reference": "60808a7d9acd53461fd69634c08fc7e0a99fbf98"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/011f2e1d49a11c22519a7899b46ddf3bc5b0f40b",
-                "reference": "011f2e1d49a11c22519a7899b46ddf3bc5b0f40b",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/60808a7d9acd53461fd69634c08fc7e0a99fbf98",
+                "reference": "60808a7d9acd53461fd69634c08fc7e0a99fbf98",
                 "shasum": ""
             },
             "require": {
@@ -2582,7 +2582,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2022-12-06T14:26:07+00:00"
+            "time": "2022-12-15T14:56:36+00:00"
         },
         {
             "name": "laravel/helpers",
@@ -2642,16 +2642,16 @@
         },
         {
             "name": "laravel/horizon",
-            "version": "v5.10.5",
+            "version": "v5.10.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/horizon.git",
-                "reference": "01b26da26ca8abe3a525a307b1155d52d7293c8d"
+                "reference": "22b6d7c67bb86722cf380dbaed55ff1a3fc84053"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/horizon/zipball/01b26da26ca8abe3a525a307b1155d52d7293c8d",
-                "reference": "01b26da26ca8abe3a525a307b1155d52d7293c8d",
+                "url": "https://api.github.com/repos/laravel/horizon/zipball/22b6d7c67bb86722cf380dbaed55ff1a3fc84053",
+                "reference": "22b6d7c67bb86722cf380dbaed55ff1a3fc84053",
                 "shasum": ""
             },
             "require": {
@@ -2713,9 +2713,9 @@
             ],
             "support": {
                 "issues": "https://github.com/laravel/horizon/issues",
-                "source": "https://github.com/laravel/horizon/tree/v5.10.5"
+                "source": "https://github.com/laravel/horizon/tree/v5.10.6"
             },
-            "time": "2022-11-25T15:57:02+00:00"
+            "time": "2022-12-14T15:24:14+00:00"
         },
         {
             "name": "laravel/passport",
@@ -2985,31 +2985,34 @@
         },
         {
             "name": "lcobucci/clock",
-            "version": "2.2.0",
+            "version": "2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/lcobucci/clock.git",
-                "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3"
+                "reference": "c7aadcd6fd97ed9e199114269c0be3f335e38876"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/lcobucci/clock/zipball/fb533e093fd61321bfcbac08b131ce805fe183d3",
-                "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3",
+                "url": "https://api.github.com/repos/lcobucci/clock/zipball/c7aadcd6fd97ed9e199114269c0be3f335e38876",
+                "reference": "c7aadcd6fd97ed9e199114269c0be3f335e38876",
                 "shasum": ""
             },
             "require": {
-                "php": "^8.0",
-                "stella-maris/clock": "^0.1.4"
+                "php": "~8.1.0 || ~8.2.0",
+                "stella-maris/clock": "^0.1.7"
+            },
+            "provide": {
+                "psr/clock-implementation": "1.0"
             },
             "require-dev": {
                 "infection/infection": "^0.26",
-                "lcobucci/coding-standard": "^8.0",
-                "phpstan/extension-installer": "^1.1",
-                "phpstan/phpstan": "^0.12",
-                "phpstan/phpstan-deprecation-rules": "^0.12",
-                "phpstan/phpstan-phpunit": "^0.12",
-                "phpstan/phpstan-strict-rules": "^0.12",
-                "phpunit/phpunit": "^9.5"
+                "lcobucci/coding-standard": "^9.0",
+                "phpstan/extension-installer": "^1.2",
+                "phpstan/phpstan": "^1.9.4",
+                "phpstan/phpstan-deprecation-rules": "^1.1.1",
+                "phpstan/phpstan-phpunit": "^1.3.2",
+                "phpstan/phpstan-strict-rules": "^1.4.4",
+                "phpunit/phpunit": "^9.5.27"
             },
             "type": "library",
             "autoload": {
@@ -3030,7 +3033,7 @@
             "description": "Yet another clock abstraction",
             "support": {
                 "issues": "https://github.com/lcobucci/clock/issues",
-                "source": "https://github.com/lcobucci/clock/tree/2.2.0"
+                "source": "https://github.com/lcobucci/clock/tree/2.3.0"
             },
             "funding": [
                 {
@@ -3042,7 +3045,7 @@
                     "type": "patreon"
                 }
             ],
-            "time": "2022-04-19T19:34:17+00:00"
+            "time": "2022-12-19T14:38:11+00:00"
         },
         {
             "name": "lcobucci/jwt",
@@ -5081,16 +5084,16 @@
         },
         {
             "name": "phpseclib/phpseclib",
-            "version": "2.0.39",
+            "version": "2.0.40",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpseclib/phpseclib.git",
-                "reference": "f3a0e2b715c40cf1fd270d444901b63311725d63"
+                "reference": "5ef6f8376ddad21f3ce1da429950f7e00ec2292c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f3a0e2b715c40cf1fd270d444901b63311725d63",
-                "reference": "f3a0e2b715c40cf1fd270d444901b63311725d63",
+                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/5ef6f8376ddad21f3ce1da429950f7e00ec2292c",
+                "reference": "5ef6f8376ddad21f3ce1da429950f7e00ec2292c",
                 "shasum": ""
             },
             "require": {
@@ -5171,7 +5174,7 @@
             ],
             "support": {
                 "issues": "https://github.com/phpseclib/phpseclib/issues",
-                "source": "https://github.com/phpseclib/phpseclib/tree/2.0.39"
+                "source": "https://github.com/phpseclib/phpseclib/tree/2.0.40"
             },
             "funding": [
                 {
@@ -5187,7 +5190,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-24T10:49:03+00:00"
+            "time": "2022-12-17T17:22:59+00:00"
         },
         {
             "name": "pixelfed/fractal",
@@ -6210,23 +6213,23 @@
         },
         {
             "name": "ramsey/uuid",
-            "version": "4.6.0",
+            "version": "4.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "ad63bc700e7d021039e30ce464eba384c4a1d40f"
+                "reference": "5ed9ad582647bbc3864ef78db34bdc1afdcf9b49"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/uuid/zipball/ad63bc700e7d021039e30ce464eba384c4a1d40f",
-                "reference": "ad63bc700e7d021039e30ce464eba384c4a1d40f",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/5ed9ad582647bbc3864ef78db34bdc1afdcf9b49",
+                "reference": "5ed9ad582647bbc3864ef78db34bdc1afdcf9b49",
                 "shasum": ""
             },
             "require": {
                 "brick/math": "^0.8.8 || ^0.9 || ^0.10",
                 "ext-json": "*",
                 "php": "^8.0",
-                "ramsey/collection": "^1.0"
+                "ramsey/collection": "^1.2"
             },
             "replace": {
                 "rhumsaa/uuid": "self.version"
@@ -6286,7 +6289,7 @@
             ],
             "support": {
                 "issues": "https://github.com/ramsey/uuid/issues",
-                "source": "https://github.com/ramsey/uuid/tree/4.6.0"
+                "source": "https://github.com/ramsey/uuid/tree/4.7.0"
             },
             "funding": [
                 {
@@ -6298,7 +6301,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-05T23:03:38+00:00"
+            "time": "2022-12-19T22:30:49+00:00"
         },
         {
             "name": "ratchet/rfc6455",
@@ -7745,16 +7748,16 @@
         },
         {
             "name": "symfony/cache",
-            "version": "v6.2.0",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/cache.git",
-                "reference": "64cb231dfb25677097d18503d1ad4d016b19f19c"
+                "reference": "68625530468c5ff4557fc8825dcfa478b94a8309"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/cache/zipball/64cb231dfb25677097d18503d1ad4d016b19f19c",
-                "reference": "64cb231dfb25677097d18503d1ad4d016b19f19c",
+                "url": "https://api.github.com/repos/symfony/cache/zipball/68625530468c5ff4557fc8825dcfa478b94a8309",
+                "reference": "68625530468c5ff4557fc8825dcfa478b94a8309",
                 "shasum": ""
             },
             "require": {
@@ -7821,7 +7824,7 @@
                 "psr6"
             ],
             "support": {
-                "source": "https://github.com/symfony/cache/tree/v6.2.0"
+                "source": "https://github.com/symfony/cache/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -7837,7 +7840,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-24T11:58:37+00:00"
+            "time": "2022-12-16T12:37:34+00:00"
         },
         {
             "name": "symfony/cache-contracts",
@@ -7920,16 +7923,16 @@
         },
         {
             "name": "symfony/console",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "58f6cef5dc5f641b7bbdbf8b32b44cc926c35f3f"
+                "reference": "5a9bd5c543f00157c55face973c149957467db31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/58f6cef5dc5f641b7bbdbf8b32b44cc926c35f3f",
-                "reference": "58f6cef5dc5f641b7bbdbf8b32b44cc926c35f3f",
+                "url": "https://api.github.com/repos/symfony/console/zipball/5a9bd5c543f00157c55face973c149957467db31",
+                "reference": "5a9bd5c543f00157c55face973c149957467db31",
                 "shasum": ""
             },
             "require": {
@@ -7996,7 +7999,7 @@
                 "terminal"
             ],
             "support": {
-                "source": "https://github.com/symfony/console/tree/v6.2.1"
+                "source": "https://github.com/symfony/console/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8012,7 +8015,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-01T13:44:20+00:00"
+            "time": "2022-12-16T15:08:36+00:00"
         },
         {
             "name": "symfony/css-selector",
@@ -8148,16 +8151,16 @@
         },
         {
             "name": "symfony/error-handler",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "b4e41f62c1124378863ff2705158a60da3e4c6b9"
+                "reference": "12a25d01cc5273b2445e125d62b61d34db42297e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/b4e41f62c1124378863ff2705158a60da3e4c6b9",
-                "reference": "b4e41f62c1124378863ff2705158a60da3e4c6b9",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/12a25d01cc5273b2445e125d62b61d34db42297e",
+                "reference": "12a25d01cc5273b2445e125d62b61d34db42297e",
                 "shasum": ""
             },
             "require": {
@@ -8199,7 +8202,7 @@
             "description": "Provides tools to manage errors and ease debugging PHP code",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/error-handler/tree/v6.2.1"
+                "source": "https://github.com/symfony/error-handler/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8215,20 +8218,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-01T21:07:46+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v6.2.0",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "9efb1618fabee89515fe031314e8ed5625f85a53"
+                "reference": "3ffeb31139b49bf6ef0bc09d1db95eac053388d1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9efb1618fabee89515fe031314e8ed5625f85a53",
-                "reference": "9efb1618fabee89515fe031314e8ed5625f85a53",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3ffeb31139b49bf6ef0bc09d1db95eac053388d1",
+                "reference": "3ffeb31139b49bf6ef0bc09d1db95eac053388d1",
                 "shasum": ""
             },
             "require": {
@@ -8282,7 +8285,7 @@
             "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/event-dispatcher/tree/v6.2.0"
+                "source": "https://github.com/symfony/event-dispatcher/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8298,7 +8301,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-02T09:08:04+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/event-dispatcher-contracts",
@@ -8445,16 +8448,16 @@
         },
         {
             "name": "symfony/http-client",
-            "version": "v6.2.0",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-client.git",
-                "reference": "153540b6ed72eecdcb42dc847f8d8cf2e2516e8e"
+                "reference": "7054ad466f836309aef511789b9c697bc986d8ce"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-client/zipball/153540b6ed72eecdcb42dc847f8d8cf2e2516e8e",
-                "reference": "153540b6ed72eecdcb42dc847f8d8cf2e2516e8e",
+                "url": "https://api.github.com/repos/symfony/http-client/zipball/7054ad466f836309aef511789b9c697bc986d8ce",
+                "reference": "7054ad466f836309aef511789b9c697bc986d8ce",
                 "shasum": ""
             },
             "require": {
@@ -8510,7 +8513,7 @@
             "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-client/tree/v6.2.0"
+                "source": "https://github.com/symfony/http-client/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8526,7 +8529,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-14T10:13:36+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
@@ -8611,16 +8614,16 @@
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "d0bbd5a7e81b38f32504399b9199f265505b7bac"
+                "reference": "ddf4dd35de1623e7c02013523e6c2137b67b636f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0bbd5a7e81b38f32504399b9199f265505b7bac",
-                "reference": "d0bbd5a7e81b38f32504399b9199f265505b7bac",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ddf4dd35de1623e7c02013523e6c2137b67b636f",
+                "reference": "ddf4dd35de1623e7c02013523e6c2137b67b636f",
                 "shasum": ""
             },
             "require": {
@@ -8669,7 +8672,7 @@
             "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-foundation/tree/v6.2.1"
+                "source": "https://github.com/symfony/http-foundation/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8685,20 +8688,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-04T18:26:13+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "bcbd2ea12fee651a4c8bff4f6f00cce2ac1f8404"
+                "reference": "860a0189969b755cd571709bd32313aa8599867a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/bcbd2ea12fee651a4c8bff4f6f00cce2ac1f8404",
-                "reference": "bcbd2ea12fee651a4c8bff4f6f00cce2ac1f8404",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/860a0189969b755cd571709bd32313aa8599867a",
+                "reference": "860a0189969b755cd571709bd32313aa8599867a",
                 "shasum": ""
             },
             "require": {
@@ -8780,7 +8783,7 @@
             "description": "Provides a structured process for converting a Request into a Response",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-kernel/tree/v6.2.1"
+                "source": "https://github.com/symfony/http-kernel/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8796,20 +8799,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-06T17:28:26+00:00"
+            "time": "2022-12-16T19:38:34+00:00"
         },
         {
             "name": "symfony/mailer",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mailer.git",
-                "reference": "a18c3dd41cfcf011e3866802e39b9ae9e541deaf"
+                "reference": "b355ad81f1d2987c47dcd3b04d5dce669e1e62e6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mailer/zipball/a18c3dd41cfcf011e3866802e39b9ae9e541deaf",
-                "reference": "a18c3dd41cfcf011e3866802e39b9ae9e541deaf",
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/b355ad81f1d2987c47dcd3b04d5dce669e1e62e6",
+                "reference": "b355ad81f1d2987c47dcd3b04d5dce669e1e62e6",
                 "shasum": ""
             },
             "require": {
@@ -8859,7 +8862,7 @@
             "description": "Helps sending emails",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/mailer/tree/v6.2.1"
+                "source": "https://github.com/symfony/mailer/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -8875,7 +8878,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-06T16:54:23+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/mailgun-mailer",
@@ -8944,16 +8947,16 @@
         },
         {
             "name": "symfony/mime",
-            "version": "v6.2.0",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "1e8005a7cbd79fb824ad81308ef2a76592a08bc0"
+                "reference": "8c98bf40406e791043890a163f6f6599b9cfa1ed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/1e8005a7cbd79fb824ad81308ef2a76592a08bc0",
-                "reference": "1e8005a7cbd79fb824ad81308ef2a76592a08bc0",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/8c98bf40406e791043890a163f6f6599b9cfa1ed",
+                "reference": "8c98bf40406e791043890a163f6f6599b9cfa1ed",
                 "shasum": ""
             },
             "require": {
@@ -9007,7 +9010,7 @@
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v6.2.0"
+                "source": "https://github.com/symfony/mime/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -9023,7 +9026,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-28T12:28:19+00:00"
+            "time": "2022-12-14T16:38:10+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
@@ -10001,16 +10004,16 @@
         },
         {
             "name": "symfony/service-contracts",
-            "version": "v3.1.1",
+            "version": "v3.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/service-contracts.git",
-                "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239"
+                "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239",
-                "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/aac98028c69df04ee77eb69b96b86ee51fbf4b75",
+                "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75",
                 "shasum": ""
             },
             "require": {
@@ -10026,7 +10029,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "3.1-dev"
+                    "dev-main": "3.3-dev"
                 },
                 "thanks": {
                     "name": "symfony/contracts",
@@ -10066,7 +10069,7 @@
                 "standards"
             ],
             "support": {
-                "source": "https://github.com/symfony/service-contracts/tree/v3.1.1"
+                "source": "https://github.com/symfony/service-contracts/tree/v3.2.0"
             },
             "funding": [
                 {
@@ -10082,20 +10085,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-30T19:18:58+00:00"
+            "time": "2022-11-25T10:21:52+00:00"
         },
         {
             "name": "symfony/string",
-            "version": "v6.2.0",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/string.git",
-                "reference": "145702685e0d12f81d755c71127bfff7582fdd36"
+                "reference": "863219fd713fa41cbcd285a79723f94672faff4d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/string/zipball/145702685e0d12f81d755c71127bfff7582fdd36",
-                "reference": "145702685e0d12f81d755c71127bfff7582fdd36",
+                "url": "https://api.github.com/repos/symfony/string/zipball/863219fd713fa41cbcd285a79723f94672faff4d",
+                "reference": "863219fd713fa41cbcd285a79723f94672faff4d",
                 "shasum": ""
             },
             "require": {
@@ -10152,7 +10155,7 @@
                 "utf8"
             ],
             "support": {
-                "source": "https://github.com/symfony/string/tree/v6.2.0"
+                "source": "https://github.com/symfony/string/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -10168,20 +10171,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-30T17:13:47+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v6.2.0",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "c08de62caead8357244efcb809d0b1a2584f2198"
+                "reference": "3294288c335b6267eab14964bf2c46015663d93f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/c08de62caead8357244efcb809d0b1a2584f2198",
-                "reference": "c08de62caead8357244efcb809d0b1a2584f2198",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/3294288c335b6267eab14964bf2c46015663d93f",
+                "reference": "3294288c335b6267eab14964bf2c46015663d93f",
                 "shasum": ""
             },
             "require": {
@@ -10250,7 +10253,7 @@
             "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/translation/tree/v6.2.0"
+                "source": "https://github.com/symfony/translation/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -10266,7 +10269,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-02T09:08:04+00:00"
+            "time": "2022-12-13T18:04:17+00:00"
         },
         {
             "name": "symfony/translation-contracts",
@@ -10425,16 +10428,16 @@
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "1e7544c8698627b908657e5276854d52ab70087a"
+                "reference": "6168f544827e897f708a684f75072a8c33a5e309"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1e7544c8698627b908657e5276854d52ab70087a",
-                "reference": "1e7544c8698627b908657e5276854d52ab70087a",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6168f544827e897f708a684f75072a8c33a5e309",
+                "reference": "6168f544827e897f708a684f75072a8c33a5e309",
                 "shasum": ""
             },
             "require": {
@@ -10493,7 +10496,7 @@
                 "dump"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-dumper/tree/v6.2.1"
+                "source": "https://github.com/symfony/var-dumper/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -10509,20 +10512,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-03T22:32:58+00:00"
+            "time": "2022-12-14T16:11:27+00:00"
         },
         {
             "name": "symfony/var-exporter",
-            "version": "v6.2.1",
+            "version": "v6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-exporter.git",
-                "reference": "8a3f442d48567a5447e984ce9e86875ed768304a"
+                "reference": "ada947160cf9444d17d9ac0b2df46c06941b5526"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-exporter/zipball/8a3f442d48567a5447e984ce9e86875ed768304a",
-                "reference": "8a3f442d48567a5447e984ce9e86875ed768304a",
+                "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ada947160cf9444d17d9ac0b2df46c06941b5526",
+                "reference": "ada947160cf9444d17d9ac0b2df46c06941b5526",
                 "shasum": ""
             },
             "require": {
@@ -10567,7 +10570,7 @@
                 "serialize"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-exporter/tree/v6.2.1"
+                "source": "https://github.com/symfony/var-exporter/tree/v6.2.2"
             },
             "funding": [
                 {
@@ -10583,7 +10586,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-03T22:32:58+00:00"
+            "time": "2022-12-12T08:57:11+00:00"
         },
         {
             "name": "tightenco/collect",
@@ -11074,20 +11077,20 @@
         },
         {
             "name": "fakerphp/faker",
-            "version": "v1.20.0",
+            "version": "v1.21.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FakerPHP/Faker.git",
-                "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b"
+                "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/37f751c67a5372d4e26353bd9384bc03744ec77b",
-                "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b",
+                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/92efad6a967f0b79c499705c69b662f738cc9e4d",
+                "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1 || ^8.0",
+                "php": "^7.4 || ^8.0",
                 "psr/container": "^1.0 || ^2.0",
                 "symfony/deprecation-contracts": "^2.2 || ^3.0"
             },
@@ -11098,7 +11101,8 @@
                 "bamarni/composer-bin-plugin": "^1.4.1",
                 "doctrine/persistence": "^1.3 || ^2.0",
                 "ext-intl": "*",
-                "symfony/phpunit-bridge": "^4.4 || ^5.2"
+                "phpunit/phpunit": "^9.5.26",
+                "symfony/phpunit-bridge": "^5.4.16"
             },
             "suggest": {
                 "doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
@@ -11110,7 +11114,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "v1.20-dev"
+                    "dev-main": "v1.21-dev"
                 }
             },
             "autoload": {
@@ -11135,22 +11139,22 @@
             ],
             "support": {
                 "issues": "https://github.com/FakerPHP/Faker/issues",
-                "source": "https://github.com/FakerPHP/Faker/tree/v1.20.0"
+                "source": "https://github.com/FakerPHP/Faker/tree/v1.21.0"
             },
-            "time": "2022-07-20T13:12:54+00:00"
+            "time": "2022-12-13T13:54:32+00:00"
         },
         {
             "name": "fidry/cpu-core-counter",
-            "version": "0.4.0",
+            "version": "0.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/theofidry/cpu-core-counter.git",
-                "reference": "666cb04a02f2801f3b19955fc23c824f9018bf64"
+                "reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/666cb04a02f2801f3b19955fc23c824f9018bf64",
-                "reference": "666cb04a02f2801f3b19955fc23c824f9018bf64",
+                "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/79261cc280aded96d098e1b0e0ba0c4881b432c2",
+                "reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2",
                 "shasum": ""
             },
             "require": {
@@ -11190,7 +11194,7 @@
             ],
             "support": {
                 "issues": "https://github.com/theofidry/cpu-core-counter/issues",
-                "source": "https://github.com/theofidry/cpu-core-counter/tree/0.4.0"
+                "source": "https://github.com/theofidry/cpu-core-counter/tree/0.4.1"
             },
             "funding": [
                 {
@@ -11198,7 +11202,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2022-12-10T21:26:31+00:00"
+            "time": "2022-12-16T22:01:02+00:00"
         },
         {
             "name": "filp/whoops",
@@ -11383,16 +11387,16 @@
         },
         {
             "name": "laravel/telescope",
-            "version": "v4.10.0",
+            "version": "v4.10.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/telescope.git",
-                "reference": "e4b16dd22db3e8a8b52e3a03343b11ffcee2aaaa"
+                "reference": "fcf4d360c003a1d27e2c1b298691645f8942943b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/telescope/zipball/e4b16dd22db3e8a8b52e3a03343b11ffcee2aaaa",
-                "reference": "e4b16dd22db3e8a8b52e3a03343b11ffcee2aaaa",
+                "url": "https://api.github.com/repos/laravel/telescope/zipball/fcf4d360c003a1d27e2c1b298691645f8942943b",
+                "reference": "fcf4d360c003a1d27e2c1b298691645f8942943b",
                 "shasum": ""
             },
             "require": {
@@ -11445,9 +11449,9 @@
             ],
             "support": {
                 "issues": "https://github.com/laravel/telescope/issues",
-                "source": "https://github.com/laravel/telescope/tree/v4.10.0"
+                "source": "https://github.com/laravel/telescope/tree/v4.10.1"
             },
-            "time": "2022-12-05T15:34:49+00:00"
+            "time": "2022-12-14T15:00:00+00:00"
         },
         {
             "name": "mockery/mockery",
@@ -11781,16 +11785,16 @@
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.19",
+            "version": "9.2.22",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "c77b56b63e3d2031bd8997fcec43c1925ae46559"
+                "reference": "e4bf60d2220b4baaa0572986b5d69870226b06df"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c77b56b63e3d2031bd8997fcec43c1925ae46559",
-                "reference": "c77b56b63e3d2031bd8997fcec43c1925ae46559",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e4bf60d2220b4baaa0572986b5d69870226b06df",
+                "reference": "e4bf60d2220b4baaa0572986b5d69870226b06df",
                 "shasum": ""
             },
             "require": {
@@ -11846,7 +11850,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.19"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.22"
             },
             "funding": [
                 {
@@ -11854,7 +11858,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2022-11-18T07:47:47+00:00"
+            "time": "2022-12-18T16:40:55+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",

+ 4 - 0
config/instance.php

@@ -103,4 +103,8 @@ return [
 	'avatar' => [
 		'local_to_cloud' => env('PF_LOCAL_AVATAR_TO_CLOUD', false)
 	],
+
+	'admin_invites' => [
+		'enabled' => env('PF_ADMIN_INVITES_ENABLED', true)
+	],
 ];

+ 41 - 0
database/migrations/2022_12_13_092726_create_admin_invites_table.php

@@ -0,0 +1,41 @@
+<?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()
+    {
+        Schema::create('admin_invites', function (Blueprint $table) {
+            $table->id();
+            $table->string('name')->nullable();
+            $table->string('invite_code')->unique()->index();
+            $table->text('description')->nullable();
+            $table->text('message')->nullable();
+            $table->unsignedInteger('max_uses')->nullable();
+            $table->unsignedInteger('uses')->nullable();
+            $table->boolean('skip_email_verification')->default(false);
+            $table->timestamp('expires_at')->nullable();
+            $table->json('used_by')->nullable();
+            $table->unsignedInteger('admin_user_id')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('admin_invites');
+    }
+};

+ 31 - 0
database/migrations/2022_12_18_133815_add_default_value_to_admin_invites_table.php

@@ -0,0 +1,31 @@
+<?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()
+    {
+        Schema::table('admin_invites', function (Blueprint $table) {
+            $table->unsignedInteger('uses')->default(0)->after('max_uses')->change();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('admin_invites', function (Blueprint $table) {
+        });
+    }
+};

+ 91 - 67
package-lock.json

@@ -1933,17 +1933,27 @@
 			"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
 		},
 		"node_modules/@types/express": {
-			"version": "4.17.14",
-			"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
-			"integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
+			"version": "4.17.15",
+			"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz",
+			"integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==",
 			"dependencies": {
 				"@types/body-parser": "*",
-				"@types/express-serve-static-core": "^4.17.18",
+				"@types/express-serve-static-core": "^4.17.31",
 				"@types/qs": "*",
 				"@types/serve-static": "*"
 			}
 		},
 		"node_modules/@types/express-serve-static-core": {
+			"version": "4.17.28",
+			"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
+			"integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
+			"dependencies": {
+				"@types/node": "*",
+				"@types/qs": "*",
+				"@types/range-parser": "*"
+			}
+		},
+		"node_modules/@types/express/node_modules/@types/express-serve-static-core": {
 			"version": "4.17.31",
 			"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
 			"integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
@@ -2027,9 +2037,9 @@
 			"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="
 		},
 		"node_modules/@types/node": {
-			"version": "18.11.13",
-			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.13.tgz",
-			"integrity": "sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w=="
+			"version": "18.11.17",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz",
+			"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng=="
 		},
 		"node_modules/@types/parse-json": {
 			"version": "4.0.0",
@@ -3528,14 +3538,14 @@
 			}
 		},
 		"node_modules/css-loader": {
-			"version": "6.7.2",
-			"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz",
-			"integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==",
+			"version": "6.7.3",
+			"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz",
+			"integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==",
 			"dev": true,
 			"peer": true,
 			"dependencies": {
 				"icss-utils": "^5.1.0",
-				"postcss": "^8.4.18",
+				"postcss": "^8.4.19",
 				"postcss-modules-extract-imports": "^3.0.0",
 				"postcss-modules-local-by-default": "^4.0.0",
 				"postcss-modules-scope": "^3.0.0",
@@ -5075,9 +5085,9 @@
 			]
 		},
 		"node_modules/ignore": {
-			"version": "5.2.1",
-			"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
-			"integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
+			"version": "5.2.4",
+			"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+			"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
 			"engines": {
 				"node": ">= 4"
 			}
@@ -5413,9 +5423,9 @@
 			}
 		},
 		"node_modules/jquery": {
-			"version": "3.6.1",
-			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz",
-			"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
+			"version": "3.6.2",
+			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.2.tgz",
+			"integrity": "sha512-/e7ulNIEEYk1Z/l4X0vpxGt+B/dNsV8ghOPAWZaJs8pkGvsSC0tm33aMGylXcj/U7y4IcvwtMXPMyBFZn/gK9A=="
 		},
 		"node_modules/jquery-scroll-lock": {
 			"version": "3.1.3",
@@ -5459,9 +5469,9 @@
 			"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
 		},
 		"node_modules/json5": {
-			"version": "2.2.1",
-			"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
-			"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+			"version": "2.2.2",
+			"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
+			"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
 			"bin": {
 				"json5": "lib/cli.js"
 			},
@@ -6180,9 +6190,9 @@
 			"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
 		},
 		"node_modules/node-releases": {
-			"version": "2.0.6",
-			"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
-			"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
+			"version": "2.0.8",
+			"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
+			"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
 		},
 		"node_modules/normalize-path": {
 			"version": "3.0.0",
@@ -7227,19 +7237,20 @@
 			"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
 		},
 		"node_modules/pusher-js": {
-			"version": "7.5.0",
-			"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.5.0.tgz",
-			"integrity": "sha512-R8eL3v2hnOC7NY8ufvrcDPdEjit//2pqVmcC7h1sUyoZQ4M+bwlwkszmMVuVbYNKZUS8WRFmSvdeb9LkfLyvZQ==",
+			"version": "7.6.0",
+			"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.6.0.tgz",
+			"integrity": "sha512-5CJ7YN5ZdC24E0ETraCU5VYFv0IY5ziXhrS0gS5+9Qrro1E4M1lcZhtr9H1H+6jNSLj1LKKAgcLeE1EH9GxMlw==",
 			"dev": true,
 			"dependencies": {
+				"@types/express-serve-static-core": "4.17.28",
 				"@types/node": "^14.14.31",
 				"tweetnacl": "^1.0.3"
 			}
 		},
 		"node_modules/pusher-js/node_modules/@types/node": {
-			"version": "14.18.34",
-			"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.34.tgz",
-			"integrity": "sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA==",
+			"version": "14.18.35",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.35.tgz",
+			"integrity": "sha512-2ATO8pfhG1kDvw4Lc4C0GXIMSQFFJBCo/R1fSgTwmUlq5oy95LXyjDQinsRVgQY6gp6ghh3H91wk9ES5/5C+Tw==",
 			"dev": true
 		},
 		"node_modules/qs": {
@@ -7659,9 +7670,9 @@
 			"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
 		},
 		"node_modules/sass": {
-			"version": "1.56.2",
-			"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz",
-			"integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==",
+			"version": "1.57.1",
+			"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
+			"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
 			"dev": true,
 			"dependencies": {
 				"chokidar": ">=3.0.0 <4.0.0",
@@ -10710,20 +10721,32 @@
 			"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
 		},
 		"@types/express": {
-			"version": "4.17.14",
-			"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
-			"integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
+			"version": "4.17.15",
+			"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz",
+			"integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==",
 			"requires": {
 				"@types/body-parser": "*",
-				"@types/express-serve-static-core": "^4.17.18",
+				"@types/express-serve-static-core": "^4.17.31",
 				"@types/qs": "*",
 				"@types/serve-static": "*"
+			},
+			"dependencies": {
+				"@types/express-serve-static-core": {
+					"version": "4.17.31",
+					"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
+					"integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
+					"requires": {
+						"@types/node": "*",
+						"@types/qs": "*",
+						"@types/range-parser": "*"
+					}
+				}
 			}
 		},
 		"@types/express-serve-static-core": {
-			"version": "4.17.31",
-			"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
-			"integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
+			"version": "4.17.28",
+			"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
+			"integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
 			"requires": {
 				"@types/node": "*",
 				"@types/qs": "*",
@@ -10804,9 +10827,9 @@
 			"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="
 		},
 		"@types/node": {
-			"version": "18.11.13",
-			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.13.tgz",
-			"integrity": "sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w=="
+			"version": "18.11.17",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz",
+			"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng=="
 		},
 		"@types/parse-json": {
 			"version": "4.0.0",
@@ -12023,14 +12046,14 @@
 			"requires": {}
 		},
 		"css-loader": {
-			"version": "6.7.2",
-			"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz",
-			"integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==",
+			"version": "6.7.3",
+			"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz",
+			"integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==",
 			"dev": true,
 			"peer": true,
 			"requires": {
 				"icss-utils": "^5.1.0",
-				"postcss": "^8.4.18",
+				"postcss": "^8.4.19",
 				"postcss-modules-extract-imports": "^3.0.0",
 				"postcss-modules-local-by-default": "^4.0.0",
 				"postcss-modules-scope": "^3.0.0",
@@ -13182,9 +13205,9 @@
 			"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
 		},
 		"ignore": {
-			"version": "5.2.1",
-			"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
-			"integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA=="
+			"version": "5.2.4",
+			"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+			"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
 		},
 		"imagemin": {
 			"version": "7.0.1",
@@ -13419,9 +13442,9 @@
 			}
 		},
 		"jquery": {
-			"version": "3.6.1",
-			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz",
-			"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
+			"version": "3.6.2",
+			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.2.tgz",
+			"integrity": "sha512-/e7ulNIEEYk1Z/l4X0vpxGt+B/dNsV8ghOPAWZaJs8pkGvsSC0tm33aMGylXcj/U7y4IcvwtMXPMyBFZn/gK9A=="
 		},
 		"jquery-scroll-lock": {
 			"version": "3.1.3",
@@ -13459,9 +13482,9 @@
 			"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
 		},
 		"json5": {
-			"version": "2.2.1",
-			"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
-			"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
+			"version": "2.2.2",
+			"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
+			"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ=="
 		},
 		"jsonfile": {
 			"version": "6.1.0",
@@ -14006,9 +14029,9 @@
 			}
 		},
 		"node-releases": {
-			"version": "2.0.6",
-			"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
-			"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
+			"version": "2.0.8",
+			"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
+			"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
 		},
 		"normalize-path": {
 			"version": "3.0.0",
@@ -14696,19 +14719,20 @@
 			"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
 		},
 		"pusher-js": {
-			"version": "7.5.0",
-			"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.5.0.tgz",
-			"integrity": "sha512-R8eL3v2hnOC7NY8ufvrcDPdEjit//2pqVmcC7h1sUyoZQ4M+bwlwkszmMVuVbYNKZUS8WRFmSvdeb9LkfLyvZQ==",
+			"version": "7.6.0",
+			"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.6.0.tgz",
+			"integrity": "sha512-5CJ7YN5ZdC24E0ETraCU5VYFv0IY5ziXhrS0gS5+9Qrro1E4M1lcZhtr9H1H+6jNSLj1LKKAgcLeE1EH9GxMlw==",
 			"dev": true,
 			"requires": {
+				"@types/express-serve-static-core": "4.17.28",
 				"@types/node": "^14.14.31",
 				"tweetnacl": "^1.0.3"
 			},
 			"dependencies": {
 				"@types/node": {
-					"version": "14.18.34",
-					"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.34.tgz",
-					"integrity": "sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA==",
+					"version": "14.18.35",
+					"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.35.tgz",
+					"integrity": "sha512-2ATO8pfhG1kDvw4Lc4C0GXIMSQFFJBCo/R1fSgTwmUlq5oy95LXyjDQinsRVgQY6gp6ghh3H91wk9ES5/5C+Tw==",
 					"dev": true
 				}
 			}
@@ -15013,9 +15037,9 @@
 			"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
 		},
 		"sass": {
-			"version": "1.56.2",
-			"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz",
-			"integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==",
+			"version": "1.57.1",
+			"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
+			"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
 			"dev": true,
 			"requires": {
 				"chokidar": ">=3.0.0 <4.0.0",

BIN
public/js/admin_invite.js


BIN
public/js/compose-12722-3lkw2.js


BIN
public/js/spa.js


BIN
public/js/vendor.js


BIN
public/mix-manifest.json


+ 488 - 0
resources/assets/components/invite/AdminInvite.vue

@@ -0,0 +1,488 @@
+<template>
+    <div class="admin-invite-component">
+        <div class="admin-invite-component-inner">
+            <div class="card bg-dark">
+                <div v-if="tabIndex === 0" class="card-body d-flex align-items-center justify-content-center">
+                    <div class="text-center">
+                        <b-spinner variant="muted" />
+                        <p class="text-muted mb-0">Loading...</p>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 1" class="card-body">
+                    <div class="d-flex justify-content-center my-3">
+                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
+                    </div>
+                    <div class="d-flex flex-column align-items-center justify-content-center">
+                        <p class="lead mb-1 text-muted">You've been invited to join</p>
+                        <p class="h3 mb-2">{{ instance.uri }}</p>
+                        <p class="mb-0 text-muted">
+                            <span>{{ instance.stats.user_count.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"}) }} users</span>
+                            <span>·</span>
+                            <span>{{ instance.stats.status_count.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"}) }} posts</span>
+                        </p>
+
+                        <div v-if="inviteConfig.message != 'You\'ve been invited to join'">
+                            <div class="admin-message">
+                                <p class="small text-light mb-0">Message from admin(s):</p>
+                                {{ inviteConfig.message }}
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="mt-5">
+                        <div class="form-group">
+                            <label for="username">Username</label>
+                            <input
+                                type="text"
+                                class="form-control form-control-lg"
+                                placeholder="What should everyone call you?"
+                                minlength="2"
+                                maxlength="15"
+                                v-model="form.username" />
+
+                            <p v-if="errors.username" class="form-text text-danger">
+                                <i class="far fa-exclamation-triangle mr-1"></i>
+                                {{ errors.username }}
+                            </p>
+                        </div>
+
+                        <button
+                            class="btn btn-primary btn-block font-weight-bold"
+                            @click="proceed(tabIndex)"
+                            :disabled="isProceeding || !form.username || form.username.length < 2">
+                            <template v-if="isProceeding">
+                                <b-spinner small />
+                            </template>
+                            <template v-else>
+                                Continue
+                            </template>
+                        </button>
+
+                        <p class="login-link">
+                            <a href="/login">Already have an account?</a>
+                        </p>
+
+                        <p class="register-terms">
+                            By registering, you agree to our <a href="/site/terms">Terms of Service</a> and <a href="/site/privacy">Privacy Policy</a>.
+                        </p>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 2" class="card-body">
+                    <div class="d-flex justify-content-center my-3">
+                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
+                    </div>
+                    <div class="d-flex flex-column align-items-center justify-content-center">
+                        <p class="lead mb-1 text-muted">You've been invited to join</p>
+                        <p class="h3 mb-2">{{ instance.uri }}</p>
+                    </div>
+                    <div class="mt-5">
+                        <div class="form-group">
+                            <label for="username">Email Address</label>
+                            <input
+                                type="email"
+                                class="form-control form-control-lg"
+                                placeholder="Your email address"
+                                v-model="form.email" />
+
+                            <p v-if="errors.email" class="form-text text-danger">
+                                <i class="far fa-exclamation-triangle mr-1"></i>
+                                {{ errors.email }}
+                            </p>
+                        </div>
+
+                        <button
+                            class="btn btn-primary btn-block font-weight-bold"
+                            @click="proceed(tabIndex)"
+                            :disabled="isProceeding || !form.email || !validateEmail()">
+                            <template v-if="isProceeding">
+                                <b-spinner small />
+                            </template>
+                            <template v-else>
+                                Continue
+                            </template>
+                        </button>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 3" class="card-body">
+                    <div class="d-flex justify-content-center my-3">
+                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
+                    </div>
+                    <div class="d-flex flex-column align-items-center justify-content-center">
+                        <p class="lead mb-1 text-muted">You've been invited to join</p>
+                        <p class="h3 mb-2">{{ instance.uri }}</p>
+                    </div>
+                    <div class="mt-5">
+                        <div class="form-group">
+                            <label for="username">Password</label>
+                            <input
+                                type="password"
+                                class="form-control form-control-lg"
+                                placeholder="Use a secure password"
+                                minlength="8"
+                                v-model="form.password" />
+
+                            <p v-if="errors.password" class="form-text text-danger">
+                                <i class="far fa-exclamation-triangle mr-1"></i>
+                                {{ errors.password }}
+                            </p>
+                        </div>
+
+                        <button
+                            class="btn btn-primary btn-block font-weight-bold"
+                            @click="proceed(tabIndex)"
+                            :disabled="isProceeding || !form.password || form.password.length < 8">
+                            <template v-if="isProceeding">
+                                <b-spinner small />
+                            </template>
+                            <template v-else>
+                                Continue
+                            </template>
+                        </button>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 4" class="card-body">
+                    <div class="d-flex justify-content-center my-3">
+                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
+                    </div>
+                    <div class="d-flex flex-column align-items-center justify-content-center">
+                        <p class="lead mb-1 text-muted">You've been invited to join</p>
+                        <p class="h3 mb-2">{{ instance.uri }}</p>
+                    </div>
+                    <div class="mt-5">
+                        <div class="form-group">
+                            <label for="username">Confirm Password</label>
+                            <input
+                                type="password"
+                                class="form-control form-control-lg"
+                                placeholder="Use a secure password"
+                                minlength="8"
+                                v-model="form.password_confirm" />
+
+                            <p v-if="errors.password_confirm" class="form-text text-danger">
+                                <i class="far fa-exclamation-triangle mr-1"></i>
+                                {{ errors.password_confirm }}
+                            </p>
+                        </div>
+
+                        <button
+                            class="btn btn-primary btn-block font-weight-bold"
+                            @click="proceed(tabIndex)"
+                            :disabled="isProceeding || !form.password_confirm || form.password !== form.password_confirm">
+                            <template v-if="isProceeding">
+                                <b-spinner small />
+                            </template>
+                            <template v-else>
+                                Continue
+                            </template>
+                        </button>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 5" class="card-body">
+                    <div class="d-flex justify-content-center my-3">
+                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
+                    </div>
+                    <div class="d-flex flex-column align-items-center justify-content-center">
+                        <p class="lead mb-1 text-muted">You've been invited to join</p>
+                        <p class="h3 mb-2">{{ instance.uri }}</p>
+                    </div>
+                    <div class="mt-5">
+                        <div class="form-group">
+                            <label for="username">Display Name</label>
+                            <input
+                                type="text"
+                                class="form-control form-control-lg"
+                                placeholder="Add an optional display name"
+                                minlength="8"
+                                v-model="form.display_name" />
+
+                            <p v-if="errors.display_name" class="form-text text-danger">
+                                <i class="far fa-exclamation-triangle mr-1"></i>
+                                {{ errors.display_name }}
+                            </p>
+                        </div>
+
+                        <button
+                            class="btn btn-primary btn-block font-weight-bold"
+                            @click="proceed(tabIndex)"
+                            :disabled="isProceeding">
+                            <template v-if="isProceeding">
+                                <b-spinner small />
+                            </template>
+                            <template v-else>
+                                Continue
+                            </template>
+                        </button>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 6" class="card-body d-flex flex-column">
+                    <div class="d-flex justify-content-center my-3">
+                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
+                    </div>
+                    <div class="d-flex flex-column align-items-center justify-content-center">
+                        <p class="lead mb-1 text-muted">You've been invited to join</p>
+                        <p class="h3 mb-2">{{ instance.uri }}</p>
+                    </div>
+                    <div class="mt-5 d-flex align-items-center justify-content-center flex-column flex-grow-1">
+                        <b-spinner variant="muted" />
+                        <p class="text-muted">Registering...</p>
+                    </div>
+                </div>
+
+                <div v-else-if="tabIndex === 'invalid-code'" class="card-body d-flex align-items-center justify-content-center">
+                    <div>
+                        <h1 class="text-center">Invalid Invite Code</h1>
+                        <hr>
+                        <p class="text-muted mb-1">The invite code you were provided is not valid, this can happen when:</p>
+                        <ul class="text-muted">
+                            <li>Invite code has typos</li>
+                            <li>Invite code was already used</li>
+                            <li>Invite code has reached max uses</li>
+                            <li>Invite code has expired</li>
+                            <li>You have been rate limited</li>
+                        </ul>
+                        <hr>
+                        <a href="/" class="btn btn-primary btn-block rounded-pill font-weight-bold">Go back home</a>
+                    </div>
+                </div>
+
+                <div v-else class="card-body">
+                    <p>An error occured.</p>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script type="text/javascript">
+    export default {
+        props: ['code'],
+
+        data() {
+            return {
+                instance: {},
+                inviteConfig: {},
+                tabIndex: 0,
+                isProceeding: false,
+                errors: {
+                    username: undefined,
+                    email: undefined,
+                    password: undefined,
+                    password_confirm: undefined
+                },
+
+                form: {
+                    username: undefined,
+                    email: undefined,
+                    password: undefined,
+                    password_confirm: undefined,
+                    display_name: undefined,
+                }
+            }
+        },
+
+        mounted() {
+            this.fetchInstanceData();
+        },
+
+        methods: {
+            fetchInstanceData() {
+                axios.get('/api/v1/instance')
+                .then(res => {
+                    this.instance = res.data;
+                })
+                .then(res => {
+                    this.verifyToken();
+                })
+                .catch(err => {
+                    console.log(err);
+                })
+            },
+
+            verifyToken() {
+                axios.post('/api/v1.1/auth/invite/admin/verify', {
+                    token: this.code,
+                })
+                .then(res => {
+                    this.tabIndex = 1;
+                    this.inviteConfig = res.data;
+                })
+                .catch(err => {
+                    this.tabIndex = 'invalid-code';
+                })
+            },
+
+            checkUsernameAvailability() {
+                axios.post('/api/v1.1/auth/invite/admin/uc', {
+                    token: this.code,
+                    username: this.form.username
+                })
+                .then(res => {
+                    if(res && res.data) {
+                        this.isProceeding = false;
+                        this.tabIndex = 2;
+                    } else {
+                        this.tabIndex = 'invalid-code';
+                        this.isProceeding = false;
+                    }
+                })
+                .catch(err => {
+                    if(err.response.data && err.response.data.username) {
+                        this.errors.username = err.response.data.username[0];
+                        this.isProceeding = false;
+                    } else {
+                        this.tabIndex = 'invalid-code';
+                        this.isProceeding = false;
+                    }
+                })
+            },
+
+            checkEmailAvailability() {
+                axios.post('/api/v1.1/auth/invite/admin/ec', {
+                    token: this.code,
+                    email: this.form.email
+                })
+                .then(res => {
+                    if(res && res.data) {
+                        this.isProceeding = false;
+                        this.tabIndex = 3;
+                    } else {
+                        this.tabIndex = 'invalid-code';
+                        this.isProceeding = false;
+                    }
+                })
+                .catch(err => {
+                    if(err.response.data && err.response.data.email) {
+                        this.errors.email = err.response.data.email[0];
+                        this.isProceeding = false;
+                    } else {
+                        this.tabIndex = 'invalid-code';
+                        this.isProceeding = false;
+                    }
+                })
+            },
+
+            validateEmail() {
+                if(!this.form.email || !this.form.email.length) {
+                    return false;
+                }
+
+                return /^[a-z0-9.]{1,64}@[a-z0-9.]{1,64}$/i.test(this.form.email);
+            },
+
+            handleRegistration() {
+                var $form = $('<form>', {
+                    action: '/api/v1.1/auth/invite/admin/re',
+                    method: 'post'
+                });
+                let fields = {
+                    '_token': document.head.querySelector('meta[name="csrf-token"]').content,
+                    token: this.code,
+                    username: this.form.username,
+                    name: this.form.display_name,
+                    email: this.form.email,
+                    password: this.form.password,
+                    password_confirm: this.form.password_confirm
+                };
+
+                $.each(fields, function(key, val) {
+                     $('<input>').attr({
+                         type: "hidden",
+                         name: key,
+                         value: val
+                     }).appendTo($form);
+                });
+                $form.appendTo('body').submit();
+            },
+
+            proceed(cur) {
+                this.isProceeding = true;
+                event.currentTarget.blur();
+
+                switch(cur) {
+                    case 1:
+                        this.checkUsernameAvailability();
+                    break;
+
+                    case 2:
+                        this.checkEmailAvailability();
+                    break;
+
+                    case 3:
+                        this.isProceeding = false;
+                        this.tabIndex = 4;
+                    break;
+
+                    case 4:
+                        this.isProceeding = false;
+                        this.tabIndex = 5;
+                    break;
+
+                    case 5:
+                        this.tabIndex = 6;
+                        this.handleRegistration();
+                    break;
+                }
+            }
+        }
+    }
+</script>
+
+<style lang="scss">
+    .admin-invite-component {
+        font-family: var(--font-family-sans-serif);
+
+        &-inner {
+            display: flex;
+            width: 100wv;
+            height: 100vh;
+            justify-content: center;
+            align-items: center;
+
+            .card {
+                width: 100%;
+                color: #fff;
+                padding: 1.25rem 2.5rem;
+                border-radius: 10px;
+                min-height: 530px;
+
+                @media(min-width: 768px) {
+                    width: 30%;
+                }
+
+                label {
+                    color: var(--muted);
+                    font-weight: bold;
+                    text-transform: uppercase;
+                }
+
+                .login-link {
+                    margin-top: 10px;
+                    font-weight: 600;
+                }
+
+                .register-terms {
+                    font-size: 12px;
+                    color: var(--muted);
+                }
+
+                .form-control {
+                    color: #fff;
+                }
+
+                .admin-message {
+                    margin-top: 20px;
+                    border: 1px solid var(--dropdown-item-hover-color);
+                    color: var(--text-lighter);
+                    padding: 1rem;
+                    border-radius: 5px;
+                }
+            }
+        }
+    }
+</style>

+ 4 - 0
resources/assets/js/admin_invite.js

@@ -0,0 +1,4 @@
+Vue.component(
+    'admin-invite',
+    require('./../components/invite/AdminInvite.vue').default
+);

+ 21 - 0
resources/views/invite/admin_invite.blade.php

@@ -0,0 +1,21 @@
+@extends('layouts.blank')
+
+@section('content')
+<admin-invite code="{{$code}}" />
+@endsection
+
+@push('scripts')
+<script type="text/javascript" src="{{ mix('js/admin_invite.js') }}"></script>
+<script type="text/javascript">App.boot();</script>
+@endpush
+
+@push('styles')
+<link href="{{ mix('css/spa.css') }}" rel="stylesheet" data-stylesheet="light">
+<style type="text/css">
+    body {
+        background: #4776E6;
+        background: -webkit-linear-gradient(to right, #8E54E9, #4776E6);
+        background: linear-gradient(to right, #8E54E9, #4776E6);
+    }
+</style>
+@endpush

+ 4 - 0
routes/api.php

@@ -155,6 +155,10 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
 			Route::post('iar', 'Api\ApiV1Dot1Controller@inAppRegistration');
 			Route::post('iarc', 'Api\ApiV1Dot1Controller@inAppRegistrationConfirm');
 			Route::get('iarer', 'Api\ApiV1Dot1Controller@inAppRegistrationEmailRedirect');
+
+			Route::post('invite/admin/verify', 'AdminInviteController@apiVerifyCheck')->middleware('throttle:20,120');
+			Route::post('invite/admin/uc', 'AdminInviteController@apiUsernameCheck')->middleware('throttle:20,120');
+			Route::post('invite/admin/ec', 'AdminInviteController@apiEmailCheck')->middleware('throttle:10,1440');
 		});
 	});
 

+ 3 - 0
routes/web.php

@@ -587,6 +587,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 		Route::get('privacy', 'MobileController@privacy');
 	});
 
+	Route::get('auth/invite/a/{code}', 'AdminInviteController@index');
+	Route::post('api/v1.1/auth/invite/admin/re', 'AdminInviteController@apiRegister')->middleware('throttle:5,1440');
+
 	Route::get('stories/{username}', 'ProfileController@stories');
 	Route::get('p/{id}', 'StatusController@shortcodeRedirect');
 	Route::get('c/{collection}', 'CollectionController@show');