diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 905554c6b..4e6395efd 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/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 + ]); + } } diff --git a/config/pixelfed.php b/config/pixelfed.php index e303d5ae4..fcc118796 100644 --- a/config/pixelfed.php +++ b/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), ]; diff --git a/database/migrations/2022_11_24_065214_add_register_source_to_users_table.php b/database/migrations/2022_11_24_065214_add_register_source_to_users_table.php new file mode 100644 index 000000000..d29c574a9 --- /dev/null +++ b/database/migrations/2022_11_24_065214_add_register_source_to_users_table.php @@ -0,0 +1,36 @@ +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'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 4be4e71ba..c63498abb 100644 --- a/routes/api.php +++ b/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) {