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 @@
+
+ Sign-in with Mastodon Please wait...
+ Go back to login
+ Sign-in with Mastodon Oops! We cannot complete your request at this time It appears that you've signed-in on other Pixelfed instances and reached the max limit that we accept. Sign-in with Mastodon Welcome back! One moment please, we're logging you in... Sign-in with Mastodon Oops, something went wrong! We cannot complete your request at this time, please try again later. This can happen for a few different reasons:
+ Go back to login
+ Sign-in with Mastodon
+
+
+
+
+
+
+
+
+