From 8dac2caf1dc59c4cbd652da5c9c1ab8c39a2b6ed Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 04:00:31 -0700 Subject: [PATCH] Add Curated Onboarding --- .../Admin/AdminSettingsController.php | 487 +++++++++--------- app/Http/Controllers/Api/ApiV1Controller.php | 4 +- app/Http/Controllers/Api/ApiV2Controller.php | 1 + .../Controllers/Auth/RegisterController.php | 8 +- .../Controllers/CuratedRegisterController.php | 398 ++++++++++++++ ...rdingNotifyAdminNewApplicationPipeline.php | 65 +++ app/Mail/CuratedRegisterAcceptUser.php | 55 ++ app/Mail/CuratedRegisterConfirmEmail.php | 55 ++ app/Mail/CuratedRegisterNotifyAdmin.php | 55 ++ ...CuratedRegisterNotifyAdminUserResponse.php | 55 ++ app/Mail/CuratedRegisterRejectUser.php | 55 ++ .../CuratedRegisterRequestDetailsFromUser.php | 58 +++ app/Mail/CuratedRegisterSendMessage.php | 55 ++ app/Models/CuratedRegister.php | 49 ++ app/Models/CuratedRegisterActivity.php | 38 ++ app/Services/ConfigCacheService.php | 2 + app/Services/LandingService.php | 4 +- config/instance.php | 31 ++ ..._073327_create_curated_registers_table.php | 44 ++ ...eate_curated_register_activities_table.php | 42 ++ .../admin/curated-register/index.blade.php | 86 ++++ .../partials/activity-log.blade.php | 463 +++++++++++++++++ .../curated-register/partials/nav.blade.php | 39 ++ .../partials/not-enabled.blade.php | 24 + .../admin/curated-register/show.blade.php | 102 ++++ .../views/admin/partial/sidenav.blade.php | 26 +- resources/views/admin/settings/home.blade.php | 23 +- .../auth/curated-register/concierge.blade.php | 124 +++++ .../curated-register/concierge_form.blade.php | 129 +++++ .../curated-register/confirm_email.blade.php | 91 ++++ .../email_confirmed.blade.php | 74 +++ .../auth/curated-register/index.blade.php | 88 ++++ .../partials/message-email-confirm.blade.php | 23 + .../partials/progress-bar.blade.php | 134 +++++ .../partials/server-rules.blade.php | 18 + .../partials/step-1.blade.php | 175 +++++++ .../partials/step-2.blade.php | 117 +++++ .../partials/step-3.blade.php | 30 ++ .../partials/step-4.blade.php | 2 + .../resend-confirmation.blade.php | 112 ++++ .../resent-confirmation.blade.php | 87 ++++ .../user_response_sent.blade.php | 79 +++ resources/views/auth/login.blade.php | 2 +- .../curated-register/admin_notify.blade.php | 34 ++ .../admin_notify_user_response.blade.php | 21 + .../curated-register/confirm_email.blade.php | 21 + .../message-from-admin.blade.php | 21 + .../request-accepted.blade.php | 37 ++ .../request-details-from-user.blade.php | 22 + .../request-rejected.blade.php | 19 + .../settings/privacy/domain-blocks.blade.php | 4 +- .../site/help/curated-onboarding.blade.php | 162 ++++++ .../views/site/help/partial/sidebar.blade.php | 7 + routes/web-admin.php | 20 +- routes/web.php | 24 +- 55 files changed, 3732 insertions(+), 269 deletions(-) create mode 100644 app/Http/Controllers/CuratedRegisterController.php create mode 100644 app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php create mode 100644 app/Mail/CuratedRegisterAcceptUser.php create mode 100644 app/Mail/CuratedRegisterConfirmEmail.php create mode 100644 app/Mail/CuratedRegisterNotifyAdmin.php create mode 100644 app/Mail/CuratedRegisterNotifyAdminUserResponse.php create mode 100644 app/Mail/CuratedRegisterRejectUser.php create mode 100644 app/Mail/CuratedRegisterRequestDetailsFromUser.php create mode 100644 app/Mail/CuratedRegisterSendMessage.php create mode 100644 app/Models/CuratedRegister.php create mode 100644 app/Models/CuratedRegisterActivity.php create mode 100644 database/migrations/2024_01_16_073327_create_curated_registers_table.php create mode 100644 database/migrations/2024_01_20_091352_create_curated_register_activities_table.php create mode 100644 resources/views/admin/curated-register/index.blade.php create mode 100644 resources/views/admin/curated-register/partials/activity-log.blade.php create mode 100644 resources/views/admin/curated-register/partials/nav.blade.php create mode 100644 resources/views/admin/curated-register/partials/not-enabled.blade.php create mode 100644 resources/views/admin/curated-register/show.blade.php create mode 100644 resources/views/auth/curated-register/concierge.blade.php create mode 100644 resources/views/auth/curated-register/concierge_form.blade.php create mode 100644 resources/views/auth/curated-register/confirm_email.blade.php create mode 100644 resources/views/auth/curated-register/email_confirmed.blade.php create mode 100644 resources/views/auth/curated-register/index.blade.php create mode 100644 resources/views/auth/curated-register/partials/message-email-confirm.blade.php create mode 100644 resources/views/auth/curated-register/partials/progress-bar.blade.php create mode 100644 resources/views/auth/curated-register/partials/server-rules.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-1.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-2.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-3.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-4.blade.php create mode 100644 resources/views/auth/curated-register/resend-confirmation.blade.php create mode 100644 resources/views/auth/curated-register/resent-confirmation.blade.php create mode 100644 resources/views/auth/curated-register/user_response_sent.blade.php create mode 100644 resources/views/emails/curated-register/admin_notify.blade.php create mode 100644 resources/views/emails/curated-register/admin_notify_user_response.blade.php create mode 100644 resources/views/emails/curated-register/confirm_email.blade.php create mode 100644 resources/views/emails/curated-register/message-from-admin.blade.php create mode 100644 resources/views/emails/curated-register/request-accepted.blade.php create mode 100644 resources/views/emails/curated-register/request-details-from-user.blade.php create mode 100644 resources/views/emails/curated-register/request-rejected.blade.php create mode 100644 resources/views/site/help/curated-onboarding.blade.php diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index 9d9c3dfb6..8b5a26796 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -17,269 +17,288 @@ use Illuminate\Support\Str; trait AdminSettingsController { - public function settings(Request $request) - { - $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); - $cloud_disk = config('filesystems.cloud'); - $cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret')); - $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); - $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; - $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); - $png = in_array('image/png', $types); - $gif = in_array('image/gif', $types); - $mp4 = in_array('video/mp4', $types); - $webp = in_array('image/webp', $types); + public function settings(Request $request) + { + $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret')); + $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); + $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; + $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); + $png = in_array('image/png', $types); + $gif = in_array('image/gif', $types); + $mp4 = in_array('video/mp4', $types); + $webp = in_array('image/webp', $types); - $availableAdmins = User::whereIsAdmin(true)->get(); - $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $availableAdmins = User::whereIsAdmin(true)->get(); + $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $openReg = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed'); - // $system = [ - // 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')), - // 'max_upload_size' => ini_get('post_max_size'), - // 'image_driver' => config('image.driver'), - // 'image_driver_loaded' => extension_loaded(config('image.driver')) - // ]; + return view('admin.settings.home', compact( + 'jpeg', + 'png', + 'gif', + 'mp4', + 'webp', + 'rules', + 'cloud_storage', + 'cloud_disk', + 'cloud_ready', + 'availableAdmins', + 'currentAdmin', + 'regState' + )); + } - return view('admin.settings.home', compact( - 'jpeg', - 'png', - 'gif', - 'mp4', - 'webp', - 'rules', - 'cloud_storage', - 'cloud_disk', - 'cloud_ready', - 'availableAdmins', - 'currentAdmin' - // 'system' - )); - } + public function settingsHomeStore(Request $request) + { + $this->validate($request, [ + 'name' => 'nullable|string', + 'short_description' => 'nullable', + 'long_description' => 'nullable', + 'max_photo_size' => 'nullable|integer|min:1', + 'max_album_length' => 'nullable|integer|min:1|max:100', + 'image_quality' => 'nullable|integer|min:1|max:100', + 'type_jpeg' => 'nullable', + 'type_png' => 'nullable', + 'type_gif' => 'nullable', + 'type_mp4' => 'nullable', + 'type_webp' => 'nullable', + 'admin_account_id' => 'nullable', + 'regs' => 'required|in:open,filtered,closed' + ]); - public function settingsHomeStore(Request $request) - { - $this->validate($request, [ - 'name' => 'nullable|string', - 'short_description' => 'nullable', - 'long_description' => 'nullable', - 'max_photo_size' => 'nullable|integer|min:1', - 'max_album_length' => 'nullable|integer|min:1|max:100', - 'image_quality' => 'nullable|integer|min:1|max:100', - 'type_jpeg' => 'nullable', - 'type_png' => 'nullable', - 'type_gif' => 'nullable', - 'type_mp4' => 'nullable', - 'type_webp' => 'nullable', - 'admin_account_id' => 'nullable', - ]); + $orb = false; + $cob = false; + switch($request->input('regs')) { + case 'open': + $orb = true; + $cob = false; + break; - if($request->filled('admin_account_id')) { - ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); - Cache::forget('api:v1:instance-data:contact'); - Cache::forget('api:v1:instance-data-response-v1'); - } - if($request->filled('rule_delete')) { - $index = (int) $request->input('rule_delete'); - $rules = ConfigCacheService::get('app.rules'); - $json = json_decode($rules, true); - if(!$rules || empty($json)) { - return; - } - unset($json[$index]); - $json = json_encode(array_values($json)); - ConfigCacheService::put('app.rules', $json); - Cache::forget('api:v1:instance-data:rules'); - Cache::forget('api:v1:instance-data-response-v1'); - return 200; - } + case 'filtered': + $orb = false; + $cob = true; + break; - $media_types = explode(',', config_cache('pixelfed.media_types')); - $media_types_original = $media_types; + case 'closed': + $orb = false; + $cob = false; + break; + } - $mimes = [ - 'type_jpeg' => 'image/jpeg', - 'type_png' => 'image/png', - 'type_gif' => 'image/gif', - 'type_mp4' => 'video/mp4', - 'type_webp' => 'image/webp', - ]; + ConfigCacheService::put('pixelfed.open_registration', (bool) $orb); + ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob); - foreach ($mimes as $key => $value) { - if($request->input($key) == 'on') { - if(!in_array($value, $media_types)) { - array_push($media_types, $value); - } - } else { - $media_types = array_diff($media_types, [$value]); - } - } + if($request->filled('admin_account_id')) { + ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); + Cache::forget('api:v1:instance-data:contact'); + Cache::forget('api:v1:instance-data-response-v1'); + } + if($request->filled('rule_delete')) { + $index = (int) $request->input('rule_delete'); + $rules = ConfigCacheService::get('app.rules'); + $json = json_decode($rules, true); + if(!$rules || empty($json)) { + return; + } + unset($json[$index]); + $json = json_encode(array_values($json)); + ConfigCacheService::put('app.rules', $json); + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + return 200; + } - if($media_types !== $media_types_original) { - ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); - } + $media_types = explode(',', config_cache('pixelfed.media_types')); + $media_types_original = $media_types; - $keys = [ - 'name' => 'app.name', - 'short_description' => 'app.short_description', - 'long_description' => 'app.description', - 'max_photo_size' => 'pixelfed.max_photo_size', - 'max_album_length' => 'pixelfed.max_album_length', - 'image_quality' => 'pixelfed.image_quality', - 'account_limit' => 'pixelfed.max_account_size', - 'custom_css' => 'uikit.custom.css', - 'custom_js' => 'uikit.custom.js', - 'about_title' => 'about.title' - ]; + $mimes = [ + 'type_jpeg' => 'image/jpeg', + 'type_png' => 'image/png', + 'type_gif' => 'image/gif', + 'type_mp4' => 'video/mp4', + 'type_webp' => 'image/webp', + ]; - foreach ($keys as $key => $value) { - $cc = ConfigCache::whereK($value)->first(); - $val = $request->input($key); - if($cc && $cc->v != $val) { - ConfigCacheService::put($value, $val); - } else if(!empty($val)) { - ConfigCacheService::put($value, $val); - } - } + foreach ($mimes as $key => $value) { + if($request->input($key) == 'on') { + if(!in_array($value, $media_types)) { + array_push($media_types, $value); + } + } else { + $media_types = array_diff($media_types, [$value]); + } + } - $bools = [ - 'activitypub' => 'federation.activitypub.enabled', - 'open_registration' => 'pixelfed.open_registration', - 'mobile_apis' => 'pixelfed.oauth_enabled', - 'stories' => 'instance.stories.enabled', - 'ig_import' => 'pixelfed.import.instagram.enabled', - 'spam_detection' => 'pixelfed.bouncer.enabled', - 'require_email_verification' => 'pixelfed.enforce_email_verification', - 'enforce_account_limit' => 'pixelfed.enforce_account_limit', - 'show_custom_css' => 'uikit.show_custom.css', - 'show_custom_js' => 'uikit.show_custom.js', - 'cloud_storage' => 'pixelfed.cloud_storage', - 'account_autofollow' => 'account.autofollow', - 'show_directory' => 'instance.landing.show_directory', - 'show_explore_feed' => 'instance.landing.show_explore', - ]; + if($media_types !== $media_types_original) { + ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); + } - foreach ($bools as $key => $value) { - $active = $request->input($key) == 'on'; + $keys = [ + 'name' => 'app.name', + 'short_description' => 'app.short_description', + 'long_description' => 'app.description', + 'max_photo_size' => 'pixelfed.max_photo_size', + 'max_album_length' => 'pixelfed.max_album_length', + 'image_quality' => 'pixelfed.image_quality', + 'account_limit' => 'pixelfed.max_account_size', + 'custom_css' => 'uikit.custom.css', + 'custom_js' => 'uikit.custom.js', + 'about_title' => 'about.title' + ]; - if($key == 'activitypub' && $active && !InstanceActor::exists()) { - Artisan::call('instance:actor'); - } + foreach ($keys as $key => $value) { + $cc = ConfigCache::whereK($value)->first(); + $val = $request->input($key); + if($cc && $cc->v != $val) { + ConfigCacheService::put($value, $val); + } else if(!empty($val)) { + ConfigCacheService::put($value, $val); + } + } - if( $key == 'mobile_apis' && - $active && - !file_exists(storage_path('oauth-public.key')) && - !file_exists(storage_path('oauth-private.key')) - ) { - Artisan::call('passport:keys'); - Artisan::call('route:cache'); - } + $bools = [ + 'activitypub' => 'federation.activitypub.enabled', + // 'open_registration' => 'pixelfed.open_registration', + 'mobile_apis' => 'pixelfed.oauth_enabled', + 'stories' => 'instance.stories.enabled', + 'ig_import' => 'pixelfed.import.instagram.enabled', + 'spam_detection' => 'pixelfed.bouncer.enabled', + 'require_email_verification' => 'pixelfed.enforce_email_verification', + 'enforce_account_limit' => 'pixelfed.enforce_account_limit', + 'show_custom_css' => 'uikit.show_custom.css', + 'show_custom_js' => 'uikit.show_custom.js', + 'cloud_storage' => 'pixelfed.cloud_storage', + 'account_autofollow' => 'account.autofollow', + 'show_directory' => 'instance.landing.show_directory', + 'show_explore_feed' => 'instance.landing.show_explore', + ]; - if(config_cache($value) !== $active) { - ConfigCacheService::put($value, (bool) $active); - } - } + foreach ($bools as $key => $value) { + $active = $request->input($key) == 'on'; - if($request->filled('new_rule')) { - $rules = ConfigCacheService::get('app.rules'); - $val = $request->input('new_rule'); - if(!$rules) { - ConfigCacheService::put('app.rules', json_encode([$val])); - } else { - $json = json_decode($rules, true); - $json[] = $val; - ConfigCacheService::put('app.rules', json_encode(array_values($json))); - } - Cache::forget('api:v1:instance-data:rules'); - Cache::forget('api:v1:instance-data-response-v1'); - } + if($key == 'activitypub' && $active && !InstanceActor::exists()) { + Artisan::call('instance:actor'); + } - if($request->filled('account_autofollow_usernames')) { - $usernames = explode(',', $request->input('account_autofollow_usernames')); - $names = []; + if( $key == 'mobile_apis' && + $active && + !file_exists(storage_path('oauth-public.key')) && + !file_exists(storage_path('oauth-private.key')) + ) { + Artisan::call('passport:keys'); + Artisan::call('route:cache'); + } - foreach($usernames as $n) { - $p = Profile::whereUsername($n)->first(); - if(!$p) { - continue; - } - array_push($names, $p->username); - } + if(config_cache($value) !== $active) { + ConfigCacheService::put($value, (bool) $active); + } + } - ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); - } + if($request->filled('new_rule')) { + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('new_rule'); + if(!$rules) { + ConfigCacheService::put('app.rules', json_encode([$val])); + } else { + $json = json_decode($rules, true); + $json[] = $val; + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + } - Cache::forget(Config::CACHE_KEY); + if($request->filled('account_autofollow_usernames')) { + $usernames = explode(',', $request->input('account_autofollow_usernames')); + $names = []; - return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); - } + foreach($usernames as $n) { + $p = Profile::whereUsername($n)->first(); + if(!$p) { + continue; + } + array_push($names, $p->username); + } - public function settingsBackups(Request $request) - { - $path = storage_path('app/'.config('app.name')); - $files = is_dir($path) ? new \DirectoryIterator($path) : []; - return view('admin.settings.backups', compact('files')); - } + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + } - public function settingsMaintenance(Request $request) - { - return view('admin.settings.maintenance'); - } + Cache::forget(Config::CACHE_KEY); - public function settingsStorage(Request $request) - { - $storage = []; - return view('admin.settings.storage', compact('storage')); - } + return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); + } - public function settingsFeatures(Request $request) - { - return view('admin.settings.features'); - } + public function settingsBackups(Request $request) + { + $path = storage_path('app/'.config('app.name')); + $files = is_dir($path) ? new \DirectoryIterator($path) : []; + return view('admin.settings.backups', compact('files')); + } - public function settingsPages(Request $request) - { - $pages = Page::orderByDesc('updated_at')->paginate(10); - return view('admin.pages.home', compact('pages')); - } + public function settingsMaintenance(Request $request) + { + return view('admin.settings.maintenance'); + } - public function settingsPageEdit(Request $request) - { - return view('admin.pages.edit'); - } + public function settingsStorage(Request $request) + { + $storage = []; + return view('admin.settings.storage', compact('storage')); + } - public function settingsSystem(Request $request) - { - $sys = [ - 'pixelfed' => config('pixelfed.version'), - 'php' => phpversion(), - 'laravel' => app()->version(), - ]; - switch (config('database.default')) { - case 'pgsql': - $exp = DB::raw('select version();'); - $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); - $sys['database'] = [ - 'name' => 'Postgres', - 'version' => explode(' ', DB::select($expQuery)[0]->version)[1] - ]; - break; + public function settingsFeatures(Request $request) + { + return view('admin.settings.features'); + } - case 'mysql': - $exp = DB::raw('select version()'); - $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); - $sys['database'] = [ - 'name' => 'MySQL', - 'version' => DB::select($expQuery)[0]->{'version()'} - ]; - break; + public function settingsPages(Request $request) + { + $pages = Page::orderByDesc('updated_at')->paginate(10); + return view('admin.pages.home', compact('pages')); + } - default: - $sys['database'] = [ - 'name' => 'Unknown', - 'version' => '?' - ]; - break; - } - return view('admin.settings.system', compact('sys')); - } + public function settingsPageEdit(Request $request) + { + return view('admin.pages.edit'); + } + + public function settingsSystem(Request $request) + { + $sys = [ + 'pixelfed' => config('pixelfed.version'), + 'php' => phpversion(), + 'laravel' => app()->version(), + ]; + switch (config('database.default')) { + case 'pgsql': + $exp = DB::raw('select version();'); + $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); + $sys['database'] = [ + 'name' => 'Postgres', + 'version' => explode(' ', DB::select($expQuery)[0]->version)[1] + ]; + break; + + case 'mysql': + $exp = DB::raw('select version()'); + $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); + $sys['database'] = [ + 'name' => 'MySQL', + 'version' => DB::select($expQuery)[0]->{'version()'} + ]; + break; + + default: + $sys['database'] = [ + 'name' => 'Unknown', + 'version' => '?' + ]; + break; + } + return view('admin.settings.system', compact('sys')); + } } diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 55ecb25e2..0205648e8 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1652,13 +1652,13 @@ class ApiV1Controller extends Controller 'email' => config('instance.email'), 'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')', 'urls' => [ - 'streaming_api' => 'wss://' . config('pixelfed.domain.app') + 'streaming_api' => null, ], 'stats' => $stats, 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), 'languages' => [config('app.locale')], 'registrations' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, // (bool) config_cache('instance.curated_registration.enabled'), + 'approval_required' => (bool) config_cache('instance.curated_registration.enabled'), 'contact_account' => $contact, 'rules' => $rules, 'configuration' => [ diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 23a122397..c585b3b04 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -141,6 +141,7 @@ class ApiV2Controller extends Controller }); $res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration'); + $res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled'); return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8c10e5d0c..8bdd57bf8 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -174,7 +174,7 @@ class RegisterController extends Controller */ public function showRegistrationForm() { - if(config_cache('pixelfed.open_registration')) { + if((bool) config_cache('pixelfed.open_registration')) { if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp(request()->ip()), 404); } @@ -191,7 +191,11 @@ class RegisterController extends Controller return view('auth.register'); } } else { - abort(404); + if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) { + return redirect('/auth/sign_up'); + } else { + abort(404); + } } } diff --git a/app/Http/Controllers/CuratedRegisterController.php b/app/Http/Controllers/CuratedRegisterController.php new file mode 100644 index 000000000..73bd17bff --- /dev/null +++ b/app/Http/Controllers/CuratedRegisterController.php @@ -0,0 +1,398 @@ +user(), 404); + return view('auth.curated-register.index', ['step' => 1]); + } + + public function concierge(Request $request) + { + abort_if($request->user(), 404); + $emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') && + $request->has('next') && + $request->session()->has('cur-reg-con.cr-id'); + return view('auth.curated-register.concierge', compact('emailConfirmed')); + } + + public function conciergeResponseSent(Request $request) + { + return view('auth.curated-register.user_response_sent'); + } + + public function conciergeFormShow(Request $request) + { + abort_if($request->user(), 404); + abort_unless( + $request->session()->has('cur-reg-con.email-confirmed') && + $request->session()->has('cur-reg-con.cr-id') && + $request->session()->has('cur-reg-con.ac-id'), 404); + $crid = $request->session()->get('cur-reg-con.cr-id'); + $arid = $request->session()->get('cur-reg-con.ac-id'); + $showCaptcha = config('instance.curated_registration.captcha_enabled'); + if($attempts = $request->session()->get('cur-reg-con-attempt')) { + $showCaptcha = $attempts && $attempts >= 2; + } else { + $showCaptcha = false; + } + $activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid); + return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha')); + } + + public function conciergeFormStore(Request $request) + { + abort_if($request->user(), 404); + $request->session()->increment('cur-reg-con-attempt'); + abort_unless( + $request->session()->has('cur-reg-con.email-confirmed') && + $request->session()->has('cur-reg-con.cr-id') && + $request->session()->has('cur-reg-con.ac-id'), 404); + $attempts = $request->session()->get('cur-reg-con-attempt'); + $messages = []; + $rules = [ + 'response' => 'required|string|min:5|max:1000', + 'crid' => 'required|integer|min:1', + 'acid' => 'required|integer|min:1' + ]; + if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + $crid = $request->session()->get('cur-reg-con.cr-id'); + $acid = $request->session()->get('cur-reg-con.ac-id'); + abort_if((string) $crid !== $request->input('crid'), 404); + abort_if((string) $acid !== $request->input('acid'), 404); + + if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) { + return redirect()->back()->withErrors(['code' => 'You already replied to this request.']); + } + + $act = CuratedRegisterActivity::create([ + 'register_id' => $crid, + 'reply_to_id' => $acid, + 'type' => 'user_response', + 'message' => $request->input('response'), + 'from_user' => true, + 'action_required' => true, + ]); + + $request->session()->pull('cur-reg-con'); + $request->session()->pull('cur-reg-con-attempt'); + + return view('auth.curated-register.user_response_sent'); + } + + public function conciergeStore(Request $request) + { + abort_if($request->user(), 404); + $rules = [ + 'sid' => 'required_if:action,email|integer|min:1|max:20000000', + 'id' => 'required_if:action,email|integer|min:1|max:20000000', + 'code' => 'required_if:action,email', + 'action' => 'required|string|in:email,message', + 'email' => 'required_if:action,email|email', + 'response' => 'required_if:action,message|string|min:20|max:1000', + ]; + $messages = []; + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + + $action = $request->input('action'); + $sid = $request->input('sid'); + $id = $request->input('id'); + $code = $request->input('code'); + $email = $request->input('email'); + + $cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid); + $ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id); + + if(!hash_equals($ac->secret_code, $code)) { + return redirect()->back()->withErrors(['code' => 'Invalid code']); + } + + if(!hash_equals($cr->email, $email)) { + return redirect()->back()->withErrors(['email' => 'Invalid email']); + } + + $request->session()->put('cur-reg-con.email-confirmed', true); + $request->session()->put('cur-reg-con.cr-id', $cr->id); + $request->session()->put('cur-reg-con.ac-id', $ac->id); + $emailConfirmed = true; + return redirect('/auth/sign_up/concierge/form'); + } + + public function confirmEmail(Request $request) + { + if($request->user()) { + return redirect(route('help.email-confirmation-issues')); + } + return view('auth.curated-register.confirm_email'); + } + + public function emailConfirmed(Request $request) + { + if($request->user()) { + return redirect(route('help.email-confirmation-issues')); + } + return view('auth.curated-register.email_confirmed'); + } + + public function resendConfirmation(Request $request) + { + return view('auth.curated-register.resend-confirmation'); + } + + public function resendConfirmationProcess(Request $request) + { + $rules = [ + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'exists:curated_registers', + ] + ]; + + $messages = []; + + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + + $this->validate($request, $rules, $messages); + + $cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first(); + if(!$cur) { + return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']); + } + + $totalCount = CuratedRegisterActivity::whereRegisterId($cur->id) + ->whereType('user_resend_email_confirmation') + ->count(); + + if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) { + return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please contact the admin team.']); + } + + $count = CuratedRegisterActivity::whereRegisterId($cur->id) + ->whereType('user_resend_email_confirmation') + ->where('created_at', '>', now()->subHours(12)) + ->count(); + + if($count) { + return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']); + } + + CuratedRegisterActivity::create([ + 'register_id' => $cur->id, + 'type' => 'user_resend_email_confirmation', + 'admin_only_view' => true, + 'from_admin' => false, + 'from_user' => false, + 'action_required' => false, + ]); + + Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur)); + return view('auth.curated-register.resent-confirmation'); + return $request->all(); + } + + public function confirmEmailHandle(Request $request) + { + $rules = [ + 'sid' => 'required', + 'code' => 'required' + ]; + $messages = []; + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + + $cr = CuratedRegister::whereNull('email_verified_at') + ->where('created_at', '>', now()->subHours(24)) + ->find($request->input('sid')); + if(!$cr) { + return redirect(route('help.email-confirmation-issues')); + } + if(!hash_equals($cr->verify_code, $request->input('code'))) { + return redirect(route('help.email-confirmation-issues')); + } + $cr->email_verified_at = now(); + $cr->save(); + + if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) { + CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr); + } + return view('auth.curated-register.email_confirmed'); + } + + public function proceed(Request $request) + { + $this->validate($request, [ + 'step' => 'required|integer|in:1,2,3,4' + ]); + $step = $request->input('step'); + + switch($step) { + case 1: + $step = 2; + $request->session()->put('cur-step', 1); + return view('auth.curated-register.index', compact('step')); + break; + + case 2: + $this->stepTwo($request); + $step = 3; + $request->session()->put('cur-step', 2); + return view('auth.curated-register.index', compact('step')); + break; + + case 3: + $this->stepThree($request); + $step = 3; + $request->session()->put('cur-step', 3); + $verifiedEmail = true; + $request->session()->pull('cur-reg'); + return view('auth.curated-register.index', compact('step', 'verifiedEmail')); + break; + } + } + + protected function stepTwo($request) + { + if($request->filled('reason')) { + $request->session()->put('cur-reg.form-reason', $request->input('reason')); + } + if($request->filled('username')) { + $request->session()->put('cur-reg.form-username', $request->input('username')); + } + if($request->filled('email')) { + $request->session()->put('cur-reg.form-email', $request->input('email')); + } + $this->validate($request, [ + 'username' => [ + 'required', + 'min:2', + 'max:15', + 'unique:curated_registers', + '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.'); + } + }, + ], + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'max:255', + 'unique:users', + 'unique:curated_registers', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if($banned) { + return $fail('Email is invalid.'); + } + }, + ], + 'password' => 'required|min:8', + 'password_confirmation' => 'required|same:password', + 'reason' => 'required|min:20|max:1000', + 'agree' => 'required|accepted' + ]); + $request->session()->put('cur-reg.form-email', $request->input('email')); + $request->session()->put('cur-reg.form-password', $request->input('password')); + } + + protected function stepThree($request) + { + $this->validate($request, [ + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'max:255', + 'unique:users', + 'unique:curated_registers', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if($banned) { + return $fail('Email is invalid.'); + } + }, + ] + ]); + $cr = new CuratedRegister; + $cr->email = $request->email; + $cr->username = $request->session()->get('cur-reg.form-username'); + $cr->password = bcrypt($request->session()->get('cur-reg.form-password')); + $cr->ip_address = $request->ip(); + $cr->reason_to_join = $request->session()->get('cur-reg.form-reason'); + $cr->verify_code = Str::random(40); + $cr->save(); + + Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr)); + } +} diff --git a/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php b/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php new file mode 100644 index 000000000..a1b5f279a --- /dev/null +++ b/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php @@ -0,0 +1,65 @@ +cr = $cr; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) { + return; + } + + config('instance.curated_registration.notify.admin.on_verify_email.bundle') ? + $this->handleBundled() : + $this->handleUnbundled(); + } + + protected function handleBundled() + { + $cr = $this->cr; + Storage::append('conanap.json', json_encode([ + 'id' => $cr->id, + 'email' => $cr->email, + 'created_at' => $cr->created_at, + 'updated_at' => $cr->updated_at, + ])); + } + + protected function handleUnbundled() + { + $cr = $this->cr; + if($aid = config_cache('instance.admin.pid')) { + $admin = User::whereProfileId($aid)->first(); + if($admin && $admin->email) { + Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr)); + } + } + } +} diff --git a/app/Mail/CuratedRegisterAcceptUser.php b/app/Mail/CuratedRegisterAcceptUser.php new file mode 100644 index 000000000..a87278ace --- /dev/null +++ b/app/Mail/CuratedRegisterAcceptUser.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-accepted', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterConfirmEmail.php b/app/Mail/CuratedRegisterConfirmEmail.php new file mode 100644 index 000000000..bf06c4311 --- /dev/null +++ b/app/Mail/CuratedRegisterConfirmEmail.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Welcome to Pixelfed! Please Confirm Your Email', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.confirm_email', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterNotifyAdmin.php b/app/Mail/CuratedRegisterNotifyAdmin.php new file mode 100644 index 000000000..28bdad971 --- /dev/null +++ b/app/Mail/CuratedRegisterNotifyAdmin.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[Requires Action]: New Curated Onboarding Application', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.admin_notify', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterNotifyAdminUserResponse.php b/app/Mail/CuratedRegisterNotifyAdminUserResponse.php new file mode 100644 index 000000000..bc54d5c3e --- /dev/null +++ b/app/Mail/CuratedRegisterNotifyAdminUserResponse.php @@ -0,0 +1,55 @@ +activity = $activity; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Curated Register Notify Admin User Response', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.admin_notify_user_response', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterRejectUser.php b/app/Mail/CuratedRegisterRejectUser.php new file mode 100644 index 000000000..448ea8462 --- /dev/null +++ b/app/Mail/CuratedRegisterRejectUser.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-rejected', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterRequestDetailsFromUser.php b/app/Mail/CuratedRegisterRequestDetailsFromUser.php new file mode 100644 index 000000000..b0ff1bb4d --- /dev/null +++ b/app/Mail/CuratedRegisterRequestDetailsFromUser.php @@ -0,0 +1,58 @@ +verify = $verify; + $this->activity = $activity; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[Action Needed]: Additional information requested', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-details-from-user', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterSendMessage.php b/app/Mail/CuratedRegisterSendMessage.php new file mode 100644 index 000000000..20ffc2749 --- /dev/null +++ b/app/Mail/CuratedRegisterSendMessage.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.message-from-admin', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/CuratedRegister.php b/app/Models/CuratedRegister.php new file mode 100644 index 000000000..edb4e1b22 --- /dev/null +++ b/app/Models/CuratedRegister.php @@ -0,0 +1,49 @@ + 'array', + 'admin_notes' => 'array', + 'email_verified_at' => 'datetime', + 'admin_notified_at' => 'datetime', + 'action_taken_at' => 'datetime', + ]; + + public function adminStatusLabel() + { + if(!$this->email_verified_at) { + return 'Unverified email'; + } + if($this->is_accepted) { return 'Approved'; } + if($this->is_rejected) { return 'Rejected'; } + if($this->is_awaiting_more_info ) { + return 'Awaiting Details'; + } + if($this->is_closed ) { return 'Closed'; } + + return 'Open'; + } + + public function emailConfirmUrl() + { + return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code); + } + + public function emailReplyUrl() + { + return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8)); + } + + public function adminReviewUrl() + { + return url('/i/admin/curated-onboarding/show/' . $this->id); + } +} diff --git a/app/Models/CuratedRegisterActivity.php b/app/Models/CuratedRegisterActivity.php new file mode 100644 index 000000000..5b5071e01 --- /dev/null +++ b/app/Models/CuratedRegisterActivity.php @@ -0,0 +1,38 @@ + 'array', + 'admin_notified_at' => 'datetime', + 'action_taken_at' => 'datetime', + ]; + + public function application() + { + return $this->belongsTo(CuratedRegister::class, 'register_id'); + } + + public function emailReplyUrl() + { + return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code); + } + + public function adminReviewUrl() + { + $url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id; + if($this->reply_to_id) { + $url .= '&rtid=' . $this->reply_to_id; + } + return url($url); + } +} diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index 9da5c8adc..65d9a8337 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -72,6 +72,8 @@ class ConfigCacheService 'instance.banner.blurhash', 'autospam.nlp.enabled', + + 'instance.curated_registration.enabled', // 'system.user_mode' ]; diff --git a/app/Services/LandingService.php b/app/Services/LandingService.php index ba16af5b6..6b1aa62d4 100644 --- a/app/Services/LandingService.php +++ b/app/Services/LandingService.php @@ -48,13 +48,15 @@ class LandingService ->toArray() : []; }); + $openReg = (bool) config_cache('pixelfed.open_registration') || (bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg'); + $res = [ 'name' => config_cache('app.name'), 'url' => config_cache('app.url'), 'domain' => config('pixelfed.domain.app'), 'show_directory' => config_cache('instance.landing.show_directory'), 'show_explore_feed' => config_cache('instance.landing.show_explore'), - 'open_registration' => config_cache('pixelfed.open_registration') == 1, + 'open_registration' => (bool) $openReg, 'version' => config('pixelfed.version'), 'about' => [ 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), diff --git a/config/instance.php b/config/instance.php index d1566da4a..cfc0468ab 100644 --- a/config/instance.php +++ b/config/instance.php @@ -145,4 +145,35 @@ return [ 'software-update' => [ 'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false) ], + + 'notifications' => [ + 'gc' => [ + 'enabled' => env('INSTANCE_NOTIFY_AUTO_GC', false), + 'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365) + ] + ], + + 'curated_registration' => [ + 'enabled' => env('INSTANCE_CUR_REG', false), + + 'resend_confirmation_limit' => env('INSTANCE_CUR_REG_RESEND_LIMIT', 5), + + 'captcha_enabled' => env('INSTANCE_CUR_REG_CAPTCHA', env('CAPTCHA_ENABLED', false)), + + 'state' => [ + 'fallback_on_closed_reg' => true, + 'only_enabled_on_closed_reg' => env('INSTANCE_CUR_REG_STATE_ONLY_ON_CLOSED', true), + ], + + 'notify' => [ + 'admin' => [ + 'on_verify_email' => [ + 'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false), + 'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false), + 'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10), + ], + 'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false), + ] + ], + ], ]; diff --git a/database/migrations/2024_01_16_073327_create_curated_registers_table.php b/database/migrations/2024_01_16_073327_create_curated_registers_table.php new file mode 100644 index 000000000..8f665cdc3 --- /dev/null +++ b/database/migrations/2024_01_16_073327_create_curated_registers_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('email')->unique()->nullable()->index(); + $table->string('username')->unique()->nullable()->index(); + $table->string('password')->nullable(); + $table->string('ip_address')->nullable(); + $table->string('verify_code')->nullable(); + $table->text('reason_to_join')->nullable(); + $table->unsignedBigInteger('invited_by')->nullable()->index(); + $table->boolean('is_approved')->default(0)->index(); + $table->boolean('is_rejected')->default(0)->index(); + $table->boolean('is_awaiting_more_info')->default(0)->index(); + $table->boolean('is_closed')->default(0)->index(); + $table->json('autofollow_account_ids')->nullable(); + $table->json('admin_notes')->nullable(); + $table->unsignedInteger('approved_by_admin_id')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->timestamp('admin_notified_at')->nullable(); + $table->timestamp('action_taken_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_registers'); + } +}; diff --git a/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php b/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php new file mode 100644 index 000000000..410804750 --- /dev/null +++ b/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php @@ -0,0 +1,42 @@ +id(); + $table->unsignedInteger('register_id')->nullable()->index(); + $table->unsignedInteger('admin_id')->nullable(); + $table->unsignedInteger('reply_to_id')->nullable()->index(); + $table->string('secret_code')->nullable(); + $table->string('type')->nullable()->index(); + $table->string('title')->nullable(); + $table->string('link')->nullable(); + $table->text('message')->nullable(); + $table->json('metadata')->nullable(); + $table->boolean('from_admin')->default(false)->index(); + $table->boolean('from_user')->default(false)->index(); + $table->boolean('admin_only_view')->default(true); + $table->boolean('action_required')->default(false); + $table->timestamp('admin_notified_at')->nullable(); + $table->timestamp('action_taken_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_register_activities'); + } +}; diff --git a/resources/views/admin/curated-register/index.blade.php b/resources/views/admin/curated-register/index.blade.php new file mode 100644 index 000000000..1094e9113 --- /dev/null +++ b/resources/views/admin/curated-register/index.blade.php @@ -0,0 +1,86 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + + @if($records && $records->count()) +
+ + + + + + @if(in_array($filter, ['all', 'open'])) + + @endif + + + + + + + @foreach($records as $record) + + + + @if(in_array($filter, ['all', 'open'])) + + @endif + + + + + @endforeach + +
IDUsernameStatusReason for JoiningEmailCreated
+ + #{{ $record->id }} + + +

+ @{{ $record->username }} +

+
+ {!! $record->adminStatusLabel() !!} + + {{ str_limit($record->reason_to_join, 100) }} + +

+ {{ str_limit(\Illuminate\Support\Str::mask($record->email, '*', 5, 10), 10) }} +

+
{{ $record->created_at->diffForHumans() }}
+ +
+ {{ $records->links() }} +
+
+ @else +
+
+

+

No {{ request()->filled('filter') ? request()->filter : 'open' }} applications found!

+
+
+ @endif +
+
+@else +@include('admin.curated-register.partials.not-enabled') +@endif +@endsection diff --git a/resources/views/admin/curated-register/partials/activity-log.blade.php b/resources/views/admin/curated-register/partials/activity-log.blade.php new file mode 100644 index 000000000..1d7701f44 --- /dev/null +++ b/resources/views/admin/curated-register/partials/activity-log.blade.php @@ -0,0 +1,463 @@ + + + +@push('scripts') + +@endpush + +@push('styles') + +@endpush diff --git a/resources/views/admin/curated-register/partials/nav.blade.php b/resources/views/admin/curated-register/partials/nav.blade.php new file mode 100644 index 000000000..16241997e --- /dev/null +++ b/resources/views/admin/curated-register/partials/nav.blade.php @@ -0,0 +1,39 @@ +@if(request()->filled('a')) +@if(request()->input('a') === 'rj') +
+

Successfully rejected application!

+
+@endif +@if(request()->input('a') === 'aj') +
+

Successfully accepted application!

+
+@endif +@endif + + + + +@push('scripts') + +@endpush diff --git a/resources/views/admin/curated-register/partials/not-enabled.blade.php b/resources/views/admin/curated-register/partials/not-enabled.blade.php new file mode 100644 index 000000000..9248c3ea2 --- /dev/null +++ b/resources/views/admin/curated-register/partials/not-enabled.blade.php @@ -0,0 +1,24 @@ +
+
+
+
+
+
+

Feature not enabled

+ +

To enable this feature: + +

    +
  1. Go to the Settings page
  2. +
  3. + Under Registration Status select: +
    Filtered - Anyone can apply (Curated Onboarding)
    +
  4. +
  5. Save the changes
  6. +
+
+
+
+
+
+
diff --git a/resources/views/admin/curated-register/show.blade.php b/resources/views/admin/curated-register/show.blade.php new file mode 100644 index 000000000..8da14df0e --- /dev/null +++ b/resources/views/admin/curated-register/show.blade.php @@ -0,0 +1,102 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

+ + Back to Curated Onboarding + +

+

Application #{{ $record->id }}

+
+ @if($record->is_closed) + @else + + + Open / Awaiting Admin Action + + @endif +
+
+
+
+
+
+ +
+
+
+
+
+
Details
+
+
+
Username
+
{{ $record->username }}
+
+
+
Email
+
{{ $record->email }}
+
+
+
Created
+
{{ $record->created_at->diffForHumans() }}
+
+ @if($record->email_verified_at) +
+
Email Verified
+
{{ $record->email_verified_at->diffForHumans() }}
+
+ @else +
+
Email Verified
+
Not yet
+
+ @endif +
+
+ +
+
Reason for Joining
+
+
{{ $record->reason_to_join }}
+
+
+
+ +
+ @include('admin.curated-register.partials.activity-log', [ + 'id' => $record->id, + 'is_closed' => $record->is_closed, + 'is_approved' => $record->is_approved, + 'is_rejected' => $record->is_rejected, + 'action_taken_at' => $record->action_taken_at, + 'email_verified_at' => $record->email_verified_at + ]) +
+
+
+
+@endsection + +@push('styles') + +@endpush diff --git a/resources/views/admin/partial/sidenav.blade.php b/resources/views/admin/partial/sidenav.blade.php index bfd93e77e..4226c3488 100644 --- a/resources/views/admin/partial/sidenav.blade.php +++ b/resources/views/admin/partial/sidenav.blade.php @@ -18,7 +18,7 @@ @@ -56,6 +56,15 @@ Settings + + @if((bool) config_cache('instance.curated_registration.enabled')) + + @endif
@@ -64,7 +73,7 @@ @@ -96,10 +105,17 @@ + {{-- --}} + @@ -118,14 +134,14 @@ diff --git a/resources/views/admin/settings/home.blade.php b/resources/views/admin/settings/home.blade.php index 7635ac61f..6201872c8 100644 --- a/resources/views/admin/settings/home.blade.php +++ b/resources/views/admin/settings/home.blade.php @@ -77,6 +77,18 @@
+ +
+ +
+ +
+
+ @if($cloud_ready)
@@ -91,11 +103,18 @@

ActivityPub federation, compatible with Pixelfed, Mastodon and other projects.

-
+ {{--
-

Allow new user registrations.

+

Allow new user registrations.

--}} + + + {{--
+ + +
+

Manually review new account registration applications.

--}}
diff --git a/resources/views/auth/curated-register/concierge.blade.php b/resources/views/auth/curated-register/concierge.blade.php new file mode 100644 index 000000000..3d8f300b7 --- /dev/null +++ b/resources/views/auth/curated-register/concierge.blade.php @@ -0,0 +1,124 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + + @if($emailConfirmed) +

Information Requested

+

Our admin team requests the following information from you:

+
+

testing

+
+
+ +
+ @csrf + + +
+ + +
+ 0/1000 +
+
+ + +
+
+

For additional information, please see our Curated Onboarding Help Center page.

+ @else + @include('auth.curated-register.partials.message-email-confirm', ['step' => 4]) + @endif +
+
+
+@endsection + +@push('scripts') + +@endpush + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/concierge_form.blade.php b/resources/views/auth/curated-register/concierge_form.blade.php new file mode 100644 index 000000000..b8a1f922c --- /dev/null +++ b/resources/views/auth/curated-register/concierge_form.blade.php @@ -0,0 +1,129 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + +

Information Requested

+

Before we can process your application to join, our admin team have requested additional information from you. Please respond at your earliest convenience!

+
+

From our Admins:

+
+

{{ $activity->message }}

+
+

If you don't understand this request, or need additional context you should request clarification from the admin team.

+ {{--
--}} + +
+ @csrf + + +
+ + +
+ 0/1000 +
+
+ @if($showCaptcha) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+
+

For additional information, please see our Curated Onboarding Help Center page.

+
+
+
+@endsection + +@push('scripts') + +@endpush + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/confirm_email.blade.php b/resources/views/auth/curated-register/confirm_email.blade.php new file mode 100644 index 000000000..786023a92 --- /dev/null +++ b/resources/views/auth/curated-register/confirm_email.blade.php @@ -0,0 +1,91 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 3]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + +

Confirm Email

+

Please confirm your email address so we can continue processing your registration application.

+ +
+ @csrf + input('sid')}}> + input('code')}}> + @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/email_confirmed.blade.php b/resources/views/auth/curated-register/email_confirmed.blade.php new file mode 100644 index 000000000..8d4f8dc13 --- /dev/null +++ b/resources/views/auth/curated-register/email_confirmed.blade.php @@ -0,0 +1,74 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + +

+

Email Confirmed!

+

Our admin team will review your application.

+
+

Most applications are processed within 24-48 hours. We will send you an email once your account is ready!

+

If we need any additional information, we will send you an automated request with a link that you can visit and provide further details to help process your application.

+
+

For additional information, please see our Curated Onboarding Help Center page.

+
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/index.blade.php b/resources/views/auth/curated-register/index.blade.php new file mode 100644 index 000000000..934c15ec7 --- /dev/null +++ b/resources/views/auth/curated-register/index.blade.php @@ -0,0 +1,88 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => $step ?? 1]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + @if($step === 1) + @include('auth.curated-register.partials.step-1') + @elseif ($step === 2) + @include('auth.curated-register.partials.step-2') + @elseif ($step === 3) + @include('auth.curated-register.partials.step-3') + @elseif ($step === 4) + @include('auth.curated-register.partials.step-4') + @endif +
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/partials/message-email-confirm.blade.php b/resources/views/auth/curated-register/partials/message-email-confirm.blade.php new file mode 100644 index 000000000..3f0711b63 --- /dev/null +++ b/resources/views/auth/curated-register/partials/message-email-confirm.blade.php @@ -0,0 +1,23 @@ +

Please verify your email address

+
+ @csrf + +
+ +
+ @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+
+

For additional information, please see our Curated Onboarding Help Center page.

diff --git a/resources/views/auth/curated-register/partials/progress-bar.blade.php b/resources/views/auth/curated-register/partials/progress-bar.blade.php new file mode 100644 index 000000000..778ab1a9c --- /dev/null +++ b/resources/views/auth/curated-register/partials/progress-bar.blade.php @@ -0,0 +1,134 @@ +
+
    +
  • + 1 + Review Rules +
  • +
  • + 2 + Your Details +
  • +
  • + 3 + Confirm Email +
  • +
  • + 4 + Await Review +
  • +
+
+ +@push('styles') + +@endpush diff --git a/resources/views/auth/curated-register/partials/server-rules.blade.php b/resources/views/auth/curated-register/partials/server-rules.blade.php new file mode 100644 index 000000000..8264697f6 --- /dev/null +++ b/resources/views/auth/curated-register/partials/server-rules.blade.php @@ -0,0 +1,18 @@ +@php +$rules = json_decode(config_cache('app.rules'), true) +@endphp + +
+ @foreach($rules as $id => $rule) +
+
+
+ {{ $id + 1 }} +
+
+
+

{{ $rule }}

+
+
+ @endforeach +
diff --git a/resources/views/auth/curated-register/partials/step-1.blade.php b/resources/views/auth/curated-register/partials/step-1.blade.php new file mode 100644 index 000000000..c51ca3fef --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-1.blade.php @@ -0,0 +1,175 @@ +@php +$id = str_random(14); +@endphp +

Before you continue.

+@if(config_cache('app.rules') && strlen(config_cache('app.rules')) > 5) +

Let's go over a few basic guidelines established by the server's administrators.

+ +@include('auth.curated-register.partials.server-rules') +@else +

The admins have not specified any community rules, however we suggest youreview the Terms of Use and Privacy Policy.

+@endif + +
+
+ @csrf + + +
+ + Go back +
+ + + +@push('scripts') + +@endpush diff --git a/resources/views/auth/curated-register/partials/step-2.blade.php b/resources/views/auth/curated-register/partials/step-2.blade.php new file mode 100644 index 000000000..1d508a4c3 --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-2.blade.php @@ -0,0 +1,117 @@ +

Let's begin setting up your account on {{ config('pixelfed.domain.app') }}

+
+ @csrf + +
+
+ +
+ +
+ @{{ config('pixelfed.domain.app') }} +
+
+

You can use letters, numbers, and underscores with a max length of 15 chars.

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+

+ Our moderators manually review sign-ups. To assist in the processing of your registration, please provide some information about yourself and explain why you wish to create an account on {{ config('pixelfed.domain.app') }}. +

+
+
+ + +
+ 0/1000 +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+ +@push('scripts') + +@endpush + +@push('styles') + +@endpush diff --git a/resources/views/auth/curated-register/partials/step-3.blade.php b/resources/views/auth/curated-register/partials/step-3.blade.php new file mode 100644 index 000000000..5cb0541ac --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-3.blade.php @@ -0,0 +1,30 @@ +

Confirm Your Email

+@if(isset($verifiedEmail)) +
+

+

Please check your email inbox, we sent an email confirmation with a link that you need to visit.

+
+@else +

Please confirm your email address is correct, we will send a verification e-mail with a special verification link that you need to visit before proceeding.

+
+ @csrf + +
+ +
+ @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+@endif diff --git a/resources/views/auth/curated-register/partials/step-4.blade.php b/resources/views/auth/curated-register/partials/step-4.blade.php new file mode 100644 index 000000000..d2a1eafd1 --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-4.blade.php @@ -0,0 +1,2 @@ +

Processing your membership request

+

We will send you an email once your account is ready!

diff --git a/resources/views/auth/curated-register/resend-confirmation.blade.php b/resources/views/auth/curated-register/resend-confirmation.blade.php new file mode 100644 index 000000000..822146b1a --- /dev/null +++ b/resources/views/auth/curated-register/resend-confirmation.blade.php @@ -0,0 +1,112 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 3]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{!! $error !!}

+
+ @endforeach +
+ @endif + +

Resend Confirmation

+

Please confirm your email address so we verify your registration application to re-send your email verification email.

+ +
+ @csrf +
+ +
+ @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+ +
+
+ +
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/resent-confirmation.blade.php b/resources/views/auth/curated-register/resent-confirmation.blade.php new file mode 100644 index 000000000..2ad27bb2d --- /dev/null +++ b/resources/views/auth/curated-register/resent-confirmation.blade.php @@ -0,0 +1,87 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 3]) + +
+

+

Please check your email inbox

+
+

We sent a confirmation link to your email that you need to verify before we can process your registration application.

+

The verification link expires after 24 hours.

+
+
+
+ +
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/user_response_sent.blade.php b/resources/views/auth/curated-register/user_response_sent.blade.php new file mode 100644 index 000000000..17e57eaf1 --- /dev/null +++ b/resources/views/auth/curated-register/user_response_sent.blade.php @@ -0,0 +1,79 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + +

+

Succesfully Sent Response!

+

Our admin team will review your application.

+
+

Most applications are processed within 24-48 hours. We will send you an email once your account is ready!

+

If we need any additional information, we will send you an automated request with a link that you can visit and provide further details to help process your application.

+
+

For additional information, please see our Curated Onboarding Help Center page.

+
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index dadd08a4f..9df9ea8c9 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -111,7 +111,7 @@ @endif - @if(config_cache('pixelfed.open_registration')) + @if((bool) config_cache('pixelfed.open_registration') || (bool) config_cache('instance.curated_registration.enabled'))

diff --git a/resources/views/emails/curated-register/admin_notify.blade.php b/resources/views/emails/curated-register/admin_notify.blade.php new file mode 100644 index 000000000..72e7e082b --- /dev/null +++ b/resources/views/emails/curated-register/admin_notify.blade.php @@ -0,0 +1,34 @@ +@component('mail::message') +# [#{{$verify->id}}] New Curated Onboarding Application + +Hello admin, + +**Please review this new onboarding application.** + + +

+ +Username: {{ $verify->username }} + +
+ +Email: {{ $verify->email }} + +

+ +
+ +*The user provided the following reason to join:* +

{!!str_limit(nl2br($verify->reason_to_join), 300)!!}

+ + + +Review Onboarding Application + + +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/admin_notify_user_response.blade.php b/resources/views/emails/curated-register/admin_notify_user_response.blade.php new file mode 100644 index 000000000..504795f83 --- /dev/null +++ b/resources/views/emails/curated-register/admin_notify_user_response.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +# New Curated Onboarding Response ({{ '#' . $activity->id}}) + +Hello, + +You have a new response from a curated onboarding application from **{{$activity->application->email}}**. + + +

{!! $activity->message !!}

+
+ + +Review Onboarding Response + + +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/confirm_email.blade.php b/resources/views/emails/curated-register/confirm_email.blade.php new file mode 100644 index 000000000..bcadc3832 --- /dev/null +++ b/resources/views/emails/curated-register/confirm_email.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +# Action Needed: Confirm Your Email to Activate Your Pixelfed Account + +Hello **{{'@'.$verify->username}}**, + +Please confirm your email address so we can process your new registration application. + + +Confirm Email Address + + + +

If you did not create this account, please disregard this email. This link expires after 24 hours.

+
+ +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/message-from-admin.blade.php b/resources/views/emails/curated-register/message-from-admin.blade.php new file mode 100644 index 000000000..55f4ceff9 --- /dev/null +++ b/resources/views/emails/curated-register/message-from-admin.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +# New Message from {{config('pixelfed.domain.app')}} + +Hello, + +You recently applied to join our Pixelfed community using the @**{{ $verify->username }}** username. + +The admins have a message for you: + + +

{{ $verify->message }}

+
+ +Please do not respond to this email, any replies will not be seen by our admin team. + +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message on behalf of our admin team, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/request-accepted.blade.php b/resources/views/emails/curated-register/request-accepted.blade.php new file mode 100644 index 000000000..79badefe5 --- /dev/null +++ b/resources/views/emails/curated-register/request-accepted.blade.php @@ -0,0 +1,37 @@ +@component('mail::message') +Hello **{{'@'.$verify->username}}**, + + +We are excited to inform you that your account has been successfully activated! + +Your journey into the world of visual storytelling begins now, and we can’t wait to see the incredible content you’ll create and share. + + +Sign-in to your account + + +Here’s what you can do next: + + +**Personalize Your Profile**: Customize your profile to reflect your personality or brand. + +**Start Sharing**: Post your first photo or album and share your unique perspective with the world. + +**Engage with the Community**: Follow other users, like and comment on posts, and become an active member of our vibrant community. + +**Explore**: Discover amazing content from a diverse range of users and hashtags. + + +Need help getting started? Visit our [Help Center]({{url('site/help')}}) for tips, tutorials, and FAQs. Remember, our community thrives on respect and creativity, so please familiarize yourself with our [Community Guidelines]({{url('site/kb/community-guidelines')}}). + +If you have any questions or need assistance, feel free to reach out to [our support team]({{url('/site/contact')}}). + +Happy posting, and once again, welcome to Pixelfed! + +Warm regards,
+{{ config('pixelfed.domain.app') }} + +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/request-details-from-user.blade.php b/resources/views/emails/curated-register/request-details-from-user.blade.php new file mode 100644 index 000000000..59c26c7cd --- /dev/null +++ b/resources/views/emails/curated-register/request-details-from-user.blade.php @@ -0,0 +1,22 @@ +@component('mail::message') +# Action Needed: Additional information requested + +Hello **{{'@'.$verify->username}}** + +To help us process your registration application, we require more information. + +Our onboarding team have requested the following details: + +@component('mail::panel') +

{!! $activity->message !!}

+@endcomponent + +Reply with your response + + +

Please respond promptly, your application will be automatically removed 7 days after your last interaction.

+
+ +Thanks,
+{{ config('pixelfed.domain.app') }} +@endcomponent diff --git a/resources/views/emails/curated-register/request-rejected.blade.php b/resources/views/emails/curated-register/request-rejected.blade.php new file mode 100644 index 000000000..6d7a475dc --- /dev/null +++ b/resources/views/emails/curated-register/request-rejected.blade.php @@ -0,0 +1,19 @@ +@component('mail::message') +Hello **{{'@'.$verify->username}}**, + +We appreciate the time you took to apply for an account on {{ config('pixelfed.domain.app') }}. + +Unfortunately, after reviewing your [application]({{route('help.curated-onboarding')}}), we have decided not to proceed with the activation of your account. + +This decision is made to ensure the best experience for all members of our community. We encourage you to review our [guidelines]({{route('help.community-guidelines')}}) and consider applying again in the future. + +We appreciate your understanding. If you believe this decision was made in error, or if you have any questions, please don’t hesitate to [contact us]({{route('site.contact')}}). + +
+ +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/settings/privacy/domain-blocks.blade.php b/resources/views/settings/privacy/domain-blocks.blade.php index d93e0b58e..1602527ec 100644 --- a/resources/views/settings/privacy/domain-blocks.blade.php +++ b/resources/views/settings/privacy/domain-blocks.blade.php @@ -208,7 +208,9 @@ swal.stopLoading() swal.close() this.index = 0 - this.blocks.unshift(parsedUrl.hostname) + if(this.blocks.indexOf(parsedUrl.hostname) === -1) { + this.blocks.unshift(parsedUrl.hostname) + } this.buildList() }) .catch(err => { diff --git a/resources/views/site/help/curated-onboarding.blade.php b/resources/views/site/help/curated-onboarding.blade.php new file mode 100644 index 000000000..6a4d980f3 --- /dev/null +++ b/resources/views/site/help/curated-onboarding.blade.php @@ -0,0 +1,162 @@ +@extends('site.help.partial.template', ['breadcrumb'=>'Curated Onboarding']) + +@section('section') +
+

Curated Onboarding

+
+
+@if((bool) config_cache('instance.curated_registration.enabled') == false) +
+
+ @if((bool) config_cache('pixelfed.open_registration')) +

Curated Onboarding is not available on this server, however anyone can join.

+
+

Create New Account

+ @else +

Curated Onboarding is not available on this server.

+ @endif +
+
+@endif +

Curated Onboarding is our innovative approach to ensure each new member is a perfect fit for our community.

+

This process goes beyond the usual sign-up routine. It's a thoughtful method to understand each applicant's intentions and aspirations within our platform.

+

If you're excited to be a part of a platform that values individuality, creativity, and community, we invite you to apply to join our community. Share with us your story, and let's embark on this visual journey together!

+@if((bool) config_cache('instance.curated_registration.enabled') && !request()->user()) +

+ Apply to Join +

+@endif +
+
How does Curated Onboarding work?
+
    +
  1. +

    Application Submission

    +

    Start your journey by providing your username and email, along with a personal note about why you're excited to join Pixelfed. This insight into your interests and aspirations helps us get to know you better.

    +
  2. +
    +
  3. +

    Admin Review and Interaction

    +

    Our team carefully reviews each application, assessing your fit within our community. If we're intrigued but need more information, we'll reach out directly. You'll receive an email with a link to a special form where you can view our request and respond in detail. This two-way communication ensures a thorough and fair evaluation process.

    +
  4. +
    +
  5. +

    Decision – Acceptance or Rejection

    +

    Each application is thoughtfully considered. If you're a match for our community, you'll be greeted with a warm welcome email and instructions to activate your account. If your application is not accepted, we will inform you respectfully, leaving the possibility open for future applications.

    +
  6. +
+
+
Why Curated Onboarding?
+
    +
  • +

    Fostering Quality Connections

    +

    At Pixelfed, we believe in the power of meaningful connections. It's not just about how many people are in the community, but how they enrich and enliven our platform. Our curated onboarding process is designed to welcome members who share our enthusiasm for creativity and engagement, ensuring every interaction on Pixelfed is enjoyable and rewarding.

    +
  • +
    +
  • +

    Ensuring Safety and Respect

    +

    A careful onboarding process is critical for maintaining a safe and respectful environment. It allows Pixelfed to align every new member with the platform's values of kindness and inclusivity.

    +
  • +
    +
  • +

    Encouraging Community Engagement

    +

    By engaging with applicants from the start, Pixelfed fosters a community that's not just active but passionate. This approach welcomes users who are genuinely interested in making a positive contribution to Pixelfed's vibrant community.

    +
  • +
+
+
FAQs & Troubleshooting
+

+ +

+
+ This indicates that you've attempted to verify your email address too many times. This most likely is the result of an issue delivering the verification emails to your email provider. If you are experiencing this issue, we suggest that you contact the admin onboarding team and mention that you're having issues verifying your email address. +
+
+

+

+ +

+
+ This indicates the desired username is already in-use or was previously used by a now deleted account. You need to pick a different username. +
+
+

+

+ +

+
+ This indicates the desired email is not supported. While it may be a valid email, admins may have blocked specific domains from being associated with account email addresses. +
+
+

+

+ +

+
+ This indicates the desired username is already in-use or was previously used by a now deleted account. You need to pick a different username. +
+
+

+

+ +

+
+ This indicates the desired username is not a valid format, usernames may only contain one dash (-), period (.) or underscore (_) and must start with a letter or number. +
+
+

+

+ +

+
+ This indicates the reason you provided for joining is less than the minimum accepted characters. The reason should be atleast 20 characters long, up to 1000 characters. If you desire to share a longer reason than 1000 characters, consider using a pastebin and posting the link. We can't guarantee that admins will visit any links you provided, so ideally you can keep the length within 1000 chars. +
+
+

+

+ +

+
+

We understand that receiving a notification of rejection can be disappointing. Here's what you can consider if your application to join Pixelfed hasn't been successful:

+ +
    +
  • + Review Your Application: Reflect on the information you provided. Our decision may have been influenced by a variety of factors, including the clarity of your intentions or how well they align with our community values. Consider if there was any additional context or passion for Pixelfed that you could have included. +
  • +
  • + Reapply with Updated Information: We encourage you to reapply if you feel your initial application didn’t fully capture your enthusiasm or alignment with our community values. However, we recommend exercising caution and thoughtfulness. Please take time to refine and enhance your application before resubmitting, as repetitive or frequent submissions can overwhelm our admin team. We value careful consideration and meaningful updates in reapplications, as this helps us maintain a fair and manageable review process for everyone. Your patience and understanding in this regard are greatly appreciated and can positively influence the outcome of future applications. +
  • +
  • + Seek Feedback: If you are seeking clarity on why your application wasn't successful, you're welcome to contact us for feedback. However, please be mindful that our admins handle a high volume of queries and applications. While we strive to provide helpful responses, our ability to offer detailed individual feedback may be limited. We ask for your patience and understanding in this matter. When reaching out, ensure your query is concise and considerate of the admins' time. This approach will help us assist you more effectively and maintain a positive interaction, even if your initial application didn't meet our criteria. +
  • +
  • + Stay Engaged: Even if you're not a member yet, you can stay connected with Pixelfed through our public forums, blog, or social media channels. This will keep you updated on any changes or new features that might make our platform a better fit for you in the future. +
  • +
+ +

Remember, a rejection is not necessarily a reflection of your qualities or potential as a member of our community. It's often about finding the right fit at the right time. We appreciate your interest in Pixelfed and hope you won't be discouraged from exploring other ways to engage with our platform.

+

+
+
+

+@endsection diff --git a/resources/views/site/help/partial/sidebar.blade.php b/resources/views/site/help/partial/sidebar.blade.php index e0a82c9d3..1e8c3d68f 100644 --- a/resources/views/site/help/partial/sidebar.blade.php +++ b/resources/views/site/help/partial/sidebar.blade.php @@ -33,6 +33,13 @@ + @if((bool) config_cache('instance.curated_registration.enabled')) + + @endif diff --git a/routes/web-admin.php b/routes/web-admin.php index 72572b5c0..9f01d5c7c 100644 --- a/routes/web-admin.php +++ b/routes/web-admin.php @@ -104,11 +104,11 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('asf/create', 'AdminShadowFilterController@store'); Route::get('asf/home', 'AdminShadowFilterController@home'); - // Route::redirect('curated-onboarding/', 'curated-onboarding/home'); - // Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); - // Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); - // Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); - // Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); + Route::redirect('curated-onboarding/', 'curated-onboarding/home'); + Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); + Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); + Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); + Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); Route::prefix('api')->group(function() { Route::get('stats', 'AdminController@getStats'); @@ -157,10 +157,10 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('autospam/config/enable', 'AdminController@enableAutospamApi'); Route::post('autospam/config/disable', 'AdminController@disableAutospamApi'); // Route::get('instances/{id}/accounts', 'AdminController@getInstanceAccounts'); - // Route::get('curated-onboarding/show/{id}/activity-log', 'AdminCuratedRegisterController@apiActivityLog'); - // Route::post('curated-onboarding/show/{id}/message/preview', 'AdminCuratedRegisterController@apiMessagePreviewStore'); - // Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); - // Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); - // Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); + Route::get('curated-onboarding/show/{id}/activity-log', 'AdminCuratedRegisterController@apiActivityLog'); + Route::post('curated-onboarding/show/{id}/message/preview', 'AdminCuratedRegisterController@apiMessagePreviewStore'); + Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); + Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); + Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); }); }); diff --git a/routes/web.php b/routes/web.php index 3947bf5da..6a5b878ee 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,16 +29,18 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); - // Route::get('auth/sign_up', 'CuratedRegisterController@index'); - // Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); - // Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); - // Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge'); - // Route::post('auth/sign_up/concierge', 'CuratedRegisterController@conciergeStore'); - // Route::get('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormShow'); - // Route::post('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormStore'); - // Route::get('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmail'); - // Route::post('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmailHandle'); - // Route::get('auth/sign_up/confirmed', 'CuratedRegisterController@emailConfirmed'); + Route::get('auth/sign_up', 'CuratedRegisterController@index')->name('auth.curated-onboarding'); + Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); + Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); + Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge'); + Route::post('auth/sign_up/concierge', 'CuratedRegisterController@conciergeStore'); + Route::get('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormShow'); + Route::post('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormStore'); + Route::get('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmail'); + Route::post('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmailHandle'); + Route::get('auth/sign_up/confirmed', 'CuratedRegisterController@emailConfirmed'); + Route::get('auth/sign_up/resend-confirmation', 'CuratedRegisterController@resendConfirmation'); + Route::post('auth/sign_up/resend-confirmation', 'CuratedRegisterController@resendConfirmationProcess'); Route::get('auth/forgot/email', 'UserEmailForgotController@index')->name('email.forgot'); Route::post('auth/forgot/email', 'UserEmailForgotController@store')->middleware('throttle:10,900,forgotEmail'); @@ -306,7 +308,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::view('import', 'site.help.import')->name('help.import'); Route::view('parental-controls', 'site.help.parental-controls'); // Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues'); - // Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding'); + Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding'); }); Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); Route::get('newsroom/archive', 'NewsroomController@archive');