mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-22 13:03:16 +00:00
Merge pull request #334 from dansup/frontend-ui-refactor
Frontend ui refactor
This commit is contained in:
commit
56750833e9
49 changed files with 2895 additions and 312 deletions
10
app/AccountLog.php
Normal file
10
app/AccountLog.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AccountLog extends Model
|
||||
{
|
||||
//
|
||||
}
|
36
app/Events/AuthLoginEvent.php
Normal file
36
app/Events/AuthLoginEvent.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use App\{User, UserSetting};
|
||||
|
||||
class AuthLoginEvent
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function handle(User $user)
|
||||
{
|
||||
if(empty($user->settings)) {
|
||||
$settings = new UserSetting;
|
||||
$settings->user_id = $user->id;
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Hashtag extends Model
|
||||
{
|
||||
protected $fillable = ['name','slug'];
|
||||
public $fillable = ['name','slug'];
|
||||
|
||||
public function posts()
|
||||
{
|
||||
|
|
|
@ -18,15 +18,24 @@ class AccountController extends Controller
|
|||
public function notifications(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3'
|
||||
'page' => 'nullable|min:1|max:3',
|
||||
'a' => 'nullable|alpha_dash',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$action = $request->input('a');
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->take(30)
|
||||
->simplePaginate();
|
||||
if($action && in_array($action, ['comment', 'follow', 'mention'])) {
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereAction($action)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->simplePaginate(30);
|
||||
} else {
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->simplePaginate(30);
|
||||
}
|
||||
|
||||
return view('account.activity', compact('profile', 'notifications'));
|
||||
}
|
||||
|
@ -38,10 +47,19 @@ class AccountController extends Controller
|
|||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
{
|
||||
if(EmailVerification::whereUserId(Auth::id())->count() !== 0) {
|
||||
return redirect()->back()->with('status', 'A verification email has already been sent! Please check your email.');
|
||||
$timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
|
||||
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||
->where('created_at', '>', $timeLimit)->count();
|
||||
$exists = EmailVerification::whereUserId(Auth::id())->count();
|
||||
|
||||
if($recentAttempt == 1 && $exists == 1) {
|
||||
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||
} elseif ($recentAttempt == 0 && $exists !== 0) {
|
||||
// Delete old verification and send new one.
|
||||
EmailVerification::whereUserId(Auth::id())->delete();
|
||||
}
|
||||
|
||||
|
||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||
$utoken = hash('sha512', $user->id);
|
||||
$rtoken = str_random(40);
|
||||
|
@ -60,14 +78,15 @@ class AccountController extends Controller
|
|||
|
||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||
{
|
||||
$verify = EmailVerification::where(DB::raw('BINARY user_token'), $userToken)
|
||||
->where(DB::raw('BINARY random_token'), $randomToken)
|
||||
$verify = EmailVerification::where('user_token', $userToken)
|
||||
->where('random_token', $randomToken)
|
||||
->firstOrFail();
|
||||
|
||||
if(Auth::id() === $verify->user_id) {
|
||||
$user = User::find(Auth::id());
|
||||
$user->email_verified_at = Carbon::now();
|
||||
$user->save();
|
||||
return redirect('/timeline');
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,4 +114,5 @@ class AccountController extends Controller
|
|||
}
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\{AccountLog, User};
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
|
||||
|
@ -25,7 +26,7 @@ class LoginController extends Controller
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/home';
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
@ -56,4 +57,25 @@ class LoginController extends Controller
|
|||
|
||||
$this->validate($request, $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $user
|
||||
* @return mixed
|
||||
*/
|
||||
protected function authenticated($request, $user)
|
||||
{
|
||||
$log = new AccountLog;
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'auth.login';
|
||||
$log->message = 'Account Login';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImportDataController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Hashids;
|
||||
use Auth, Cache, Hashids;
|
||||
use App\{Like, Profile, Status, User};
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
|
||||
|
@ -27,7 +27,7 @@ class LikeController extends Controller
|
|||
|
||||
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
$like->delete();
|
||||
$like->forceDelete();
|
||||
$count--;
|
||||
} else {
|
||||
$like = new Like;
|
||||
|
@ -35,9 +35,15 @@ class LikeController extends Controller
|
|||
$like->status_id = $status->id;
|
||||
$like->save();
|
||||
$count++;
|
||||
LikePipeline::dispatch($like);
|
||||
}
|
||||
|
||||
LikePipeline::dispatch($like);
|
||||
$likes = Like::whereProfileId($profile->id)
|
||||
->orderBy('id', 'desc')
|
||||
->take(1000)
|
||||
->pluck('status_id');
|
||||
|
||||
Cache::put('api:like-ids:user:'.$profile->id, $likes, 1440);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
|
|
@ -21,7 +21,6 @@ class ProfileController extends Controller
|
|||
|
||||
$mimes = [
|
||||
'application/activity+json',
|
||||
'application/ld+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
|
||||
|
@ -36,7 +35,7 @@ class ProfileController extends Controller
|
|||
$timeline = $user->statuses()
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->orderBy('id','desc')
|
||||
->orderBy('created_at','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(21);
|
||||
|
||||
|
|
|
@ -2,11 +2,39 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use App, Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Follower, Status, User};
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
|
||||
public function home()
|
||||
{
|
||||
if(Auth::check()) {
|
||||
return $this->homeTimeline();
|
||||
} else {
|
||||
return $this->homeGuest();
|
||||
}
|
||||
}
|
||||
|
||||
public function homeGuest()
|
||||
{
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function homeTimeline()
|
||||
{
|
||||
// TODO: Use redis for timelines
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||
$following->push(Auth::user()->profile->id);
|
||||
$timeline = Status::whereIn('profile_id', $following)
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes', 'shares'])
|
||||
->simplePaginate(10);
|
||||
return view('timeline.template', compact('timeline'));
|
||||
}
|
||||
|
||||
public function changeLocale(Request $request, $locale)
|
||||
{
|
||||
if(!App::isLocale($locale)) {
|
||||
|
|
|
@ -18,7 +18,7 @@ class EmailVerificationCheck
|
|||
if($request->user() &&
|
||||
config('pixelfed.enforce_email_verification') &&
|
||||
is_null($request->user()->email_verified_at) &&
|
||||
!$request->is('i/verify-email') && !$request->is('login') &&
|
||||
!$request->is('i/verify-email') && !$request->is('log*') &&
|
||||
!$request->is('i/confirm-email/*')
|
||||
) {
|
||||
return redirect('/i/verify-email');
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace App;
|
|||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Report extends Model
|
||||
class ImportJob extends Model
|
||||
{
|
||||
//
|
||||
}
|
43
app/Jobs/InboxPipeline/InboxWorker.php
Normal file
43
app/Jobs/InboxPipeline/InboxWorker.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Inbox;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class InboxWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $request;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($request, Profile $profile, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->profile = $profile;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->request, $this->profile, $this->payload))->handle();
|
||||
}
|
||||
|
||||
}
|
41
app/Jobs/InboxPipeline/SharedInboxWorker.php
Normal file
41
app/Jobs/InboxPipeline/SharedInboxWorker.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Inbox;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class SharedInboxWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $request;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($request, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->request, null, $this->payload))->handleSharedInbox();
|
||||
}
|
||||
}
|
|
@ -37,6 +37,11 @@ class LikePipeline implements ShouldQueue
|
|||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if($status->url !== null) {
|
||||
// Ignore notifications to remote statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Cache;
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
Hashtag,
|
||||
Media,
|
||||
|
@ -68,12 +68,14 @@ class StatusEntityLexer implements ShouldQueue
|
|||
|
||||
public function storeEntities()
|
||||
{
|
||||
$status = $this->status;
|
||||
$this->storeHashtags();
|
||||
$this->storeMentions();
|
||||
$status->rendered = $this->autolink;
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
DB::transaction(function () {
|
||||
$status = $this->status;
|
||||
$status->rendered = $this->autolink;
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
});
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
|
@ -82,17 +84,15 @@ class StatusEntityLexer implements ShouldQueue
|
|||
$status = $this->status;
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$slug = str_slug($tag);
|
||||
|
||||
$htag = Hashtag::firstOrCreate(
|
||||
['name' => $tag],
|
||||
['slug' => $slug]
|
||||
);
|
||||
|
||||
StatusHashtag::firstOrCreate(
|
||||
['status_id' => $status->id],
|
||||
['hashtag_id' => $htag->id]
|
||||
);
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$slug = str_slug($tag);
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
['name' => $tag, 'slug' => $slug]
|
||||
);
|
||||
StatusHashtag::firstOrCreate(
|
||||
['status_id' => $status->id, 'hashtag_id' => $hashtag->id]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,16 +102,18 @@ class StatusEntityLexer implements ShouldQueue
|
|||
$status = $this->status;
|
||||
|
||||
foreach($mentions as $mention) {
|
||||
$mentioned = Profile::whereUsername($mention)->first();
|
||||
$mentioned = Profile::whereUsername($mention)->firstOrFail();
|
||||
|
||||
if(empty($mentioned) || !isset($mentioned->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$m = new Mention;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
|
||||
DB::transaction(function () use ($status, $mentioned) {
|
||||
$m = new Mention;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
});
|
||||
|
||||
MentionPipeline::dispatch($status, $m);
|
||||
}
|
||||
|
|
|
@ -23,4 +23,11 @@ class Media extends Model
|
|||
$url = Storage::url($path);
|
||||
return url($url);
|
||||
}
|
||||
|
||||
public function thumbnailUrl()
|
||||
{
|
||||
$path = $this->thumbnail_path;
|
||||
$url = Storage::url($path);
|
||||
return url($url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\{Profile, User};
|
||||
use App\{Profile, User, UserSetting};
|
||||
use App\Jobs\AvatarPipeline\CreateAvatar;
|
||||
|
||||
class UserObserver
|
||||
|
@ -36,6 +36,12 @@ class UserObserver
|
|||
|
||||
CreateAvatar::dispatch($profile);
|
||||
}
|
||||
|
||||
if(empty($user->settings)) {
|
||||
$settings = new UserSetting;
|
||||
$settings->user_id = $user->id;
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -29,6 +29,15 @@ class Profile extends Model
|
|||
}
|
||||
|
||||
public function url($suffix = '')
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return $this->remote_url;
|
||||
} else {
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
}
|
||||
|
||||
public function localUrl($suffix = '')
|
||||
{
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
|
@ -124,4 +133,9 @@ class Profile extends Model
|
|||
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function statusCount()
|
||||
{
|
||||
return $this->statuses()->whereHas('media')->count();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ class EventServiceProvider extends ServiceProvider
|
|||
'App\Events\Event' => [
|
||||
'App\Listeners\EventListener',
|
||||
],
|
||||
'auth.login' => [
|
||||
'App\Events\AuthLoginEvent',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
10
app/ReportComment.php
Normal file
10
app/ReportComment.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReportComment extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/ReportLog.php
Normal file
10
app/ReportLog.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReportLog extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Storage;
|
||||
use Auth, Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
@ -71,15 +71,39 @@ class Status extends Model
|
|||
return $this->hasMany(Like::class);
|
||||
}
|
||||
|
||||
public function liked() : bool
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(Status::class, 'in_reply_to_id');
|
||||
}
|
||||
|
||||
public function bookmarked()
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
public function shares()
|
||||
{
|
||||
return $this->hasMany(Status::class, 'reblog_of_id');
|
||||
}
|
||||
|
||||
public function shared() : bool
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Status::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
if(!empty($this->in_reply_to_id)) {
|
||||
return Status::findOrFail($this->in_reply_to_id);
|
||||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if(!empty($parent)) {
|
||||
return Status::findOrFail($parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,6 +124,23 @@ class Status extends Model
|
|||
);
|
||||
}
|
||||
|
||||
public function mentions()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Mention::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function reportUrl()
|
||||
{
|
||||
return route('report.form') . "?type=post&id={$this->id}";
|
||||
}
|
||||
|
||||
public function toActivityStream()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
|
|
@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class StatusHashtag extends Model
|
||||
{
|
||||
protected $fillable = ['status_id', 'hashtag_id'];
|
||||
public $fillable = ['status_id', 'hashtag_id'];
|
||||
}
|
||||
|
|
33
app/Transformer/Api/AccountTransformer.php
Normal file
33
app/Transformer/Api/AccountTransformer.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
|
||||
class AccountTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
return [
|
||||
'id' => $profile->id,
|
||||
'username' => $profile->username,
|
||||
'acct' => $profile->username,
|
||||
'display_name' => $profile->name,
|
||||
'locked' => (bool) $profile->is_private,
|
||||
'created_at' => $profile->created_at->format('c'),
|
||||
'followers_count' => $profile->followerCount(),
|
||||
'following_count' => $profile->followingCount(),
|
||||
'statuses_count' => $profile->statusCount(),
|
||||
'note' => $profile->bio,
|
||||
'url' => $profile->url(),
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'avatar_static' => $profile->avatarUrl(),
|
||||
'header' => '',
|
||||
'header_static' => '',
|
||||
'moved' => null,
|
||||
'fields' => null,
|
||||
'bot' => null
|
||||
];
|
||||
}
|
||||
}
|
16
app/Transformer/Api/ApplicationTransformer.php
Normal file
16
app/Transformer/Api/ApplicationTransformer.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use League\Fractal;
|
||||
|
||||
class ApplicationTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform()
|
||||
{
|
||||
return [
|
||||
'name' => '',
|
||||
'website' => null
|
||||
];
|
||||
}
|
||||
}
|
18
app/Transformer/Api/HashtagTransformer.php
Normal file
18
app/Transformer/Api/HashtagTransformer.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Hashtag;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class HashtagTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Hashtag $hashtag)
|
||||
{
|
||||
return [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url(),
|
||||
];
|
||||
}
|
||||
}
|
24
app/Transformer/Api/MediaTransformer.php
Normal file
24
app/Transformer/Api/MediaTransformer.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Media;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class MediaTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Media $media)
|
||||
{
|
||||
return [
|
||||
'id' => $media->id,
|
||||
'type' => 'image',
|
||||
'url' => $media->url(),
|
||||
'remote_url' => null,
|
||||
'preview_url' => $media->thumbnailUrl(),
|
||||
'text_url' => null,
|
||||
'meta' => null,
|
||||
'description' => null
|
||||
];
|
||||
}
|
||||
}
|
19
app/Transformer/Api/MentionTransformer.php
Normal file
19
app/Transformer/Api/MentionTransformer.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
|
||||
class MentionTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
return [
|
||||
'id' => $profile->id,
|
||||
'url' => $profile->url(),
|
||||
'username' => $profile->username,
|
||||
'acct' => $profile->username,
|
||||
];
|
||||
}
|
||||
}
|
69
app/Transformer/Api/StatusTransformer.php
Normal file
69
app/Transformer/Api/StatusTransformer.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
|
||||
class StatusTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'mentions',
|
||||
'media_attachments',
|
||||
'tags'
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'id' => $status->id,
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
|
||||
// TODO: fixme
|
||||
'reblog' => null,
|
||||
|
||||
'content' => "<p>$status->rendered</p>",
|
||||
'created_at' => $status->created_at->format('c'),
|
||||
'emojis' => [],
|
||||
'reblogs_count' => $status->shares()->count(),
|
||||
'favourites_count' => $status->likes()->count(),
|
||||
'reblogged' => $status->shared(),
|
||||
'favourited' => $status->liked(),
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => '',
|
||||
'visibility' => $status->visibility,
|
||||
'application' => null,
|
||||
'language' => null,
|
||||
'pinned' => null
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
return $this->item($account, new AccountTransformer);
|
||||
}
|
||||
|
||||
public function includeMentions(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions;
|
||||
return $this->collection($mentions, new MentionTransformer);
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
$media = $status->media;
|
||||
return $this->collection($media, new MediaTransformer);
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
return $this->collection($tags, new HashtagTransformer);
|
||||
}
|
||||
}
|
10
app/UserFilter.php
Normal file
10
app/UserFilter.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserFilter extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/UserSetting.php
Normal file
10
app/UserSetting.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserSetting extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"99designs/http-signatures-guzzlehttp": "^2.0",
|
||||
"beyondcode/laravel-self-diagnosis": "^0.4.0",
|
||||
"bitverse/identicon": "^1.1",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"fideloper/proxy": "^4.0",
|
||||
|
@ -15,9 +16,13 @@
|
|||
"kitetail/zttp": "^0.3.0",
|
||||
"laravel/framework": "5.6.*",
|
||||
"laravel/horizon": "^1.2",
|
||||
"laravel/passport": "^6.0",
|
||||
"laravel/tinker": "^1.0",
|
||||
"league/fractal": "^0.17.0",
|
||||
"moontoast/math": "^1.1",
|
||||
"phpseclib/phpseclib": "~2.0",
|
||||
"pixelfed/dotenv-editor": "^2.0",
|
||||
"pixelfed/fractal": "^0.18.0",
|
||||
"pixelfed/google2fa-laravel": "^2.0",
|
||||
"predis/predis": "^1.1",
|
||||
"spatie/laravel-backup": "^5.0.0",
|
||||
"spatie/laravel-image-optimizer": "^1.1",
|
||||
|
@ -25,6 +30,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.1",
|
||||
"beyondcode/laravel-er-diagram-generator": "^0.2.2",
|
||||
"filp/whoops": "^2.0",
|
||||
"fzaninotto/faker": "^1.4",
|
||||
"mockery/mockery": "^1.0",
|
||||
|
|
1457
composer.lock
generated
1457
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -49,5 +49,5 @@ return [
|
|||
* If set to `true` all output of the optimizer binaries will be appended to the default log.
|
||||
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
|
||||
*/
|
||||
'log_optimizer_activity' => true,
|
||||
'log_optimizer_activity' => false,
|
||||
];
|
||||
|
|
|
@ -77,6 +77,17 @@ return [
|
|||
|
||||
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Account file size limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Update the max account size, the per user limit of files in kb.
|
||||
| This applies to local and remote users. Old remote posts may be GC.
|
||||
|
|
||||
*/
|
||||
'max_account_size' => env('MAX_ACCOUNT_SIZE', 100000),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Photo file size limit
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateWebSubsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('web_subs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('follower_id')->unsigned()->index();
|
||||
$table->bigInteger('following_id')->unsigned()->index();
|
||||
$table->string('profile_url')->index();
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->unique(['follower_id', 'following_id', 'profile_url']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_subs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateImportJobsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('import_jobs', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->string('service')->default('instagram');
|
||||
$table->string('uuid')->nullable();
|
||||
$table->string('storage_path')->nullable();
|
||||
$table->tinyInteger('stage')->unsigned()->default(0);
|
||||
$table->text('media_json')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('import_jobs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateReportCommentsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_comments', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('report_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->bigInteger('user_id')->unsigned();
|
||||
$table->text('comment');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_comments');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateReportLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_logs', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->bigInteger('item_id')->unsigned()->nullable();
|
||||
$table->string('item_type')->nullable();
|
||||
$table->string('action')->nullable();
|
||||
$table->boolean('system_message')->default(false);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_logs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateAccountLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('account_logs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->index();
|
||||
$table->bigInteger('item_id')->unsigned()->nullable();
|
||||
$table->string('item_type')->nullable();
|
||||
$table->string('action')->nullable();
|
||||
$table->string('message')->nullable();
|
||||
$table->string('link')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('user_agent')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('account_logs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateUserSettingsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_settings', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->unique();
|
||||
$table->string('role')->default('user');
|
||||
$table->boolean('crawlable')->default(true);
|
||||
$table->boolean('show_guests')->default(true);
|
||||
$table->boolean('show_discover')->default(true);
|
||||
$table->boolean('public_dm')->default(false);
|
||||
$table->boolean('hide_cw_search')->default(true);
|
||||
$table->boolean('hide_blocked_search')->default(true);
|
||||
$table->boolean('always_show_cw')->default(false);
|
||||
$table->boolean('compose_media_descriptions')->default(false);
|
||||
$table->boolean('reduce_motion')->default(false);
|
||||
$table->boolean('optimize_screen_reader')->default(false);
|
||||
$table->boolean('high_contrast_mode')->default(false);
|
||||
$table->boolean('video_autoplay')->default(false);
|
||||
$table->boolean('send_email_new_follower')->default(false);
|
||||
$table->boolean('send_email_new_follower_request')->default(true);
|
||||
$table->boolean('send_email_on_share')->default(false);
|
||||
$table->boolean('send_email_on_like')->default(false);
|
||||
$table->boolean('send_email_on_mention')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_settings');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class Add2faToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('2fa_enabled')->default(false);
|
||||
$table->string('2fa_secret')->nullable();
|
||||
$table->json('2fa_backup_codes')->nullable();
|
||||
$table->timestamp('2fa_setup_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('2fa_enabled');
|
||||
$table->dropColumn('2fa_secret');
|
||||
$table->dropColumn('2fa_backup_codes');
|
||||
$table->dropColumn('2fa_setup_at');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateUserFiltersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_filters', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->index();
|
||||
$table->bigInteger('filterable_id')->unsigned();
|
||||
$table->string('filterable_type');
|
||||
$table->string('filter_type')->default('block')->index();
|
||||
$table->unique([
|
||||
'user_id',
|
||||
'filterable_id',
|
||||
'filterable_type',
|
||||
'filter_type'
|
||||
], 'filter_unique');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_filters');
|
||||
}
|
||||
}
|
BIN
public/img/pixelfed-icon-color.svg
Normal file
BIN
public/img/pixelfed-icon-color.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
107
resources/assets/js/components/passport/AuthorizedClients.vue
Normal file
107
resources/assets/js/components/passport/AuthorizedClients.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<style scoped>
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="tokens.length > 0">
|
||||
<div class="card card-default mb-4">
|
||||
<div class="card-header font-weight-bold bg-white">Authorized Applications</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Authorized Tokens -->
|
||||
<table class="table table-borderless mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Scopes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="token in tokens">
|
||||
<!-- Client Name -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ token.client.name }}
|
||||
</td>
|
||||
|
||||
<!-- Scopes -->
|
||||
<td style="vertical-align: middle;">
|
||||
<span v-if="token.scopes.length > 0">
|
||||
{{ token.scopes.join(', ') }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Revoke Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link text-danger" @click="revoke(token)">
|
||||
Revoke
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/*
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
tokens: []
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
prepareComponent() {
|
||||
this.getTokens();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the authorized tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
axios.get('/oauth/tokens')
|
||||
.then(response => {
|
||||
this.tokens = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke the given token.
|
||||
*/
|
||||
revoke(token) {
|
||||
axios.delete('/oauth/tokens/' + token.id)
|
||||
.then(response => {
|
||||
this.getTokens();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
350
resources/assets/js/components/passport/Clients.vue
Normal file
350
resources/assets/js/components/passport/Clients.vue
Normal file
|
@ -0,0 +1,350 @@
|
|||
<style scoped>
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="card card-default mb-4">
|
||||
<div class="card-header font-weight-bold bg-white">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>
|
||||
OAuth Clients
|
||||
</span>
|
||||
|
||||
<a class="action-link" tabindex="-1" @click="showCreateClientForm">
|
||||
Create New Client
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Current Clients -->
|
||||
<p class="mb-0" v-if="clients.length === 0">
|
||||
You have not created any OAuth clients.
|
||||
</p>
|
||||
|
||||
<table class="table table-borderless mb-0" v-if="clients.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Name</th>
|
||||
<th>Secret</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="client in clients">
|
||||
<!-- ID -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ client.id }}
|
||||
</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ client.name }}
|
||||
</td>
|
||||
|
||||
<!-- Secret -->
|
||||
<td style="vertical-align: middle;">
|
||||
<code>{{ client.secret }}</code>
|
||||
</td>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link" tabindex="-1" @click="edit(client)">
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link text-danger" @click="destroy(client)">
|
||||
Delete
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Client Modal -->
|
||||
<div class="modal fade" id="modal-create-client" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Create Client
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Form Errors -->
|
||||
<div class="alert alert-danger" v-if="createForm.errors.length > 0">
|
||||
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
|
||||
<br>
|
||||
<ul>
|
||||
<li v-for="error in createForm.errors">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Create Client Form -->
|
||||
<form role="form">
|
||||
<!-- Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Name</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input id="create-client-name" type="text" class="form-control" autocomplete="off"
|
||||
@keyup.enter="store" v-model="createForm.name">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Something your users will recognize and trust.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirect URL -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Redirect URL</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control" name="redirect"
|
||||
@keyup.enter="store" v-model="createForm.redirect">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Your application's authorization callback URL.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary font-weight-bold" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary font-weight-bold" @click="store">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Client Modal -->
|
||||
<div class="modal fade" id="modal-edit-client" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Edit Client
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Form Errors -->
|
||||
<div class="alert alert-danger" v-if="editForm.errors.length > 0">
|
||||
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
|
||||
<br>
|
||||
<ul>
|
||||
<li v-for="error in editForm.errors">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Edit Client Form -->
|
||||
<form role="form">
|
||||
<!-- Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Name</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input id="edit-client-name" type="text" class="form-control"
|
||||
@keyup.enter="update" v-model="editForm.name">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Something your users will recognize and trust.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirect URL -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Redirect URL</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control" name="redirect"
|
||||
@keyup.enter="update" v-model="editForm.redirect">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Your application's authorization callback URL.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" @click="update">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/*
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
clients: [],
|
||||
|
||||
createForm: {
|
||||
errors: [],
|
||||
name: '',
|
||||
redirect: ''
|
||||
},
|
||||
|
||||
editForm: {
|
||||
errors: [],
|
||||
name: '',
|
||||
redirect: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
prepareComponent() {
|
||||
this.getClients();
|
||||
|
||||
$('#modal-create-client').on('shown.bs.modal', () => {
|
||||
$('#create-client-name').focus();
|
||||
});
|
||||
|
||||
$('#modal-edit-client').on('shown.bs.modal', () => {
|
||||
$('#edit-client-name').focus();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the OAuth clients for the user.
|
||||
*/
|
||||
getClients() {
|
||||
axios.get('/oauth/clients')
|
||||
.then(response => {
|
||||
this.clients = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the form for creating new clients.
|
||||
*/
|
||||
showCreateClientForm() {
|
||||
$('#modal-create-client').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new OAuth client for the user.
|
||||
*/
|
||||
store() {
|
||||
this.persistClient(
|
||||
'post', '/oauth/clients',
|
||||
this.createForm, '#modal-create-client'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the given client.
|
||||
*/
|
||||
edit(client) {
|
||||
this.editForm.id = client.id;
|
||||
this.editForm.name = client.name;
|
||||
this.editForm.redirect = client.redirect;
|
||||
|
||||
$('#modal-edit-client').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the client being edited.
|
||||
*/
|
||||
update() {
|
||||
this.persistClient(
|
||||
'put', '/oauth/clients/' + this.editForm.id,
|
||||
this.editForm, '#modal-edit-client'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Persist the client to storage using the given form.
|
||||
*/
|
||||
persistClient(method, uri, form, modal) {
|
||||
form.errors = [];
|
||||
|
||||
axios[method](uri, form)
|
||||
.then(response => {
|
||||
this.getClients();
|
||||
|
||||
form.name = '';
|
||||
form.redirect = '';
|
||||
form.errors = [];
|
||||
|
||||
$(modal).modal('hide');
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof error.response.data === 'object') {
|
||||
form.errors = _.flatten(_.toArray(error.response.data.errors));
|
||||
} else {
|
||||
form.errors = ['Something went wrong. Please try again.'];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the given client.
|
||||
*/
|
||||
destroy(client) {
|
||||
axios.delete('/oauth/clients/' + client.id)
|
||||
.then(response => {
|
||||
this.getClients();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
298
resources/assets/js/components/passport/PersonalAccessTokens.vue
Normal file
298
resources/assets/js/components/passport/PersonalAccessTokens.vue
Normal file
|
@ -0,0 +1,298 @@
|
|||
<style scoped>
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="card card-default mb-4">
|
||||
<div class="card-header font-weight-bold bg-white">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>
|
||||
Personal Access Tokens
|
||||
</span>
|
||||
|
||||
<a class="action-link" tabindex="-1" @click="showCreateTokenForm">
|
||||
Create New Token
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- No Tokens Notice -->
|
||||
<p class="mb-0" v-if="tokens.length === 0">
|
||||
You have not created any personal access tokens.
|
||||
</p>
|
||||
|
||||
<!-- Personal Access Tokens -->
|
||||
<table class="table table-borderless mb-0" v-if="tokens.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="token in tokens">
|
||||
<!-- Client Name -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ token.name }}
|
||||
</td>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link text-danger" @click="revoke(token)">
|
||||
Delete
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Token Modal -->
|
||||
<div class="modal fade" id="modal-create-token" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Create Token
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Form Errors -->
|
||||
<div class="alert alert-danger" v-if="form.errors.length > 0">
|
||||
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
|
||||
<br>
|
||||
<ul>
|
||||
<li v-for="error in form.errors">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Create Token Form -->
|
||||
<form role="form" @submit.prevent="store">
|
||||
<!-- Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-4 col-form-label">Name</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input id="create-token-name" type="text" class="form-control" name="name" v-model="form.name" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scopes -->
|
||||
<div class="form-group row" v-if="scopes.length > 0">
|
||||
<label class="col-md-4 col-form-label">Scopes</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-for="scope in scopes">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
@click="toggleScope(scope.id)"
|
||||
:checked="scopeIsAssigned(scope.id)">
|
||||
|
||||
{{ scope.id }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary font-weight-bold" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary font-weight-bold" @click="store">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access Token Modal -->
|
||||
<div class="modal fade" id="modal-access-token" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Personal Access Token
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Here is your new personal access token. This is the only time it will be shown so don't lose it!
|
||||
You may now use this token to make API requests.
|
||||
</p>
|
||||
|
||||
<textarea class="form-control" rows="10">{{ accessToken }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/*
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
accessToken: null,
|
||||
|
||||
tokens: [],
|
||||
scopes: [],
|
||||
|
||||
form: {
|
||||
name: '',
|
||||
scopes: [],
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
prepareComponent() {
|
||||
this.getTokens();
|
||||
this.getScopes();
|
||||
|
||||
$('#modal-create-token').on('shown.bs.modal', () => {
|
||||
$('#create-token-name').focus();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the personal access tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
axios.get('/oauth/personal-access-tokens')
|
||||
.then(response => {
|
||||
this.tokens = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the available scopes.
|
||||
*/
|
||||
getScopes() {
|
||||
axios.get('/oauth/scopes')
|
||||
.then(response => {
|
||||
this.scopes = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the form for creating new tokens.
|
||||
*/
|
||||
showCreateTokenForm() {
|
||||
$('#modal-create-token').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new personal access token.
|
||||
*/
|
||||
store() {
|
||||
this.accessToken = null;
|
||||
|
||||
this.form.errors = [];
|
||||
|
||||
axios.post('/oauth/personal-access-tokens', this.form)
|
||||
.then(response => {
|
||||
this.form.name = '';
|
||||
this.form.scopes = [];
|
||||
this.form.errors = [];
|
||||
|
||||
this.tokens.push(response.data.token);
|
||||
|
||||
this.showAccessToken(response.data.accessToken);
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof error.response.data === 'object') {
|
||||
this.form.errors = _.flatten(_.toArray(error.response.data.errors));
|
||||
} else {
|
||||
this.form.errors = ['Something went wrong. Please try again.'];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the given scope in the list of assigned scopes.
|
||||
*/
|
||||
toggleScope(scope) {
|
||||
if (this.scopeIsAssigned(scope)) {
|
||||
this.form.scopes = _.reject(this.form.scopes, s => s == scope);
|
||||
} else {
|
||||
this.form.scopes.push(scope);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the given scope has been assigned to the token.
|
||||
*/
|
||||
scopeIsAssigned(scope) {
|
||||
return _.indexOf(this.form.scopes, scope) >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the given access token to the user.
|
||||
*/
|
||||
showAccessToken(accessToken) {
|
||||
$('#modal-create-token').modal('hide');
|
||||
|
||||
this.accessToken = accessToken;
|
||||
|
||||
$('#modal-access-token').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke the given token.
|
||||
*/
|
||||
revoke(token) {
|
||||
axios.delete('/oauth/personal-access-tokens/' + token.id)
|
||||
.then(response => {
|
||||
this.getTokens();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,8 +1,8 @@
|
|||
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}" title="Logo">
|
||||
<img src="/img/pixelfed-icon-black.svg" height="60px" class="p-2">
|
||||
<span class="h4 font-weight-bold mb-0">{{ config('app.name', 'Laravel') }}</span>
|
||||
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
|
||||
<span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
|
@ -23,7 +23,9 @@
|
|||
<a class="nav-link" href="{{route('discover')}}" title="Discover"><i class="far fa-compass fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{{route('notifications')}}" title="Notifications"><i class="far fa-heart fa-lg"></i></a>
|
||||
<a class="nav-link" href="{{route('notifications')}}" title="Notifications">
|
||||
<i class="far fa-heart fa-lg"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown px-2">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre title="User Menu">
|
||||
|
|
|
@ -35,49 +35,6 @@
|
|||
<input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{{--<div class="form-group row">
|
||||
<label for="inputPassword3" class="col-sm-3 col-form-label">Password</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="password" class="form-control" id="inputPassword3" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<fieldset class="form-group">
|
||||
<div class="row">
|
||||
<legend class="col-form-label col-sm-3 pt-0">Radios</legend>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios1" value="option1" checked>
|
||||
<label class="form-check-label" for="gridRadios1">
|
||||
First radio
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios2" value="option2">
|
||||
<label class="form-check-label" for="gridRadios2">
|
||||
Second radio
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check disabled">
|
||||
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios3" value="option3" disabled>
|
||||
<label class="form-check-label" for="gridRadios3">
|
||||
Third disabled radio
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">Checkbox</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="gridCheck1">
|
||||
<label class="form-check-label" for="gridCheck1">
|
||||
Example checkbox
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>--}}
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9">
|
||||
|
|
Loading…
Reference in a new issue