mirror of https://github.com/pixelfed/pixelfed.git
commit
8911ace102
|
@ -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))
|
||||
|
|
|
@ -0,0 +1,568 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Account\RemoteAuthService;
|
||||
use App\Models\RemoteAuth;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class RemoteAuthController extends Controller
|
||||
{
|
||||
public function start(Request $request)
|
||||
{
|
||||
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
|
||||
if($request->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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RemoteAuth extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'verify_credentials' => 'array',
|
||||
'last_successful_login_at' => 'datetime',
|
||||
'last_verify_credentials_at' => 'datetime'
|
||||
];
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RemoteAuthInstance extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Account;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Models\RemoteAuthInstance;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
class RemoteAuthService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:remoteauth:';
|
||||
|
||||
public static function getMastodonClient($domain)
|
||||
{
|
||||
if(RemoteAuthInstance::whereDomain($domain)->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;
|
||||
}
|
||||
}
|
|
@ -37,7 +37,8 @@ class User extends Authenticatable
|
|||
'password',
|
||||
'app_register_ip',
|
||||
'email_verified_at',
|
||||
'last_active_at'
|
||||
'last_active_at',
|
||||
'register_source'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'mastodon' => [
|
||||
'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)
|
||||
]
|
||||
],
|
||||
];
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('remote_auths', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('remote_auth_instances', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -1 +1 @@
|
|||
(()=>{"use strict";var e,r,n,o={},t={};function c(e){var r=t[e];if(void 0!==r)return r.exports;var n=t[e]={id:e,loaded:!1,exports:{}};return o[e].call(n.exports,n,n.exports,c),n.loaded=!0,n.exports}c.m=o,e=[],c.O=(r,n,o,t)=>{if(!n){var d=1/0;for(f=0;f<e.length;f++){for(var[n,o,t]=e[f],i=!0,a=0;a<n.length;a++)(!1&t||d>=t)&&Object.keys(c.O).every((e=>c.O[e](n[a])))?n.splice(a--,1):(i=!1,t<d&&(d=t));if(i){e.splice(f--,1);var s=o();void 0!==s&&(r=s)}}return r}t=t||0;for(var f=e.length;f>0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[n,o,t]},c.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return c.d(r,{a:r}),r},c.d=(e,r)=>{for(var n in r)c.o(r,n)&&!c.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},c.f={},c.e=e=>Promise.all(Object.keys(c.f).reduce(((r,n)=>(c.f[n](e,r),r)),[])),c.u=e=>"js/"+{1084:"profile~followers.bundle",2470:"home.chunk",2530:"discover~myhashtags.chunk",2586:"compose.chunk",2732:"dms~message.chunk",3351:"discover~settings.chunk",3365:"dms.chunk",3623:"discover~findfriends.chunk",4028:"error404.bundle",4958:"discover.chunk",4965:"discover~memories.chunk",5865:"post.chunk",6053:"notifications.chunk",6869:"profile.chunk",7019:"discover~hashtag.bundle",8250:"i18n.bundle",8517:"daci.chunk",8600:"changelog.bundle",8625:"profile~following.bundle",8900:"discover~serverfeed.chunk"}[e]+"."+{1084:"f088062414c3b014",2470:"2d93b527d492e6de",2530:"70e91906f0ce857a",2586:"6464688bf5b5ef97",2732:"990c68dfc266b0cf",3351:"72cc15c7b87b662d",3365:"98e12cf9137ddd87",3623:"006f0079e9f5a3eb",4028:"182d0aaa2da9ed23",4958:"56d2d8cfbbecc761",4965:"4c0973f4400f25b4",5865:"cd535334efc77c34",6053:"bf0c641eb1fd9cde",6869:"4049e1eecea398ee",7019:"54f2ac43c55bf328",8250:"4a5ff18de549ac4e",8517:"914d307d69fcfcd4",8600:"c4c82057f9628c72",8625:"57cbb89efa73e324",8900:"017fd16f00c55e60"}[e]+".js",c.miniCssF=e=>({138:"css/spa",703:"css/admin",1242:"css/appdark",6170:"css/app",8737:"css/portfolio",9994:"css/landing"}[e]+".css"),c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},n="pixelfed:",c.l=(e,o,t,d)=>{if(r[e])r[e].push(o);else{var i,a;if(void 0!==t)for(var s=document.getElementsByTagName("script"),f=0;f<s.length;f++){var l=s[f];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==n+t){i=l;break}}i||(a=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,c.nc&&i.setAttribute("nonce",c.nc),i.setAttribute("data-webpack",n+t),i.src=e),r[e]=[o];var u=(n,o)=>{i.onerror=i.onload=null,clearTimeout(p);var t=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),t&&t.forEach((e=>e(o))),n)return n(o)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=u.bind(null,i.onerror),i.onload=u.bind(null,i.onload),a&&document.head.appendChild(i)}},c.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),c.p="/",(()=>{var e={8929:0,1242:0,6170:0,8737:0,703:0,9994:0,138:0};c.f.j=(r,n)=>{var o=c.o(e,r)?e[r]:void 0;if(0!==o)if(o)n.push(o[2]);else if(/^(1242|138|6170|703|8737|8929|9994)$/.test(r))e[r]=0;else{var t=new Promise(((n,t)=>o=e[r]=[n,t]));n.push(o[2]=t);var d=c.p+c.u(r),i=new Error;c.l(d,(n=>{if(c.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var t=n&&("load"===n.type?"missing":n.type),d=n&&n.target&&n.target.src;i.message="Loading chunk "+r+" failed.\n("+t+": "+d+")",i.name="ChunkLoadError",i.type=t,i.request=d,o[1](i)}}),"chunk-"+r,r)}},c.O.j=r=>0===e[r];var r=(r,n)=>{var o,t,[d,i,a]=n,s=0;if(d.some((r=>0!==e[r]))){for(o in i)c.o(i,o)&&(c.m[o]=i[o]);if(a)var f=a(c)}for(r&&r(n);s<d.length;s++)t=d[s],c.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return c.O(f)},n=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))})(),c.nc=void 0})();
|
||||
(()=>{"use strict";var e,r,n,o={},t={};function c(e){var r=t[e];if(void 0!==r)return r.exports;var n=t[e]={id:e,loaded:!1,exports:{}};return o[e].call(n.exports,n,n.exports,c),n.loaded=!0,n.exports}c.m=o,e=[],c.O=(r,n,o,t)=>{if(!n){var d=1/0;for(f=0;f<e.length;f++){for(var[n,o,t]=e[f],i=!0,a=0;a<n.length;a++)(!1&t||d>=t)&&Object.keys(c.O).every((e=>c.O[e](n[a])))?n.splice(a--,1):(i=!1,t<d&&(d=t));if(i){e.splice(f--,1);var s=o();void 0!==s&&(r=s)}}return r}t=t||0;for(var f=e.length;f>0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[n,o,t]},c.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return c.d(r,{a:r}),r},c.d=(e,r)=>{for(var n in r)c.o(r,n)&&!c.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},c.f={},c.e=e=>Promise.all(Object.keys(c.f).reduce(((r,n)=>(c.f[n](e,r),r)),[])),c.u=e=>"js/"+{1084:"profile~followers.bundle",2470:"home.chunk",2530:"discover~myhashtags.chunk",2586:"compose.chunk",2732:"dms~message.chunk",3351:"discover~settings.chunk",3365:"dms.chunk",3623:"discover~findfriends.chunk",4028:"error404.bundle",4958:"discover.chunk",4965:"discover~memories.chunk",5865:"post.chunk",6053:"notifications.chunk",6869:"profile.chunk",7019:"discover~hashtag.bundle",8250:"i18n.bundle",8517:"daci.chunk",8600:"changelog.bundle",8625:"profile~following.bundle",8900:"discover~serverfeed.chunk"}[e]+"."+{1084:"f088062414c3b014",2470:"2d93b527d492e6de",2530:"70e91906f0ce857a",2586:"6464688bf5b5ef97",2732:"990c68dfc266b0cf",3351:"72cc15c7b87b662d",3365:"98e12cf9137ddd87",3623:"006f0079e9f5a3eb",4028:"182d0aaa2da9ed23",4958:"56d2d8cfbbecc761",4965:"4c0973f4400f25b4",5865:"cd535334efc77c34",6053:"bf0c641eb1fd9cde",6869:"2fefc77fa8b9e0d3",7019:"54f2ac43c55bf328",8250:"4a5ff18de549ac4e",8517:"914d307d69fcfcd4",8600:"c4c82057f9628c72",8625:"57cbb89efa73e324",8900:"017fd16f00c55e60"}[e]+".js",c.miniCssF=e=>({138:"css/spa",703:"css/admin",1242:"css/appdark",6170:"css/app",8737:"css/portfolio",9994:"css/landing"}[e]+".css"),c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},n="pixelfed:",c.l=(e,o,t,d)=>{if(r[e])r[e].push(o);else{var i,a;if(void 0!==t)for(var s=document.getElementsByTagName("script"),f=0;f<s.length;f++){var l=s[f];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==n+t){i=l;break}}i||(a=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,c.nc&&i.setAttribute("nonce",c.nc),i.setAttribute("data-webpack",n+t),i.src=e),r[e]=[o];var u=(n,o)=>{i.onerror=i.onload=null,clearTimeout(b);var t=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),t&&t.forEach((e=>e(o))),n)return n(o)},b=setTimeout(u.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=u.bind(null,i.onerror),i.onload=u.bind(null,i.onload),a&&document.head.appendChild(i)}},c.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),c.p="/",(()=>{var e={8929:0,1242:0,6170:0,8737:0,703:0,9994:0,138:0};c.f.j=(r,n)=>{var o=c.o(e,r)?e[r]:void 0;if(0!==o)if(o)n.push(o[2]);else if(/^(1242|138|6170|703|8737|8929|9994)$/.test(r))e[r]=0;else{var t=new Promise(((n,t)=>o=e[r]=[n,t]));n.push(o[2]=t);var d=c.p+c.u(r),i=new Error;c.l(d,(n=>{if(c.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var t=n&&("load"===n.type?"missing":n.type),d=n&&n.target&&n.target.src;i.message="Loading chunk "+r+" failed.\n("+t+": "+d+")",i.name="ChunkLoadError",i.type=t,i.request=d,o[1](i)}}),"chunk-"+r,r)}},c.O.j=r=>0===e[r];var r=(r,n)=>{var o,t,[d,i,a]=n,s=0;if(d.some((r=>0!==e[r]))){for(o in i)c.o(i,o)&&(c.m[o]=i[o]);if(a)var f=a(c)}for(r&&r(n);s<d.length;s++)t=d[s],c.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return c.O(f)},n=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))})(),c.nc=void 0})();
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -23,11 +23,12 @@
|
|||
"/js/account-import.js": "/js/account-import.js?id=00ea9b73c8e17dff27e4a7767d9553d7",
|
||||
"/js/admin_invite.js": "/js/admin_invite.js?id=307a53250701e3b12164af9495e88447",
|
||||
"/js/landing.js": "/js/landing.js?id=7e3ab65813c4bf28182f5bdf0825774c",
|
||||
"/js/manifest.js": "/js/manifest.js?id=db562fad91ac15b1cf437809a28c5de7",
|
||||
"/js/remote_auth.js": "/js/remote_auth.js?id=55a409bf33c2f1e9950e4b83dc664a2a",
|
||||
"/js/manifest.js": "/js/manifest.js?id=1a9e092e7f4051b3ce0af3c638c8d2c5",
|
||||
"/js/home.chunk.2d93b527d492e6de.js": "/js/home.chunk.2d93b527d492e6de.js?id=809ef226cf2383e3a8973f65d4269d1c",
|
||||
"/js/compose.chunk.6464688bf5b5ef97.js": "/js/compose.chunk.6464688bf5b5ef97.js?id=45753d769a16276c2d3ad8d7d6bf3e88",
|
||||
"/js/post.chunk.cd535334efc77c34.js": "/js/post.chunk.cd535334efc77c34.js?id=73e7e49b1dbdb75d2a128d21e6e3e9f4",
|
||||
"/js/profile.chunk.4049e1eecea398ee.js": "/js/profile.chunk.4049e1eecea398ee.js?id=9caae6c4b8b3c0ab812a077ff82c3259",
|
||||
"/js/profile.chunk.2fefc77fa8b9e0d3.js": "/js/profile.chunk.2fefc77fa8b9e0d3.js?id=ff489b9c2880c30e5d3f993220b43305",
|
||||
"/js/discover~memories.chunk.4c0973f4400f25b4.js": "/js/discover~memories.chunk.4c0973f4400f25b4.js?id=6eb8a14fe9aa1d4fe3f0264022793b12",
|
||||
"/js/discover~myhashtags.chunk.70e91906f0ce857a.js": "/js/discover~myhashtags.chunk.70e91906f0ce857a.js?id=5639e162321efa8b13f23b125c632bab",
|
||||
"/js/daci.chunk.914d307d69fcfcd4.js": "/js/daci.chunk.914d307d69fcfcd4.js?id=c843c795b8551593eb19dffe1e08e694",
|
||||
|
@ -50,5 +51,5 @@
|
|||
"/css/admin.css": "/css/admin.css?id=0a66549bf79b75a0ca8cb83d11a4e2f4",
|
||||
"/css/landing.css": "/css/landing.css?id=589f3fa192867727925921b0f68ce022",
|
||||
"/css/spa.css": "/css/spa.css?id=f6bef1e343335ee2b5cf4e9fc074856f",
|
||||
"/js/vendor.js": "/js/vendor.js?id=6c95cc6034d7ff383f4fc8d47ffcf9a9"
|
||||
"/js/vendor.js": "/js/vendor.js?id=a0cc6867663084472494dceda20c3392"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
<template>
|
||||
<div class="container remote-auth-getting-started">
|
||||
<div class="row mt-5 justify-content-center">
|
||||
<div class="col-12 col-xl-5 col-md-7">
|
||||
<div v-if="!error" class="card shadow-none border" style="border-radius: 20px;">
|
||||
<div v-if="!loaded && !existing && !maxUsesReached" class="card-body d-flex align-items-center flex-column" style="min-height: 400px;">
|
||||
<div class="w-100">
|
||||
<p class="lead text-center font-weight-bold">Sign-in with Mastodon</p>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="w-100 d-flex align-items-center justify-content-center flex-grow-1 flex-column gap-1">
|
||||
<div class="position-relative w-100">
|
||||
<p class="pa-center">Please wait...</p>
|
||||
<instagram-loader></instagram-loader>
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<hr>
|
||||
<p class="text-center mb-0">
|
||||
<a class="font-weight-bold" href="/login">Go back to login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loaded && !existing && maxUsesReached" class="card-body d-flex align-items-center flex-column" style="min-height: 660px;">
|
||||
<div class="w-100">
|
||||
<p class="lead text-center font-weight-bold">Sign-in with Mastodon</p>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="w-100 d-flex align-items-center justify-content-center flex-grow-1 flex-column gap-1">
|
||||
|
||||
<p class="lead text-center font-weight-bold mt-3">Oops!</p>
|
||||
|
||||
<p class="mb-2 text-center">We cannot complete your request at this time</p>
|
||||
<p class="mb-3 text-center text-xs">It appears that you've signed-in on other Pixelfed instances and reached the max limit that we accept.</p>
|
||||
</div>
|
||||
|
||||
<div class="w-100">
|
||||
<p class="text-center mb-0">
|
||||
<a class="font-weight-bold" href="/site/contact">Contact Support</a>
|
||||
</p>
|
||||
<hr>
|
||||
<p class="text-center mb-0">
|
||||
<a class="font-weight-bold" href="/login">Go back to login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loaded && existing" class="card-body d-flex align-items-center flex-column" style="min-height: 660px;">
|
||||
<div class="w-100">
|
||||
<p class="lead text-center font-weight-bold">Sign-in with Mastodon</p>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="w-100 d-flex align-items-center justify-content-center flex-grow-1 flex-column gap-1">
|
||||
<b-spinner />
|
||||
<div class="text-center">
|
||||
<p class="lead mb-0">Welcome back!</p>
|
||||
<p class="text-xs text-muted">One moment please, we're logging you in...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<register-form v-else :initialData="prefill" v-on:setCanReload="setCanReload" />
|
||||
</div>
|
||||
<div v-else class="card shadow-none border">
|
||||
<div class="card-body d-flex align-items-center flex-column" style="min-height: 660px;">
|
||||
<div class="w-100">
|
||||
<p class="lead text-center font-weight-bold">Sign-in with Mastodon</p>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="w-100 d-flex align-items-center justify-content-center flex-grow-1 flex-column gap-1">
|
||||
|
||||
<p class="lead text-center font-weight-bold mt-3">Oops, something went wrong!</p>
|
||||
|
||||
<p class="mb-3">We cannot complete your request at this time, please try again later.</p>
|
||||
|
||||
<p class="text-xs text-muted mb-1">This can happen for a few different reasons:</p>
|
||||
|
||||
<ul class="text-xs text-muted">
|
||||
<li>The remote instance cannot be reached</li>
|
||||
<li>The remote instance is not supported yet</li>
|
||||
<li>The remote instance has been disabled by admins</li>
|
||||
<li>The remote instance does not allow remote logins</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="w-100">
|
||||
<hr>
|
||||
<p class="text-center mb-0">
|
||||
<a class="font-weight-bold" href="/login">Go back to login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import { InstagramLoader } from 'vue-content-loader';
|
||||
import RegisterForm from './partials/RegisterForm.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InstagramLoader,
|
||||
RegisterForm
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
error: false,
|
||||
prefill: false,
|
||||
existing: undefined,
|
||||
maxUsesReached: undefined,
|
||||
tab: 'loading',
|
||||
canReload: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.validateSession();
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
if(!this.canReload) {
|
||||
alert('You are trying to leave.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
validateSession() {
|
||||
axios.post('/auth/raw/mastodon/s/check')
|
||||
.then(res => {
|
||||
if(!res && !res.hasOwnProperty('action')) {
|
||||
swal('Oops!', 'An unexpected error occured, please try again later', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
switch(res.data.action) {
|
||||
case 'onboard':
|
||||
this.getPrefillData();
|
||||
return;
|
||||
break;
|
||||
|
||||
case 'redirect_existing_user':
|
||||
this.existing = true;
|
||||
this.canReload = true;
|
||||
window.onbeforeunload = undefined;
|
||||
this.redirectExistingUser();
|
||||
return;
|
||||
break;
|
||||
|
||||
case 'max_uses_reached':
|
||||
this.maxUsesReached = true;
|
||||
this.canReload = true;
|
||||
window.onbeforeunload = undefined;
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
this.error = true;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.canReload = true;
|
||||
window.onbeforeunload = undefined;
|
||||
this.error = true;
|
||||
})
|
||||
},
|
||||
|
||||
setCanReload() {
|
||||
this.canReload = true;
|
||||
window.onbeforeunload = undefined;
|
||||
},
|
||||
|
||||
redirectExistingUser() {
|
||||
this.canReload = true;
|
||||
setTimeout(() => {
|
||||
this.handleLogin();
|
||||
}, 1500);
|
||||
},
|
||||
|
||||
handleLogin() {
|
||||
axios.post('/auth/raw/mastodon/s/login')
|
||||
.then(res => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
this.canReload = false;
|
||||
this.error = true;
|
||||
})
|
||||
},
|
||||
|
||||
getPrefillData() {
|
||||
axios.post('/auth/raw/mastodon/s/prefill')
|
||||
.then(res => {
|
||||
this.prefill = res.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = true;
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.loaded = true;
|
||||
}, 1000);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../../../node_modules/bootstrap/scss/bootstrap';
|
||||
|
||||
.remote-auth-getting-started {
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.server-btn {
|
||||
@extend .btn;
|
||||
@extend .btn-primary;
|
||||
@extend .btn-block;
|
||||
@extend .rounded-pill;
|
||||
@extend .font-weight-light;
|
||||
|
||||
background: linear-gradient(#6364FF, #563ACC);
|
||||
}
|
||||
|
||||
.other-server-btn {
|
||||
@extend .btn;
|
||||
@extend .btn-dark;
|
||||
@extend .btn-block;
|
||||
@extend .rounded-pill;
|
||||
@extend .font-weight-light;
|
||||
}
|
||||
|
||||
.pa-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div class="container remote-auth-start">
|
||||
<div class="row mt-5 justify-content-center">
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="card shadow-none border" style="border-radius: 20px;">
|
||||
<div v-if="!loaded" class="card-body d-flex justify-content-center flex-column" style="min-height: 662px;">
|
||||
<p class="lead text-center font-weight-bold mb-0">Sign-in with Mastodon</p>
|
||||
<div class="w-100">
|
||||
<hr>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center align-items-center flex-grow-1">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="card-body" style="min-height: 662px;">
|
||||
<p class="lead text-center font-weight-bold">Sign-in with Mastodon</p>
|
||||
<hr>
|
||||
<p class="small text-center mb-3">Select your Mastodon server:</p>
|
||||
<button
|
||||
v-for="domain in domains"
|
||||
type="button"
|
||||
class="server-btn"
|
||||
@click="handleRedirect(domain)">
|
||||
<span class="font-weight-bold">{{ domain }}</span>
|
||||
</button>
|
||||
<hr>
|
||||
<p class="text-center">
|
||||
<button type="button" class="other-server-btn" @click="handleOther()">Sign-in with a different server</button>
|
||||
</p>
|
||||
<div class="w-100">
|
||||
<hr>
|
||||
<p class="text-center mb-0">
|
||||
<a class="font-weight-bold" href="/login">Go back to login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
domains: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchDomains();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchDomains() {
|
||||
axios.post('/auth/raw/mastodon/domains')
|
||||
.then(res => {
|
||||
this.domains = res.data;
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.loaded = true;
|
||||
}, 500);
|
||||
})
|
||||
},
|
||||
|
||||
handleRedirect(domain) {
|
||||
axios.post('/auth/raw/mastodon/redirect', { domain: domain })
|
||||
.then(res => {
|
||||
if(!res || !res.data.hasOwnProperty('ready')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(res.data.hasOwnProperty('action') && res.data.action === 'incompatible_domain') {
|
||||
swal('Oops!', 'This server is not compatible, please choose another or try again later!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(res.data.ready) {
|
||||
window.location.href = '/auth/raw/mastodon/preflight?d=' + domain + '&dsh=' + res.data.dsh;
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleOther() {
|
||||
swal({
|
||||
text: 'Enter your mastodon domain (without https://)',
|
||||
content: "input",
|
||||
button: {
|
||||
text: "Next",
|
||||
closeModal: false,
|
||||
},
|
||||
})
|
||||
.then(domain => {
|
||||
if (!domain) throw null;
|
||||
|
||||
if(domain.startsWith('https://')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.handleRedirect(domain);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../../../node_modules/bootstrap/scss/bootstrap';
|
||||
|
||||
.remote-auth-start {
|
||||
.server-btn {
|
||||
@extend .btn;
|
||||
@extend .btn-primary;
|
||||
@extend .btn-block;
|
||||
@extend .rounded-pill;
|
||||
@extend .font-weight-light;
|
||||
|
||||
background: linear-gradient(#6364FF, #563ACC);
|
||||
}
|
||||
|
||||
.other-server-btn {
|
||||
@extend .btn;
|
||||
@extend .btn-dark;
|
||||
@extend .btn-block;
|
||||
@extend .rounded-pill;
|
||||
@extend .font-weight-light;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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
|
||||
);
|
|
@ -41,7 +41,7 @@
|
|||
<div class="col-md-12">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
<span class="font-weight-bold ml-1 text-muted">
|
||||
{{ __('Remember Me') }}
|
||||
</span>
|
||||
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group row mb-0">
|
||||
<div class="form-group row mb-4">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg font-weight-bold">
|
||||
{{ __('Login') }}
|
||||
|
@ -72,7 +72,21 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@if(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'))
|
||||
<hr>
|
||||
<form method="POST" action="/auth/raw/mastodon/start">
|
||||
@csrf
|
||||
<div class="form-group row mb-0">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" class="btn btn-primary btn-sm btn-block rounded-pill font-weight-bold" style="background: linear-gradient(#6364FF, #563ACC);">
|
||||
Sign-in with Mastodon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<hr>
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<remote-auth-getting-started-component />
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/remote_auth.js')}}"></script>
|
||||
<script type="text/javascript">App.boot();</script>
|
||||
@endpush
|
|
@ -0,0 +1,10 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<remote-auth-start-component />
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/remote_auth.js')}}"></script>
|
||||
<script type="text/javascript">App.boot();</script>
|
||||
@endpush
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue