mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-20 03:53:17 +00:00
commit
8911ace102
22 changed files with 1377 additions and 3 deletions
|
@ -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))
|
||||
|
|
568
app/Http/Controllers/RemoteAuthController.php
Normal file
568
app/Http/Controllers/RemoteAuthController.php
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
19
app/Models/RemoteAuth.php
Normal file
19
app/Models/RemoteAuth.php
Normal file
|
@ -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'
|
||||
];
|
||||
}
|
13
app/Models/RemoteAuthInstance.php
Normal file
13
app/Models/RemoteAuthInstance.php
Normal file
|
@ -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 = [];
|
||||
}
|
183
app/Services/Account/RemoteAuthService.php
Normal file
183
app/Services/Account/RemoteAuthService.php
Normal file
|
@ -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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
56
config/remote-auth.php
Normal file
56
config/remote-auth.php
Normal file
|
@ -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');
|
||||
}
|
||||
};
|
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
Binary file not shown.
BIN
public/js/remote_auth.js
vendored
Normal file
BIN
public/js/remote_auth.js
vendored
Normal file
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -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>
|
133
resources/assets/components/remote-auth/StartComponent.vue
Normal file
133
resources/assets/components/remote-auth/StartComponent.vue
Normal file
|
@ -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>
|
9
resources/assets/js/remote_auth.js
vendored
Normal file
9
resources/assets/js/remote_auth.js
vendored
Normal file
|
@ -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>
|
||||
|
||||
|
|
10
resources/views/auth/remote/onboarding.blade.php
Normal file
10
resources/views/auth/remote/onboarding.blade.php
Normal file
|
@ -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
|
10
resources/views/auth/remote/start.blade.php
Normal file
10
resources/views/auth/remote/start.blade.php
Normal file
|
@ -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');
|
||||
|
||||
|
|
1
webpack.mix.js
vendored
1
webpack.mix.js
vendored
|
@ -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 a new issue