mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 14:31:26 +00:00
Add Sign-in with Mastodon
This commit is contained in:
parent
ce02f05718
commit
45b9404ec1
11 changed files with 742 additions and 7 deletions
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,13 +31,7 @@ class User extends Authenticatable
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name', 'username', 'email', 'password', 'app_register_ip', 'email_verified_at', 'register_source'
|
||||||
'username',
|
|
||||||
'email',
|
|
||||||
'password',
|
|
||||||
'app_register_ip',
|
|
||||||
'email_verified_at',
|
|
||||||
'last_active_at'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
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', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
};
|
|
@ -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>
|
113
resources/assets/components/remote-auth/StartComponent.vue
Normal file
113
resources/assets/components/remote-auth/StartComponent.vue
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<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)">
|
||||||
|
Sign-in with <span class="font-weight-bold">{{ domain }}</span>
|
||||||
|
</button>
|
||||||
|
<hr>
|
||||||
|
<p class="text-center">
|
||||||
|
<button type="button" class="other-server-btn">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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
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
|
Loading…
Reference in a new issue