diff --git a/CHANGELOG.md b/CHANGELOG.md index 577337f93..b1662caee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5)) +- Sign-in with Mastodon ([#4545](https://github.com/pixelfed/pixelfed/pull/4545)) ([45b9404e](https://github.com/pixelfed/pixelfed/commit/45b9404e)) ### Updates - Update Notifications.vue component, fix filtering logic to prevent endless spinner ([3df9b53f](https://github.com/pixelfed/pixelfed/commit/3df9b53f)) diff --git a/app/Http/Controllers/RemoteAuthController.php b/app/Http/Controllers/RemoteAuthController.php new file mode 100644 index 000000000..d48e5b982 --- /dev/null +++ b/app/Http/Controllers/RemoteAuthController.php @@ -0,0 +1,568 @@ +user()) { + return redirect('/'); + } + return view('auth.remote.start'); + } + + public function startRedirect(Request $request) + { + return redirect('/login'); + } + + public function getAuthDomains(Request $request) + { + if(config('remote-auth.mastodon.domains.only_custom')) { + $res = config('remote-auth.mastodon.domains.custom'); + if(!$res || !strlen($res)) { + return []; + } + $res = explode(',', $res); + return response()->json($res); + } + + $res = config('remote-auth.mastodon.domains.default'); + $res = explode(',', $res); + + return response()->json($res); + } + + public function redirect(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + $this->validate($request, ['domain' => 'required']); + + $domain = $request->input('domain'); + $compatible = RemoteAuthService::isDomainCompatible($domain); + + if(!$compatible) { + $res = [ + 'domain' => $domain, + 'ready' => false, + 'action' => 'incompatible_domain' + ]; + return response()->json($res); + } + + if(config('remote-auth.mastodon.domains.only_default')) { + $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default')); + if(!in_array($domain, $defaultDomains)) { + $res = [ + 'domain' => $domain, + 'ready' => false, + 'action' => 'incompatible_domain' + ]; + return response()->json($res); + } + } + + if(config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) { + $customDomains = explode(',', config('remote-auth.mastodon.domains.custom')); + if(!in_array($domain, $customDomains)) { + $res = [ + 'domain' => $domain, + 'ready' => false, + 'action' => 'incompatible_domain' + ]; + return response()->json($res); + } + } + + $client = RemoteAuthService::getMastodonClient($domain); + + abort_unless($client, 422, 'Invalid mastodon client'); + + $request->session()->put('state', $state = Str::random(40)); + $request->session()->put('oauth_domain', $domain); + + $query = http_build_query([ + 'client_id' => $client->client_id, + 'redirect_uri' => $client->redirect_uri, + 'response_type' => 'code', + 'scope' => 'read', + 'state' => $state, + ]); + + $request->session()->put('oauth_redirect_to', 'https://' . $domain . '/oauth/authorize?' . $query); + + $dsh = Str::random(17); + $res = [ + 'domain' => $domain, + 'ready' => true, + 'dsh' => $dsh + ]; + + return response()->json($res); + } + + public function preflight(Request $request) + { + if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) { + return redirect('/login'); + } + + return redirect()->away($request->session()->pull('oauth_redirect_to')); + } + + public function handleCallback(Request $request) + { + $domain = $request->session()->get('oauth_domain'); + + if($request->filled('code')) { + $code = $request->input('code'); + $state = $request->session()->pull('state'); + + throw_unless( + strlen($state) > 0 && $state === $request->state, + InvalidArgumentException::class, + 'Invalid state value.' + ); + + $res = RemoteAuthService::getToken($domain, $code); + + if(!$res || !isset($res['access_token'])) { + $request->session()->regenerate(); + return redirect('/login'); + } + + $request->session()->put('oauth_remote_session_token', $res['access_token']); + return redirect('/auth/mastodon/getting-started'); + } + + return redirect('/login'); + } + + public function onboarding(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + if($request->user()) { + return redirect('/'); + } + return view('auth.remote.onboarding'); + } + + public function sessionCheck(Request $request) + { + abort_if($request->user(), 403); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + + $domain = $request->session()->get('oauth_domain'); + $token = $request->session()->get('oauth_remote_session_token'); + + $res = RemoteAuthService::getVerifyCredentials($domain, $token); + + abort_if(!$res || !isset($res['acct']), 403, 'Invalid credentials'); + + $webfinger = strtolower('@' . $res['acct'] . '@' . $domain); + $request->session()->put('oauth_masto_webfinger', $webfinger); + + if(config('remote-auth.mastodon.max_uses.enabled')) { + $limit = config('remote-auth.mastodon.max_uses.limit'); + $uses = RemoteAuthService::lookupWebfingerUses($webfinger); + if($uses >= $limit) { + return response()->json([ + 'code' => 200, + 'msg' => 'Success!', + 'action' => 'max_uses_reached' + ]); + } + } + + $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first(); + if($exists && $exists->user_id) { + return response()->json([ + 'code' => 200, + 'msg' => 'Success!', + 'action' => 'redirect_existing_user' + ]); + } + + return response()->json([ + 'code' => 200, + 'msg' => 'Success!', + 'action' => 'onboard' + ]); + } + + public function sessionGetMastodonData(Request $request) + { + abort_if($request->user(), 403); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + + $domain = $request->session()->get('oauth_domain'); + $token = $request->session()->get('oauth_remote_session_token'); + + $res = RemoteAuthService::getVerifyCredentials($domain, $token); + $res['_webfinger'] = strtolower('@' . $res['acct'] . '@' . $domain); + $res['_domain'] = strtolower($domain); + $request->session()->put('oauth_remasto_id', $res['id']); + + $ra = RemoteAuth::updateOrCreate([ + 'domain' => $domain, + 'webfinger' => $res['_webfinger'], + ], [ + 'software' => 'mastodon', + 'ip_address' => $request->ip(), + 'bearer_token' => $token, + 'verify_credentials' => $res, + 'last_verify_credentials_at' => now(), + 'last_successful_login_at' => now() + ]); + + $request->session()->put('oauth_masto_raid', $ra->id); + + return response()->json($res); + } + + public function sessionValidateUsername(Request $request) + { + abort_if($request->user(), 403); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + + $this->validate($request, [ + 'username' => [ + 'required', + 'min:2', + 'max:15', + 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.'); + } + } + ] + ]); + $username = strtolower($request->input('username')); + + $exists = User::where('username', $username)->exists(); + + return response()->json([ + 'code' => 200, + 'username' => $username, + 'exists' => $exists + ]); + } + + public function sessionValidateEmail(Request $request) + { + abort_if($request->user(), 403); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + + $this->validate($request, [ + 'email' => [ + 'required', + 'email:strict,filter_unicode,dns,spoof', + ] + ]); + + $email = $request->input('email'); + $banned = EmailService::isBanned($email); + $exists = User::where('email', $email)->exists(); + + return response()->json([ + 'code' => 200, + 'email' => $email, + 'exists' => $exists, + 'banned' => $banned + ]); + } + + public function sessionGetMastodonFollowers(Request $request) + { + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + abort_unless($request->session()->exists('oauth_remasto_id'), 403); + + $domain = $request->session()->get('oauth_domain'); + $token = $request->session()->get('oauth_remote_session_token'); + $id = $request->session()->get('oauth_remasto_id'); + + $res = RemoteAuthService::getFollowing($domain, $token, $id); + + if(!$res) { + return response()->json([ + 'code' => 200, + 'following' => [] + ]); + } + + $res = collect($res)->filter(fn($acct) => Helpers::validateUrl($acct['url']))->values()->toArray(); + + return response()->json([ + 'code' => 200, + 'following' => $res + ]); + } + + public function handleSubmit(Request $request) + { + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + abort_unless($request->session()->exists('oauth_remasto_id'), 403); + abort_unless($request->session()->exists('oauth_masto_webfinger'), 403); + abort_unless($request->session()->exists('oauth_masto_raid'), 403); + + $this->validate($request, [ + 'email' => 'required|email:strict,filter_unicode,dns,spoof', + 'username' => [ + 'required', + 'min:2', + 'max:15', + 'unique:users,username', + 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|confirmed', + 'name' => 'nullable|max:30' + ]); + + $email = $request->input('email'); + $username = $request->input('username'); + $password = $request->input('password'); + $name = $request->input('name'); + + $user = $this->createUser([ + 'name' => $name, + 'username' => $username, + 'password' => $password, + 'email' => $email + ]); + + $raid = $request->session()->pull('oauth_masto_raid'); + $webfinger = $request->session()->pull('oauth_masto_webfinger'); + $token = $user->createToken('Onboarding')->accessToken; + + $ra = RemoteAuth::where('id', $raid)->where('webfinger', $webfinger)->firstOrFail(); + $ra->user_id = $user->id; + $ra->save(); + + return [ + 'code' => 200, + 'msg' => 'Success', + 'token' => $token + ]; + } + + public function storeBio(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + abort_unless($request->user(), 404); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + abort_unless($request->session()->exists('oauth_remasto_id'), 403); + + $this->validate($request, [ + 'bio' => 'required|nullable|max:500', + ]); + + $profile = $request->user()->profile; + $profile->bio = Purify::clean($request->input('bio')); + $profile->save(); + + return [200]; + } + + public function accountToId(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + abort_if($request->user(), 404); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + abort_unless($request->session()->exists('oauth_remasto_id'), 403); + + $this->validate($request, [ + 'account' => 'required|url' + ]); + + $account = $request->input('account'); + abort_unless(substr(strtolower($account), 0, 8) === 'https://', 404); + + $host = strtolower(config('pixelfed.domain.app')); + $domain = strtolower(parse_url($account, PHP_URL_HOST)); + + if($domain == $host) { + $username = Str::of($account)->explode('/')->last(); + $user = User::where('username', $username)->first(); + if($user) { + return ['id' => (string) $user->profile_id]; + } else { + return []; + } + } else { + try { + $profile = Helpers::profileFetch($account); + if($profile) { + return ['id' => (string) $profile->id]; + } else { + return []; + } + } catch (\GuzzleHttp\Exception\RequestException $e) { + return; + } catch (Exception $e) { + return []; + } + } + } + + public function storeAvatar(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + abort_unless($request->user(), 404); + $this->validate($request, [ + 'avatar_url' => 'required|active_url', + ]); + + $user = $request->user(); + $profile = $user->profile; + + abort_if(!$profile->avatar, 404, 'Missing avatar'); + + $avatar = $profile->avatar; + $avatar->remote_url = $request->input('avatar_url'); + $avatar->save(); + + MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false); + + return [200]; + } + + public function finishUp(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + abort_unless($request->user(), 404); + + $currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app'); + $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail(); + RemoteAuthService::submitToBeagle( + $ra->webfinger, + $ra->verify_credentials['url'], + $currentWebfinger, + $request->user()->url() + ); + + return [200]; + } + + public function handleLogin(Request $request) + { + abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404); + abort_if($request->user(), 404); + abort_unless($request->session()->exists('oauth_domain'), 403); + abort_unless($request->session()->exists('oauth_remote_session_token'), 403); + abort_unless($request->session()->exists('oauth_masto_webfinger'), 403); + + $domain = $request->session()->get('oauth_domain'); + $wf = $request->session()->get('oauth_masto_webfinger'); + + $ra = RemoteAuth::where('webfinger', $wf)->where('domain', $domain)->whereNotNull('user_id')->firstOrFail(); + + $user = User::findOrFail($ra->user_id); + abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action'); + Auth::loginUsingId($ra->user_id); + return [200]; + } + + protected function createUser($data) + { + event(new Registered($user = User::create([ + 'name' => Purify::clean($data['name']), + 'username' => $data['username'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + 'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null, + 'app_register_ip' => request()->ip(), + 'register_source' => 'mastodon' + ]))); + + $this->guarder()->login($user); + + return $user; + } + + protected function guarder() + { + return Auth::guard(); + } +} diff --git a/app/Models/RemoteAuth.php b/app/Models/RemoteAuth.php new file mode 100644 index 000000000..98909f09b --- /dev/null +++ b/app/Models/RemoteAuth.php @@ -0,0 +1,19 @@ + 'array', + 'last_successful_login_at' => 'datetime', + 'last_verify_credentials_at' => 'datetime' + ]; +} diff --git a/app/Models/RemoteAuthInstance.php b/app/Models/RemoteAuthInstance.php new file mode 100644 index 000000000..bdc03fcb2 --- /dev/null +++ b/app/Models/RemoteAuthInstance.php @@ -0,0 +1,13 @@ +exists()) { + return RemoteAuthInstance::whereDomain($domain)->first(); + } + + try { + $url = 'https://' . $domain . '/api/v1/apps'; + $res = Http::asForm()->throw()->timeout(10)->post($url, [ + 'client_name' => config('pixelfed.domain.app', 'pixelfed'), + 'redirect_uris' => url('/auth/mastodon/callback'), + 'scopes' => 'read', + 'website' => 'https://pixelfed.org' + ]); + + if(!$res->ok()) { + return false; + } + } catch (RequestException $e) { + return false; + } catch (ConnectionException $e) { + return false; + } catch (Exception $e) { + return false; + } + + $body = $res->json(); + + if(!$body || !isset($body['client_id'])) { + return false; + } + + $raw = RemoteAuthInstance::updateOrCreate([ + 'domain' => $domain + ], [ + 'client_id' => $body['client_id'], + 'client_secret' => $body['client_secret'], + 'redirect_uri' => $body['redirect_uri'], + ]); + + return $raw; + } + + public static function getToken($domain, $code) + { + $raw = RemoteAuthInstance::whereDomain($domain)->first(); + if(!$raw || !$raw->active || $raw->banned) { + return false; + } + + $url = 'https://' . $domain . '/oauth/token'; + $res = Http::asForm()->post($url, [ + 'code' => $code, + 'grant_type' => 'authorization_code', + 'client_id' => $raw->client_id, + 'client_secret' => $raw->client_secret, + 'redirect_uri' => $raw->redirect_uri, + 'scope' => 'read' + ]); + + return $res; + } + + public static function getVerifyCredentials($domain, $code) + { + $raw = RemoteAuthInstance::whereDomain($domain)->first(); + if(!$raw || !$raw->active || $raw->banned) { + return false; + } + + $url = 'https://' . $domain . '/api/v1/accounts/verify_credentials'; + + $res = Http::withToken($code)->get($url); + + return $res->json(); + } + + public static function getFollowing($domain, $code, $id) + { + $raw = RemoteAuthInstance::whereDomain($domain)->first(); + if(!$raw || !$raw->active || $raw->banned) { + return false; + } + + $url = 'https://' . $domain . '/api/v1/accounts/' . $id . '/following?limit=80'; + $key = self::CACHE_KEY . 'get-following:code:' . substr($code, 0, 16) . substr($code, -5) . ':domain:' . $domain. ':id:' .$id; + + return Cache::remember($key, 3600, function() use($url, $code) { + $res = Http::withToken($code)->get($url); + return $res->json(); + }); + } + + public static function isDomainCompatible($domain = false) + { + if(!$domain) { + return false; + } + + return Cache::remember(self::CACHE_KEY . 'domain-compatible:' . $domain, 14400, function() use($domain) { + try { + $res = Http::timeout(20)->retry(3, 750)->get('https://beagle.pixelfed.net/api/v1/raa/domain?domain=' . $domain); + if(!$res->ok()) { + return false; + } + } catch (RequestException $e) { + return false; + } catch (ConnectionException $e) { + return false; + } catch (Exception $e) { + return false; + } + $json = $res->json(); + + if(!in_array('compatible', $json)) { + return false; + } + + return $res['compatible']; + }); + } + + public static function lookupWebfingerUses($wf) + { + try { + $res = Http::timeout(20)->retry(3, 750)->get('https://beagle.pixelfed.net/api/v1/raa/lookup?webfinger=' . $wf); + if(!$res->ok()) { + return false; + } + } catch (RequestException $e) { + return false; + } catch (ConnectionException $e) { + return false; + } catch (Exception $e) { + return false; + } + $json = $res->json(); + if(!$json || !isset($json['count'])) { + return false; + } + + return $json['count']; + } + + public static function submitToBeagle($ow, $ou, $dw, $du) + { + try { + $url = 'https://beagle.pixelfed.net/api/v1/raa/submit'; + $res = Http::throw()->timeout(10)->get($url, [ + 'ow' => $ow, + 'ou' => $ou, + 'dw' => $dw, + 'du' => $du, + ]); + + if(!$res->ok()) { + return; + } + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + return; + } +} diff --git a/app/User.php b/app/User.php index 23faf63f4..a39f650be 100644 --- a/app/User.php +++ b/app/User.php @@ -37,7 +37,8 @@ class User extends Authenticatable 'password', 'app_register_ip', 'email_verified_at', - 'last_active_at' + 'last_active_at', + 'register_source' ]; /** diff --git a/config/remote-auth.php b/config/remote-auth.php new file mode 100644 index 000000000..3f85b9d40 --- /dev/null +++ b/config/remote-auth.php @@ -0,0 +1,56 @@ + [ + 'enabled' => env('PF_LOGIN_WITH_MASTODON_ENABLED', false), + + 'contraints' => [ + /* + * Skip email verification + * + * To improve the onboarding experience, you can opt to skip the email + * verification process and automatically verify their email + */ + 'skip_email_verification' => env('PF_LOGIN_WITH_MASTODON_SKIP_EMAIL', true), + ], + + 'domains' => [ + 'default' => 'mastodon.social,mastodon.online,mstdn.social,mas.to', + + /* + * Custom mastodon domains + * + * Define a comma separated list of custom domains to allow + */ + 'custom' => env('PF_LOGIN_WITH_MASTODON_DOMAINS'), + + /* + * Use only default domains + * + * Allow Sign-in with Mastodon using only the default domains + */ + 'only_default' => env('PF_LOGIN_WITH_MASTODON_ONLY_DEFAULT', false), + + /* + * Use only custom domains + * + * Allow Sign-in with Mastodon using only the custom domains + * you define, in comma separated format + */ + 'only_custom' => env('PF_LOGIN_WITH_MASTODON_ONLY_CUSTOM', false), + ], + + 'max_uses' => [ + /* + * Max Uses + * + * Using a centralized service operated by pixelfed.org that tracks mastodon imports, + * you can set a limit of how many times a mastodon account can be imported across + * all known and reporting Pixelfed instances to prevent the same masto account from + * abusing this + */ + 'enabled' => env('PF_LOGIN_WITH_MASTODON_ENFORCE_MAX_USES', true), + 'limit' => env('PF_LOGIN_WITH_MASTODON_MAX_USES_LIMIT', 3) + ] + ], +]; diff --git a/database/migrations/2023_07_07_025757_create_remote_auths_table.php b/database/migrations/2023_07_07_025757_create_remote_auths_table.php new file mode 100644 index 000000000..774965aa2 --- /dev/null +++ b/database/migrations/2023_07_07_025757_create_remote_auths_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('software')->nullable(); + $table->string('domain')->nullable()->index(); + $table->string('webfinger')->nullable()->unique()->index(); + $table->unsignedInteger('instance_id')->nullable()->index(); + $table->unsignedInteger('user_id')->nullable()->unique()->index(); + $table->unsignedInteger('client_id')->nullable()->index(); + $table->string('ip_address')->nullable(); + $table->text('bearer_token')->nullable(); + $table->json('verify_credentials')->nullable(); + $table->timestamp('last_successful_login_at')->nullable(); + $table->timestamp('last_verify_credentials_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('remote_auths'); + } +}; diff --git a/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php b/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php new file mode 100644 index 000000000..690197b9b --- /dev/null +++ b/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('domain')->nullable()->unique()->index(); + $table->unsignedInteger('instance_id')->nullable()->index(); + $table->string('client_id')->nullable(); + $table->string('client_secret')->nullable(); + $table->string('redirect_uri')->nullable(); + $table->string('root_domain')->nullable()->index(); + $table->boolean('allowed')->nullable()->index(); + $table->boolean('banned')->default(false)->index(); + $table->boolean('active')->default(true)->index(); + $table->timestamp('last_refreshed_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('remote_auth_instances'); + } +}; diff --git a/public/js/manifest.js b/public/js/manifest.js index 7380d20eb..e980ed49a 100644 Binary files a/public/js/manifest.js and b/public/js/manifest.js differ diff --git a/public/js/profile.chunk.4049e1eecea398ee.js b/public/js/profile.chunk.2fefc77fa8b9e0d3.js similarity index 68% rename from public/js/profile.chunk.4049e1eecea398ee.js rename to public/js/profile.chunk.2fefc77fa8b9e0d3.js index 2ef3482d3..16f9decb3 100644 Binary files a/public/js/profile.chunk.4049e1eecea398ee.js and b/public/js/profile.chunk.2fefc77fa8b9e0d3.js differ diff --git a/public/js/remote_auth.js b/public/js/remote_auth.js new file mode 100644 index 000000000..e4353ac70 Binary files /dev/null and b/public/js/remote_auth.js differ diff --git a/public/js/vendor.js b/public/js/vendor.js index e95bc6e95..c4633d457 100644 Binary files a/public/js/vendor.js and b/public/js/vendor.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 7874f175a..d23c311d1 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/assets/components/remote-auth/GettingStartedComponent.vue b/resources/assets/components/remote-auth/GettingStartedComponent.vue new file mode 100644 index 000000000..241730fe8 --- /dev/null +++ b/resources/assets/components/remote-auth/GettingStartedComponent.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/resources/assets/components/remote-auth/StartComponent.vue b/resources/assets/components/remote-auth/StartComponent.vue new file mode 100644 index 000000000..b8b096e1d --- /dev/null +++ b/resources/assets/components/remote-auth/StartComponent.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/resources/assets/js/remote_auth.js b/resources/assets/js/remote_auth.js new file mode 100644 index 000000000..a852e2d5f --- /dev/null +++ b/resources/assets/js/remote_auth.js @@ -0,0 +1,9 @@ +Vue.component( + 'remote-auth-start-component', + require('./../components/remote-auth/StartComponent.vue').default +); + +Vue.component( + 'remote-auth-getting-started-component', + require('./../components/remote-auth/GettingStartedComponent.vue').default +); diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 43caeb6dd..12b6b6f52 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -41,7 +41,7 @@
@endif -
+
+ + @if(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled')) +
+
+ @csrf +
+
+ +
+
+
+ @endif
diff --git a/resources/views/auth/remote/onboarding.blade.php b/resources/views/auth/remote/onboarding.blade.php new file mode 100644 index 000000000..82212a75d --- /dev/null +++ b/resources/views/auth/remote/onboarding.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/auth/remote/start.blade.php b/resources/views/auth/remote/start.blade.php new file mode 100644 index 000000000..9369d7f73 --- /dev/null +++ b/resources/views/auth/remote/start.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/routes/web.php b/routes/web.php index 200f65744..bd4d978bf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -174,6 +174,25 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('web/explore', 'LandingController@exploreRedirect'); Auth::routes(); + Route::get('auth/raw/mastodon/start', 'RemoteAuthController@startRedirect'); + Route::post('auth/raw/mastodon/config', 'RemoteAuthController@getConfig'); + Route::post('auth/raw/mastodon/domains', 'RemoteAuthController@getAuthDomains'); + Route::post('auth/raw/mastodon/start', 'RemoteAuthController@start'); + Route::post('auth/raw/mastodon/redirect', 'RemoteAuthController@redirect'); + Route::get('auth/raw/mastodon/preflight', 'RemoteAuthController@preflight'); + Route::get('auth/mastodon/callback', 'RemoteAuthController@handleCallback'); + Route::get('auth/mastodon/getting-started', 'RemoteAuthController@onboarding'); + Route::post('auth/raw/mastodon/s/check', 'RemoteAuthController@sessionCheck'); + Route::post('auth/raw/mastodon/s/prefill', 'RemoteAuthController@sessionGetMastodonData'); + Route::post('auth/raw/mastodon/s/username-check', 'RemoteAuthController@sessionValidateUsername'); + Route::post('auth/raw/mastodon/s/email-check', 'RemoteAuthController@sessionValidateEmail'); + Route::post('auth/raw/mastodon/s/following', 'RemoteAuthController@sessionGetMastodonFollowers'); + Route::post('auth/raw/mastodon/s/submit', 'RemoteAuthController@handleSubmit'); + Route::post('auth/raw/mastodon/s/store-bio', 'RemoteAuthController@storeBio'); + Route::post('auth/raw/mastodon/s/store-avatar', 'RemoteAuthController@storeAvatar'); + Route::post('auth/raw/mastodon/s/account-to-id', 'RemoteAuthController@accountToId'); + Route::post('auth/raw/mastodon/s/finish-up', 'RemoteAuthController@finishUp'); + Route::post('auth/raw/mastodon/s/login', 'RemoteAuthController@handleLogin'); Route::get('discover', 'DiscoverController@home')->name('discover'); diff --git a/webpack.mix.js b/webpack.mix.js index 14e75b313..8d78b82c8 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -37,6 +37,7 @@ mix.js('resources/assets/js/app.js', 'public/js') .js('resources/assets/js/account-import.js', 'public/js') .js('resources/assets/js/admin_invite.js', 'public/js') .js('resources/assets/js/landing.js', 'public/js') +.js('resources/assets/js/remote_auth.js', 'public/js') .vue({ version: 2 }); mix.extract();