diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 5eb1159fe..8c10e5d0c 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -60,7 +60,7 @@ class RegisterController extends Controller * * @return \Illuminate\Contracts\Validation\Validator */ - protected function validator(array $data) + public function validator(array $data) { if(config('database.default') == 'pgsql') { $data['username'] = strtolower($data['username']); @@ -151,7 +151,7 @@ class RegisterController extends Controller * * @return \App\User */ - protected function create(array $data) + public function create(array $data) { if(config('database.default') == 'pgsql') { $data['username'] = strtolower($data['username']); diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php new file mode 100644 index 000000000..5c60cfae2 --- /dev/null +++ b/app/Http/Controllers/ParentalControlsController.php @@ -0,0 +1,207 @@ +user(), 404); + } + abort_unless(config('instance.parental_controls.enabled'), 404); + if(config_cache('pixelfed.open_registration') == false) { + abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404); + } + if($maxUserCheck == true) { + $hasLimit = config('pixelfed.enforce_max_users'); + if($hasLimit) { + $count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count(); + $limit = (int) config('pixelfed.max_users'); + + abort_if($limit && $limit <= $count, 404); + } + } + } + + public function index(Request $request) + { + $this->authPreflight($request); + $children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5); + return view('settings.parental-controls.index', compact('children')); + } + + public function add(Request $request) + { + $this->authPreflight($request, true); + return view('settings.parental-controls.add'); + } + + public function view(Request $request, $id) + { + $this->authPreflight($request); + $uid = $request->user()->id; + $pc = ParentalControls::whereParentId($uid)->findOrFail($id); + return view('settings.parental-controls.manage', compact('pc')); + } + + public function update(Request $request, $id) + { + $this->authPreflight($request); + $uid = $request->user()->id; + $pc = ParentalControls::whereParentId($uid)->findOrFail($id); + $pc->permissions = $this->requestFormFields($request); + $pc->save(); + return redirect($pc->manageUrl() . '?permissions'); + } + + public function store(Request $request) + { + $this->authPreflight($request, true); + $this->validate($request, [ + 'email' => 'required|email|unique:parental_controls,email|unique:users,email', + ]); + + $state = $this->requestFormFields($request); + + $pc = new ParentalControls; + $pc->parent_id = $request->user()->id; + $pc->email = $request->input('email'); + $pc->verify_code = str_random(32); + $pc->permissions = $state; + $pc->save(); + + DispatchChildInvitePipeline::dispatch($pc); + return redirect($pc->manageUrl()); + } + + public function inviteRegister(Request $request, $id, $code) + { + $this->authPreflight($request, true, false); + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id); + abort_unless(User::whereId($pc->parent_id)->exists(), 404); + return view('settings.parental-controls.invite-register-form', compact('pc')); + } + + public function inviteRegisterStore(Request $request, $id, $code) + { + $this->authPreflight($request, true, false); + + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id); + + $fields = $request->all(); + $fields['email'] = $pc->email; + $defaults = UserRoleService::defaultRoles(); + $validator = (new RegisterController)->validator($fields); + $valid = $validator->validate(); + abort_if(!$valid, 404); + event(new Registered($user = (new RegisterController)->create($fields))); + sleep(5); + $user->has_roles = true; + $user->parent_id = $pc->parent_id; + if(config('instance.parental_controls.limits.auto_verify_email')) { + $user->email_verified_at = now(); + $user->save(); + sleep(3); + } else { + $user->save(); + sleep(3); + } + $ur = UserRoles::updateOrCreate([ + 'user_id' => $user->id, + ],[ + 'roles' => UserRoleService::mapInvite($user->id, $pc->permissions) + ]); + $pc->email_verified_at = now(); + $pc->child_id = $user->id; + $pc->save(); + sleep(2); + Auth::guard()->login($user); + + return redirect('/i/web'); + } + + public function cancelInvite(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + return view('settings.parental-controls.delete-invite', compact('pc')); + } + + public function cancelInviteHandle(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + $pc->delete(); + + return redirect('/settings/parental-controls'); + } + + public function stopManaging(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNotNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + return view('settings.parental-controls.stop-managing', compact('pc')); + } + + public function stopManagingHandle(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNotNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + $pc->child()->update([ + 'has_roles' => false, + 'parent_id' => null, + ]); + $pc->delete(); + + return redirect('/settings/parental-controls'); + } + + protected function requestFormFields($request) + { + $state = []; + $fields = [ + 'post', + 'comment', + 'like', + 'share', + 'follow', + 'bookmark', + 'story', + 'collection', + 'discovery_feeds', + 'dms', + 'federation', + 'hide_network', + 'private', + 'hide_cw' + ]; + + foreach ($fields as $field) { + $state[$field] = $request->input($field) == 'on'; + } + + return $state; + } +} diff --git a/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php new file mode 100644 index 000000000..a67f4e444 --- /dev/null +++ b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php @@ -0,0 +1,38 @@ +pc = $pc; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $pc = $this->pc; + + Mail::to($pc->email)->send(new ParentChildInvite($pc)); + } +} diff --git a/app/Mail/ParentChildInvite.php b/app/Mail/ParentChildInvite.php new file mode 100644 index 000000000..843ea472d --- /dev/null +++ b/app/Mail/ParentChildInvite.php @@ -0,0 +1,49 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/ParentalControls.php b/app/Models/ParentalControls.php new file mode 100644 index 000000000..83d47c18a --- /dev/null +++ b/app/Models/ParentalControls.php @@ -0,0 +1,55 @@ + 'array', + 'email_sent_at' => 'datetime', + 'email_verified_at' => 'datetime' + ]; + + protected $guarded = []; + + public function parent() + { + return $this->belongsTo(User::class, 'parent_id'); + } + + public function child() + { + return $this->belongsTo(User::class, 'child_id'); + } + + public function childAccount() + { + if($u = $this->child) { + if($u->profile_id) { + return AccountService::get($u->profile_id, true); + } else { + return []; + } + } else { + return []; + } + } + + public function manageUrl() + { + return url('/settings/parental-controls/manage/' . $this->id); + } + + public function inviteUrl() + { + return url('/auth/pci/' . $this->id . '/' . $this->verify_code); + } +} diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php index 500a4666e..a18810bf0 100644 --- a/app/Services/UserRoleService.php +++ b/app/Services/UserRoleService.php @@ -52,6 +52,13 @@ class UserRoleService 'can-follow' => false, 'can-make-public' => false, + + 'can-direct-message' => false, + 'can-use-stories' => false, + 'can-view-sensitive' => false, + 'can-bookmark' => false, + 'can-collections' => false, + 'can-federation' => false, ]; } @@ -114,6 +121,71 @@ class UserRoleService 'title' => 'Can make account public', 'action' => 'Allows the ability to make account public' ], + + 'can-direct-message' => [ + 'title' => '', + 'action' => '' + ], + 'can-use-stories' => [ + 'title' => '', + 'action' => '' + ], + 'can-view-sensitive' => [ + 'title' => '', + 'action' => '' + ], + 'can-bookmark' => [ + 'title' => '', + 'action' => '' + ], + 'can-collections' => [ + 'title' => '', + 'action' => '' + ], + 'can-federation' => [ + 'title' => '', + 'action' => '' + ], ]; } + + public static function mapInvite($id, $data = []) + { + $roles = self::get($id); + + $map = [ + 'account-force-private' => 'private', + 'account-ignore-follow-requests' => 'private', + + 'can-view-public-feed' => 'discovery_feeds', + 'can-view-network-feed' => 'discovery_feeds', + 'can-view-discover' => 'discovery_feeds', + 'can-view-hashtag-feed' => 'discovery_feeds', + + 'can-post' => 'post', + 'can-comment' => 'comment', + 'can-like' => 'like', + 'can-share' => 'share', + + 'can-follow' => 'follow', + 'can-make-public' => '!private', + + 'can-direct-message' => 'dms', + 'can-use-stories' => 'story', + 'can-view-sensitive' => '!hide_cw', + 'can-bookmark' => 'bookmark', + 'can-collections' => 'collection', + 'can-federation' => 'federation', + ]; + + foreach ($map as $key => $value) { + if(!isset($data[$value], $data[substr($value, 1)])) { + $map[$key] = false; + continue; + } + $map[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value]; + } + + return $map; + } } diff --git a/config/instance.php b/config/instance.php index 6357afe63..5e173684c 100644 --- a/config/instance.php +++ b/config/instance.php @@ -129,5 +129,15 @@ return [ 'banner' => [ 'blurhash' => env('INSTANCE_BANNER_BLURHASH', 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt') - ] + ], + + 'parental_controls' => [ + 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', true), + + 'limits' => [ + 'respect_open_registration' => env('INSTANCE_PARENTAL_CONTROLS_RESPECT_OPENREG', true), + 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 10), + 'auto_verify_email' => true, + ], + ] ]; diff --git a/database/migrations/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php new file mode 100644 index 000000000..4ef7fd2c7 --- /dev/null +++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php @@ -0,0 +1,45 @@ +id(); + $table->unsignedInteger('parent_id')->index(); + $table->unsignedInteger('child_id')->unique()->index()->nullable(); + $table->string('email')->unique()->nullable(); + $table->string('verify_code')->nullable(); + $table->timestamp('email_sent_at')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->json('permissions')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::table('user_roles', function (Blueprint $table) { + $table->dropIndex('user_roles_profile_id_unique'); + $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('parental_controls'); + + Schema::table('user_roles', function (Blueprint $table) { + $table->dropIndex('user_roles_profile_id_unique'); + $table->unsignedBigInteger('profile_id')->unique()->index()->change(); + }); + } +}; diff --git a/resources/views/components/collapse.blade.php b/resources/views/components/collapse.blade.php new file mode 100644 index 000000000..144579366 --- /dev/null +++ b/resources/views/components/collapse.blade.php @@ -0,0 +1,12 @@ +@php +$cid = 'col' . str_random(6); +@endphp +
Child Invite Sent!
+ +Child Invite Sent!
+ +Child Account Active
+ +@{{ $child->childAccount()['username'] }}
+{{ $child->childAccount()['display_name'] }}
+ @else +Invite Pending
+{{ $child->email }}
+ @endif +You are not managing any children accounts.
+In the digital age, ensuring your children's online safety is paramount. Designed with both fun and safety in mind, this feature allows parents to create child accounts, tailor-made for a worry-free social media experience.
+ +Key Features:
+ +This feature has been disabled by server admins.
+ @endif +