ソースを参照

Initial in-app registration logic

Daniel Supernault 2 年 前
コミット
772cfb9cee

+ 147 - 0
app/Http/Controllers/Api/ApiV1Dot1Controller.php

@@ -14,12 +14,18 @@ use App\EmailVerification;
 use App\Status;
 use App\Report;
 use App\Profile;
+use App\User;
 use App\Services\AccountService;
 use App\Services\StatusService;
 use App\Services\ProfileStatusService;
+use App\Util\Lexer\RestrictedNames;
+use App\Services\EmailService;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Hash;
 use Jenssegers\Agent\Agent;
 use Mail;
 use App\Mail\PasswordChange;
+use App\Mail\ConfirmAppEmail;
 
 class ApiV1Dot1Controller extends Controller
 {
@@ -402,4 +408,145 @@ class ApiV1Dot1Controller extends Controller
 
         return $this->json($res);
     }
+
+    public function inAppRegistrationPreFlightCheck(Request $request)
+    {
+        return [
+            'open' => config('pixelfed.open_registration'),
+            'iara' => config('pixelfed.allow_app_registration')
+        ];
+    }
+
+    public function inAppRegistration(Request $request)
+    {
+        abort_if($request->user(), 404);
+        abort_unless(config('pixelfed.open_registration'), 404);
+        abort_unless(config('pixelfed.allow_app_registration'), 404);
+        abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
+        $this->validate($request, [
+            'email' => [
+                'required',
+                'string',
+                'email',
+                'max:255',
+                'unique:users',
+                function ($attribute, $value, $fail) {
+                    $banned = EmailService::isBanned($value);
+                    if($banned) {
+                        return $fail('Email is invalid.');
+                    }
+                },
+            ],
+            'username' => [
+                'required',
+                'min:2',
+                'max:15',
+                'unique:users',
+                function ($attribute, $value, $fail) {
+                    $dash = substr_count($value, '-');
+                    $underscore = substr_count($value, '_');
+                    $period = substr_count($value, '.');
+
+                    if(ends_with($value, ['.php', '.js', '.css'])) {
+                        return $fail('Username is invalid.');
+                    }
+
+                    if(($dash + $underscore + $period) > 1) {
+                        return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+                    }
+
+                    if (!ctype_alnum($value[0])) {
+                        return $fail('Username is invalid. Must start with a letter or number.');
+                    }
+
+                    if (!ctype_alnum($value[strlen($value) - 1])) {
+                        return $fail('Username is invalid. Must end with a letter or number.');
+                    }
+
+                    $val = str_replace(['_', '.', '-'], '', $value);
+                    if(!ctype_alnum($val)) {
+                        return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+                    }
+
+                    $restricted = RestrictedNames::get();
+                    if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+                        return $fail('Username cannot be used.');
+                    }
+                },
+            ],
+            'password' => 'required|string|min:8',
+            // 'avatar' => 'required|mimetypes:image/jpeg,image/png|max:15000',
+            // 'bio' => 'required|max:140'
+        ]);
+
+        $email = $request->input('email');
+        $username = $request->input('username');
+        $password = $request->input('password');
+
+        if(config('database.default') == 'pgsql') {
+            $username = strtolower($username);
+            $email = strtolower($email);
+        }
+
+        $user = new User;
+        $user->name = $username;
+        $user->username = $username;
+        $user->email = $email;
+        $user->password = Hash::make($password);
+        $user->register_source = 'app';
+        $user->app_register_ip = $request->ip();
+        $user->app_register_token = Str::random(32);
+        $user->save();
+
+        $rtoken = Str::random(mt_rand(64, 70));
+
+        $verify = new EmailVerification();
+        $verify->user_id = $user->id;
+        $verify->email = $user->email;
+        $verify->user_token = $user->app_register_token;
+        $verify->random_token = $rtoken;
+        $verify->save();
+
+        $appUrl = 'pixelfed://confirm-account/'. $user->app_register_token . '?rt=' . $rtoken;
+
+        Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
+
+        return response()->json([
+            'success' => true,
+        ]);
+    }
+
+    public function inAppRegistrationConfirm(Request $request)
+    {
+        abort_if($request->user(), 404);
+        abort_unless(config('pixelfed.open_registration'), 404);
+        abort_unless(config('pixelfed.allow_app_registration'), 404);
+        abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
+        $this->validate($request, [
+            'user_token' => 'required',
+            'random_token' => 'required',
+            'email' => 'required'
+        ]);
+
+        $verify = EmailVerification::whereEmail($request->input('email'))
+            ->whereUserToken($request->input('user_token'))
+            ->whereRandomToken($request->input('random_token'))
+            ->first();
+
+        if(!$verify) {
+            return response()->json(['error' => 'Invalid tokens'], 403);
+        }
+
+        $user = User::findOrFail($verify->user_id);
+        $user->email_verified_at = now();
+        $user->save();
+
+        $verify->delete();
+
+        $token = $user->createToken('Pixelfed');
+
+        return response()->json([
+            'access_token' => $token->access_token
+        ]);
+    }
 }

+ 2 - 0
config/pixelfed.php

@@ -276,4 +276,6 @@ return [
 	'media_fast_process' => env('PF_MEDIA_FAST_PROCESS', true),
 
 	'max_altext_length' => env('PF_MEDIA_MAX_ALTTEXT_LENGTH', 1000),
+
+	'allow_app_registration' => env('PF_ALLOW_APP_REGISTRATION', true),
 ];

+ 36 - 0
database/migrations/2022_11_24_065214_add_register_source_to_users_table.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->string('register_source')->default('web')->nullable()->index();
+            $table->string('app_register_token')->nullable();
+            $table->string('app_register_ip')->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('register_source');
+            $table->dropColumn('app_register_token');
+            $table->dropColumn('app_register_ip');
+        });
+    }
+};

+ 6 - 0
routes/api.php

@@ -149,6 +149,12 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
 		Route::group(['prefix' => 'directory'], function () use($middleware) {
 			Route::get('listing', 'PixelfedDirectoryController@get');
 		});
+
+		Route::group(['prefix' => 'auth'], function () use($middleware) {
+			Route::get('iarpfc', 'Api\ApiV1Dot1Controller@inAppRegistrationPreFlightCheck');
+			Route::post('iar', 'Api\ApiV1Dot1Controller@inAppRegistration');
+			Route::post('iarc', 'Api\ApiV1Dot1Controller@inAppRegistrationConfirm');
+		});
 	});
 
 	Route::group(['prefix' => 'live'], function() use($middleware) {