Merge pull request #2033 from pixelfed/staging

Improved Admin Dashboard Security
This commit is contained in:
daniel 2020-02-20 22:52:11 -07:00 committed by GitHub
commit cdb8784d81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1449 additions and 317 deletions

View file

@ -18,6 +18,12 @@
- Updated DiscoverController, fixes #2009 ([b04c7170](https://github.com/pixelfed/pixelfed/commit/b04c7170))
- Updated DeleteAccountPipeline, fixes [#2016](https://github.com/pixelfed/pixelfed/issues/2016), a bug affecting account deletion.
- Updated PlaceController, fixes [#2017](https://github.com/pixelfed/pixelfed/issues/2017), a postgres bug affecting country pagination in the places directory ([dd5fa3a4](https://github.com/pixelfed/pixelfed/commit/dd5fa3a4))
- Updated confirm email blade view, remove html5 entity that doesn't display properly ([aa26fa1d](https://github.com/pixelfed/pixelfed/commit/aa26fa1d))
- Updated ApiV1Controller, fix update_credentials endpoint ([a73fad75](https://github.com/pixelfed/pixelfed/commit/a73fad75))
- Updated AdminUserController, add moderation method ([a4cf21ea](https://github.com/pixelfed/pixelfed/commit/a4cf21ea))
- Updated BaseApiController, invalidate session after account deletion ([826978ce](https://github.com/pixelfed/pixelfed/commit/826978ce))
- Updated AdminUserController, add account deletion handler ([9be19ad8](https://github.com/pixelfed/pixelfed/commit/9be19ad8))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
### Added

View file

@ -270,7 +270,6 @@ class AccountController extends Controller
return redirect()->back();
}
public function unblock(Request $request)
{
$this->validate($request, [
@ -362,6 +361,13 @@ class AccountController extends Controller
public function sudoMode(Request $request)
{
if($request->session()->has('sudoModeAttempts') && $request->session()->get('sudoModeAttempts') >= 3) {
$request->session()->pull('2fa.session.active');
$request->session()->pull('redirectNext');
$request->session()->pull('sudoModeAttempts');
Auth::logout();
return redirect(route('login'));
}
return view('auth.sudo');
}
@ -373,6 +379,12 @@ class AccountController extends Controller
$user = Auth::user();
$password = $request->input('password');
$next = $request->session()->get('redirectNext', '/');
if($request->session()->has('sudoModeAttempts')) {
$count = (int) $request->session()->get('sudoModeAttempts');
$request->session()->put('sudoModeAttempts', $count + 1);
} else {
$request->session()->put('sudoModeAttempts', 1);
}
if(password_verify($password, $user->password) === true) {
$request->session()->put('sudoMode', time());
return redirect($next);

View file

@ -0,0 +1,284 @@
<?php
namespace App\Http\Controllers\Admin;
use Cache, DB;
use Illuminate\Http\Request;
use App\ModLog;
use App\Profile;
use App\User;
use App\Mail\AdminMessage;
use Illuminate\Support\Facades\Mail;
use App\Services\ModLogService;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
trait AdminUserController
{
public function users(Request $request)
{
$col = $request->query('col') ?? 'id';
$dir = $request->query('dir') ?? 'desc';
$users = User::select('id', 'username', 'status')
->withCount('statuses')
->orderBy($col, $dir)
->simplePaginate(10);
return view('admin.users.home', compact('users'));
}
public function userShow(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.show', compact('user', 'profile'));
}
public function userEdit(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.edit', compact('user', 'profile'));
}
public function userEditSubmit(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
$changed = false;
$fields = [];
if($request->filled('name') && $request->input('name') != $user->name) {
$fields['name'] = ['old' => $user->name, 'new' => $request->input('name')];
$user->name = $profile->name = $request->input('name');
$changed = true;
}
if($request->filled('username') && $request->input('username') != $user->username) {
$fields['username'] = ['old' => $user->username, 'new' => $request->input('username')];
$user->username = $profile->username = $request->input('username');
$changed = true;
}
if($request->filled('email') && $request->input('email') != $user->email) {
if(filter_var($request->input('email'), FILTER_VALIDATE_EMAIL) == false) {
abort(500, 'Invalid email address');
}
$fields['email'] = ['old' => $user->email, 'new' => $request->input('email')];
$user->email = $request->input('email');
$changed = true;
}
if($request->input('bio') != $profile->bio) {
$fields['bio'] = ['old' => $user->bio, 'new' => $request->input('bio')];
$profile->bio = $request->input('bio');
$changed = true;
}
if($request->input('website') != $profile->website) {
$fields['website'] = ['old' => $user->website, 'new' => $request->input('website')];
$profile->website = $request->input('website');
$changed = true;
}
if($changed == true) {
ModLogService::boot()
->objectUid($user->id)
->objectId($user->id)
->objectType('App\User::class')
->user($request->user())
->action('admin.user.edit')
->metadata([
'fields' => $fields
])
->accessLevel('admin')
->save();
$profile->save();
$user->save();
}
return redirect('/i/admin/users/show/' . $user->id);
}
public function userActivity(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
$logs = $user->accountLog()->orderByDesc('created_at')->paginate(10);
return view('admin.users.activity', compact('user', 'profile', 'logs'));
}
public function userMessage(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.message', compact('user', 'profile'));
}
public function userMessageSend(Request $request, $id)
{
$this->validate($request, [
'message' => 'required|string|min:5|max:500'
]);
$user = User::findOrFail($id);
$profile = $user->profile;
$message = $request->input('message');
Mail::to($user->email)->send(new AdminMessage($message));
ModLogService::boot()
->objectUid($user->id)
->objectId($user->id)
->objectType('App\User::class')
->user($request->user())
->action('admin.user.mail')
->metadata([
'message' => $message
])
->accessLevel('admin')
->save();
return redirect('/i/admin/users/show/' . $user->id);
}
public function userModTools(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.modtools', compact('user', 'profile'));
}
public function userModLogs(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
$logs = ModLog::whereObjectUid($user->id)
->orderByDesc('created_at')
->simplePaginate(10);
return view('admin.users.modlogs', compact('user', 'profile', 'logs'));
}
public function userModLogsMessage(Request $request, $id)
{
$this->validate($request, [
'message' => 'required|string|min:5|max:500'
]);
$user = User::findOrFail($id);
$profile = $user->profile;
$msg = $request->input('message');
ModLogService::boot()
->objectUid($user->id)
->objectId($user->id)
->objectType('App\User::class')
->user($request->user())
->message($msg)
->accessLevel('admin')
->save();
return redirect('/i/admin/users/modlogs/' . $user->id);
}
public function userDelete(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.delete', compact('user', 'profile'));
}
public function userDeleteProcess(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
if($user->is_admin == true) {
$mid = $request->user()->id;
abort_if($user->id < $mid, 403);
}
$ts = now()->addMonth();
$user->status = 'delete';
$profile->status = 'delete';
$user->delete_after = $ts;
$profile->delete_after = $ts;
$user->save();
$profile->save();
ModLogService::boot()
->objectUid($user->id)
->objectId($user->id)
->objectType('App\User::class')
->user($request->user())
->action('admin.user.delete')
->accessLevel('admin')
->save();
Cache::forget('profiles:private');
DeleteAccountPipeline::dispatch($user)->onQueue('high');
$msg = "Successfully deleted {$user->username}!";
$request->session()->flash('status', $msg);
return redirect('/i/admin/users/list');
}
public function userModerate(Request $request)
{
$this->validate($request, [
'profile_id' => 'required|exists:profiles,id',
'action' => 'required|in:cw,no_autolink,unlisted'
]);
$pid = $request->input('profile_id');
$action = $request->input('action');
$profile = Profile::findOrFail($pid);
if($profile->user->is_admin == true) {
$mid = $request->user()->id;
abort_if($profile->user_id < $mid, 403);
}
switch ($action) {
case 'cw':
$profile->cw = !$profile->cw;
$msg = "Success!";
break;
case 'no_autolink':
$profile->no_autolink = !$profile->no_autolink;
$msg = "Success!";
break;
case 'unlisted':
$profile->unlisted = !$profile->unlisted;
$msg = "Success!";
break;
}
$profile->save();
ModLogService::boot()
->objectUid($profile->user_id)
->objectId($profile->user_id)
->objectType('App\User::class')
->user($request->user())
->action('admin.user.moderate')
->metadata([
'action' => $action,
'message' => $msg
])
->accessLevel('admin')
->save();
$request->session()->flash('status', $msg);
return redirect('/i/admin/users/modtools/' . $profile->user_id);
}
public function userModLogDelete(Request $request, $id)
{
$this->validate($request, [
'mid' => 'required|integer|exists:mod_logs,id'
]);
$user = User::findOrFail($id);
$uid = $request->user()->id;
$mid = $request->input('mid');
$ml = ModLog::whereUserId($uid)->findOrFail($mid)->delete();
$msg = "Successfully deleted modlog comment!";
$request->session()->flash('status', $msg);
return redirect('/i/admin/users/modlogs/' . $user->id);
}
}

View file

@ -21,7 +21,8 @@ use App\Http\Controllers\Admin\{
AdminReportController,
AdminMediaController,
AdminSettingsController,
AdminSupportController
AdminSupportController,
AdminUserController
};
use Illuminate\Validation\Rule;
use App\Services\AdminStatsService;
@ -32,11 +33,13 @@ class AdminController extends Controller
AdminDiscoverController,
AdminMediaController,
AdminSettingsController,
AdminInstanceController;
AdminInstanceController,
AdminUserController;
public function __construct()
{
$this->middleware('admin');
$this->middleware('dangerzone');
$this->middleware('twofactor');
}
@ -46,25 +49,6 @@ class AdminController extends Controller
return view('admin.home', compact('data'));
}
public function users(Request $request)
{
$col = $request->query('col') ?? 'id';
$dir = $request->query('dir') ?? 'desc';
$users = User::select('id', 'username', 'status')
->withCount('statuses')
->orderBy($col, $dir)
->simplePaginate(10);
return view('admin.users.home', compact('users'));
}
public function editUser(Request $request, $id)
{
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.edit', compact('user', 'profile'));
}
public function statuses(Request $request)
{
$statuses = Status::orderBy('id', 'desc')->simplePaginate(10);
@ -109,22 +93,25 @@ class AdminController extends Controller
'nullable',
'string',
Rule::in(['all', 'local', 'remote'])
],
'limit' => 'nullable|integer|min:1|max:50'
]
]);
$search = $request->input('search');
$filter = $request->input('filter');
$limit = 12;
if($search) {
$profiles = Profile::select('id','username')
->where('username', 'like', "%$search%")
->orderBy('id','desc')
$profiles = Profile::select('id','username')
->whereNull('status')
->when($search, function($q, $search) {
return $q->where('username', 'like', "%$search%");
})->when($filter, function($q, $filter) {
if($filter == 'local') {
return $q->whereNull('domain');
}
if($filter == 'remote') {
return $q->whereNotNull('domain');
}
return $q;
})->orderByDesc('id')
->simplePaginate($limit);
} else if($filter) {
$profiles = Profile::select('id','username')->withCount(['likes','statuses','followers'])->orderBy($filter, $order)->simplePaginate($limit);
} else {
$profiles = Profile::select('id','username')->orderBy('id','desc')->simplePaginate($limit);
}
return view('admin.profiles.home', compact('profiles'));
}

View file

@ -152,7 +152,7 @@ class ApiV1Controller extends Controller
$this->validate($request, [
'display_name' => 'nullable|string',
'note' => 'nullable|string',
'locked' => 'nullable|boolean',
'locked' => 'nullable',
// 'source.privacy' => 'nullable|in:unlisted,public,private',
// 'source.sensitive' => 'nullable|boolean'
]);

View file

@ -314,6 +314,10 @@ class BaseApiController extends Controller
{
$user = $request->user();
abort_if(!$user, 403);
if($user->status != null) {
Auth::logout();
return redirect('/login');
}
$resource = new Fractal\Resource\Item($user->profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);

View file

@ -50,13 +50,13 @@ class CollectionController extends Controller
return $request->all();
}
public function store(Request $request, int $id)
public function store(Request $request, $id)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'title' => 'nullable',
'description' => 'nullable',
'visibility' => 'required|alpha|in:public,private'
'visibility' => 'nullable|string|in:public,private'
]);
$profile = Auth::user()->profile;
@ -140,7 +140,7 @@ class CollectionController extends Controller
return 200;
}
public function get(Request $request, int $id)
public function get(Request $request, $id)
{
$profile = Auth::check() ? Auth::user()->profile : [];

View file

@ -23,11 +23,7 @@ class CommentController extends Controller
{
public function showAll(Request $request, $username, int $id)
{
$profile = Profile::whereNull(['status', 'domain'])->whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($profile->id)->findOrFail($id);
$replies = Status::whereInReplyToId($id)->simplePaginate(40);
return view('status.comments', compact('profile', 'status', 'replies'));
abort(404);
}
public function store(Request $request)

View file

@ -65,7 +65,7 @@ class NewsroomController extends Controller
->map(function($post) {
return [
'id' => $post->id,
'title' => Str::limit($post->title, 25),
'title' => Str::limit($post->title, 40),
'summary' => $post->summary,
'url' => $post->show_link ? $post->permalink() : null,
'published_at' => $post->published_at->format('F m, Y')

View file

@ -20,11 +20,19 @@ class TimelineController extends Controller
public function local(Request $request)
{
return view('timeline.local');
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.local', compact('layout'));
}
public function network(Request $request)
{
return view('timeline.network');
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.network', compact('layout'));
}
}

View file

@ -16,6 +16,12 @@ class DangerZone
*/
public function handle($request, Closure $next)
{
if( $request->session()->get('sudoModeAttempts') > 3) {
$request->session()->pull('redirectNext');
$request->session()->pull('sudoModeAttempts');
Auth::logout();
return redirect(route('login'));
}
if(!Auth::check()) {
return redirect(route('login'));
}

37
app/Mail/AdminMessage.php Normal file
View file

@ -0,0 +1,37 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class AdminMessage extends Mailable
{
use Queueable, SerializesModels;
protected $msg;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($msg)
{
$this->msg = $msg;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$admins = config('pixelfed.domain.app') . ' admins';
return $this->markdown('emails.notification.admin_message')
->with(['msg' => $this->msg])
->subject('Message from ' . $admins);
}
}

48
app/ModLog.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ModLog extends Model
{
protected $visible = ['id'];
public function admin()
{
return $this->belongsTo(User::class, 'user_id');
}
public function actionToText()
{
$msg = 'Unknown action';
switch ($this->action) {
case 'admin.user.mail':
$msg = "Sent Message";
break;
case 'admin.user.action.cw.warn':
$msg = "Sent CW reminder";
break;
case 'admin.user.edit':
$msg = "Changed Profile";
break;
case 'admin.user.moderate':
$msg = "Moderation";
break;
case 'admin.user.delete':
$msg = "Deleted Account";
break;
default:
$msg = 'Unknown action';
break;
}
return $msg;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace App\Services;
use App\ModLog;
use App\User;
class ModLogService {
protected $log;
public function __construct()
{
$this->log = new \StdClass;
}
public static function boot()
{
return new self;
}
public function user(User $user)
{
$this->log->user = $user;
return $this;
}
public function objectUid($val = null)
{
$this->log->object_uid = $val;
return $this;
}
public function objectId($val = null)
{
$this->log->object_id = $val;
return $this;
}
public function objectType($val = null)
{
$this->log->object_type = $val;
return $this;
}
public function action($val = null)
{
$this->log->action = $val;
return $this;
}
public function message($val = null)
{
$this->log->message = $val;
return $this;
}
public function metadata(array $val = null)
{
$this->log->metadata = json_encode($val);
return $this;
}
public function accessLevel($val = null)
{
if(!in_array($val, ['admin', 'mod'])) {
return $this;
}
$this->log->access_level = $val;
return $this;
}
public function save($res = false)
{
$log = $this->log;
if(!isset($log->user)) {
throw new \Exception('Invalid ModLog attribute.');
}
$ml = new ModLog();
$ml->user_id = $log->user->id;
$ml->user_username = $log->user->username;
$ml->object_uid = $log->object_uid ?? null;
$ml->object_id = $log->object_id ?? null;
$ml->object_type = $log->object_type ?? null;
$ml->action = $log->action ?? null;
$ml->message = $log->message ?? null;
$ml->metadata = $log->metadata ?? null;
$ml->access_level = $log->access_level ?? 'admin';
$ml->save();
if($res == true) {
return $ml;
} else {
return;
}
}
}

View file

@ -12,14 +12,15 @@ class StoryItemTransformer extends Fractal\TransformerAbstract
public function transform(StoryItem $item)
{
return [
'id' => (string) Str::uuid(),
'id' => (string) $item->id,
'type' => $item->type,
'length' => $item->duration,
'length' => $item->duration != 0 ? $item->duration : 3,
'src' => $item->url(),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $item->updated_at->format('U'),
'time' => $item->created_at->format('U'),
'expires_at' => $item->created_at->addHours(24)->format('U'),
'seen' => $item->story->seen(),
];
}

View file

@ -16,19 +16,16 @@ class StoryTransformer extends Fractal\TransformerAbstract
return [
'id' => (string) $story->id,
'photo' => $story->profile->avatarUrl(),
'name' => '',
'link' => '',
'name' => $story->profile->username,
'link' => $story->profile->url(),
'lastUpdated' => $story->updated_at->format('U'),
'seen' => $story->seen(),
'items' => [],
];
}
public function includeItems(Story $story)
{
$items = $story->items;
return $this->collection($items, new StoryItemTransformer());
return $this->item($story, new StoryItemTransformer());
}
}

View file

@ -83,4 +83,9 @@ class User extends Authenticatable
return 'profile:storage:used:' . $this->id;
}
public function accountLog()
{
return $this->hasMany(AccountLog::class);
}
}

View file

@ -10,6 +10,7 @@ class Config {
public static function get() {
return Cache::remember('api:site:configuration', now()->addMinutes(30), function() {
return [
'open_registration' => config('pixelfed.open_registration'),
'uploader' => [
'max_photo_size' => config('pixelfed.max_photo_size'),
'max_caption_length' => config('pixelfed.max_caption_length'),
@ -35,6 +36,7 @@ class Config {
],
'site' => [
'name' => config('app.name', 'pixelfed'),
'domain' => config('pixelfed.domain.app'),
'url' => config('app.url'),
'description' => config('instance.description')

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateModLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('mod_logs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->index();
$table->string('user_username')->nullable();
$table->bigInteger('object_uid')->nullable()->unsigned()->index();
$table->bigInteger('object_id')->nullable()->unsigned()->index();
$table->string('object_type')->nullable()->index();
$table->string('action')->nullable();
$table->text('message')->nullable();
$table->json('metadata')->nullable();
$table->string('access_level')->default('admin')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('mod_logs');
}
}

View file

@ -0,0 +1,84 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between align-items-center">
<span><a href="/i/admin/users/show/{{$user->id}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/show/{{$user->id}}">
<span class="font-weight-bold">Overview</span>
</a>
<a class="dropdown-item" href="/i/admin/users/message/{{$user->id}}">
<span class="font-weight-bold">Send Message</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/edit/{{$user->id}}">
<span class="font-weight-bold">Edit</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modtools/{{$user->id}}">
<span class="font-weight-bold">Mod Tools</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modlogs/{{$user->id}}">
<span class="font-weight-bold">Mod Logs</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/delete/{{$user->id}}">
<span class="text-danger font-weight-bold">Delete Account</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row mb-3">
<div class="col-12 col-md-8 offset-md-2">
<p class="title h4 font-weight-bold mt-2 py-2">Recent Activity</p>
<hr>
<div class="row">
<div class="col-12">
@if($logs->count() > 0)
<div class="list-group">
@foreach($logs as $log)
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<p class="small text-muted font-weight-bold mb-0">{{$log->created_at->diffForHumans()}}</p>
<p class="lead mb-0">{{$log->message}}</p>
<p class="small text-muted font-weight-bold mb-0">
IP: {{$log->ip_address}}
</p>
</div>
<div>
<i class="fas fa-chevron-right fa-lg text-lighter"></i>
</div>
</div>
@endforeach
</div>
<div class="d-flex justify-content-center mt-3">
{{$logs->links()}}
</div>
@else
<div class="card card-body border shadow-none text-center">
No Activity found
</div>
@endif
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,74 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between align-items-center">
<span><a href="/i/admin/users/show/{{$user->id}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/show/{{$user->id}}">
<span class="font-weight-bold">Overview</span>
</a>
<a class="dropdown-item" href="/i/admin/users/activity/{{$user->id}}">
<span class="font-weight-bold">Activity</span>
</a>
<a class="dropdown-item" href="/i/admin/users/message/{{$user->id}}">
<span class="font-weight-bold">Send Message</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/edit/{{$user->id}}">
<span class="font-weight-bold">Edit</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modtools/{{$user->id}}">
<span class="font-weight-bold">Mod Tools</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modlogs/{{$user->id}}">
<span class="font-weight-bold">Mod Logs</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row">
<div class="col-12 col-md-8 offset-md-2">
<div class="card card-body">
<p class="lead text-center py-5">Are you sure you want to delete this account?</p>
<p class="mb-0">
<form method="post" id="deleteForm">
@csrf
<button type="button" id="confirmDelete" class="btn btn-danger btn-block font-weight-bold">DELETE ACCOUNT</button>
</form>
</p>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$('#confirmDelete').click(function(e) {
e.preventDefault();
if(window.confirm('Are you sure you want to delete this account?') == true) {
if(window.confirm('Are you absolutely sure you want to delete this account?') == true) {
$('#deleteForm').submit();
}
}
})
</script>
@endpush

View file

@ -1,101 +1,100 @@
@extends('admin.partial.template')
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between">
<h3 class="font-weight-bold">Edit User</h3>
<span><a href="{{route('admin.users')}}" class="btn btn-outline-primary btn-sm font-weight-bold">Back</a></span>
</div>
<hr>
<div class="title d-flex justify-content-between align-items-center">
<span><a href="/i/admin/users/show/{{$user->id}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/show/{{$user->id}}">
<span class="font-weight-bold">Overview</span>
</a>
<a class="dropdown-item" href="/i/admin/users/activity/{{$user->id}}">
<span class="font-weight-bold">Activity</span>
</a>
<a class="dropdown-item" href="/i/admin/users/message/{{$user->id}}">
<span class="font-weight-bold">Send Message</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/modtools/{{$user->id}}">
<span class="font-weight-bold">Mod Tools</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modlogs/{{$user->id}}">
<span class="font-weight-bold">Mod Logs</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/delete/{{$user->id}}">
<span class="text-danger font-weight-bold">Delete Account</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row mb-3">
<div class="col-12 col-md-3">
<div class="card">
<div class="card-body text-center">
<p class="h4 mb-0 font-weight-bold">{{$profile->statusCount()}}</p>
<p class="text-muted font-weight-bold small mb-0">Posts</p>
</div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="card">
<div class="card-body text-center">
<p class="h4 mb-0 font-weight-bold">{{$profile->likes()->count()}}</p>
<p class="text-muted font-weight-bold small mb-0">Likes</p>
</div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="card">
<div class="card-body text-center">
<p class="h4 mb-0 font-weight-bold">{{$profile->reports()->count()}}</p>
<p class="text-muted font-weight-bold small mb-0">Reports</p>
</div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="card">
<div class="card-body text-center">
<p class="h4 mb-0 font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}</p>
<p class="text-muted font-weight-bold small mb-0">Storage Used</p>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-8 offset-md-2">
<p class="title h4 font-weight-bold mt-2 py-2">Edit</p>
<hr>
<div class="row">
<div class="col-12">
<form method="post">
@csrf
<div class="form-group">
<label class="font-weight-bold text-muted">Display Name</label>
<input type="text" class="form-control" name="name" value="{{$user->name}}">
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Username</label>
<input type="text" class="form-control" name="username" value="{{$user->username}}">
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Email address</label>
<input type="email" class="form-control" name="email" value="{{$user->email}}" placeholder="Enter email">
<p class="help-text small text-muted font-weight-bold">
@if($user->email_verified_at)
<span class="text-success">Verified</span> for {{$user->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">Unverified</span> email.
@endif
</p>
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Bio</label>
<textarea class="form-control" rows="4" name="bio" placeholder="Empty bio">{{$profile->bio}}</textarea>
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Website</label>
<input type="text" class="form-control" name="website" value="{{$user->website}}" placeholder="No website added">
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Admin</label>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="customSwitch1" {{$user->is_admin ? 'checked="checked"' : ''}}>
<label class="custom-control-label" for="customSwitch1"></label>
</div>
<p class="help-text small text-muted font-weight-bold">For security reasons, you cannot change admin status on this form. Use the CLI instead.</p>
</div>
<hr>
<p class="float-right">
<button type="submit" class="btn btn-primary font-weight-bold py-1">SAVE</button>
</p>
</form>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-12 col-md-4">
<div class="card">
<div class="card-body text-center">
<img src="{{$profile->avatarUrl()}}" class="img-thumbnail rounded-circle" width="128px" height="128px">
</div>
<div class="card-footer bg-white">
<p class="font-weight-bold mb-0 small">Last updated: {{$profile->avatar->updated_at->diffForHumans()}}</p>
</div>
</div>
</div>
<div class="col-12 col-md-8">
<div class="card">
<div class="card-body p-5 d-flex justify-content-center align-items-center">
<div class="text-center py-3">
<p class="font-weight-bold mb-0">
{{$profile->username}}
</p>
<p class="h3 font-weight-bold">
{{$profile->emailUrl()}}
</p>
<p class="font-weight-bold mb-0 text-muted">
Member Since: {{$profile->created_at->format('M Y')}}
</p>
</div>
</div>
</div>
</div>
</div>
<hr>
<div class="mx-3">
<div class="sub-title h4 font-weight-bold mb-4">
Account Settings
</div>
<form>
<div class="form-group">
<label class="font-weight-bold text-muted">Display Name</label>
<input type="text" class="form-control" value="{{$user->name}}">
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Username</label>
<input type="text" class="form-control" value="{{$user->username}}">
</div>
<div class="form-group">
<label class="font-weight-bold text-muted">Email address</label>
<input type="email" class="form-control" value="{{$user->email}}" placeholder="Enter email">
<p class="help-text small text-muted font-weight-bold">
@if($user->email_verified_at)
<span class="text-success">Verified</span> for {{$user->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">Unverified</span> email.
@endif
</p>
</div>
</form>
</div>
@endsection

View file

@ -2,141 +2,112 @@
@section('header')
<div class="bg-primary">
<div class="container">
<div class="my-5"></div>
</div>
<div class="container">
<div class="my-5">test</div>
</div>
</div>
@endsection
@section('section')
<div class="title">
<h3 class="font-weight-bold">Users</h3>
</div>
<hr>
<div class="table-responsive">
<table class="table">
<thead class="bg-light">
<tr class="text-center">
<th scope="col" class="border-0" width="10%">
<span>ID</span>
</th>
<th scope="col" class="border-0" width="30%">
<span>Username</span>
</th>
<th scope="col" class="border-0" width="15%">
<span>Statuses</span>
</th>
<th scope="col" class="border-0" width="15%">
<span>Storage</span>
</th>
<th scope="col" class="border-0" width="30%">
<span>Actions</span>
</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr class="font-weight-bold text-center user-row">
<th scope="row">
<span class="{{$user->status == 'deleted' ? 'text-danger':''}}">{{$user->id}}</span>
</th>
<td class="text-left">
<img src="{{$user->profile ? $user->profile->avatarUrl() : '/storage/avatars/default.png?v=1'}}" width="28px" class="rounded-circle mr-2" style="border:1px solid #ccc">
<span title="{{$user->username}}" data-toggle="tooltip" data-placement="bottom">
<span class="{{$user->status == 'deleted' ? 'text-danger':''}}">{{$user->username}}</span>
@if($user->is_admin)
<i class="text-danger fas fa-certificate" title="Admin"></i>
@endif
</span>
</td>
<td>
<span class="{{$user->status == 'deleted' ? 'text-danger':''}}">{{$user->profile ? $user->profile->statusCount() : 0}}</span>
</td>
<td>
<span class="{{$user->status == 'deleted' ? 'text-danger':''}}"><p class="human-size mb-0" data-bytes="{{App\Media::whereUserId($user->id)->sum('size')}}"></p></span>
</td>
<td>
<span class="action-row font-weight-lighter">
<a href="{{$user->url()}}" class="pr-2 text-muted small font-weight-bold" title="View Profile" data-toggle="tooltip" data-placement="bottom">
View
</a>
<div class="title">
<h3 class="font-weight-bold">Users</h3>
</div>
<hr>
<div class="table-responsive">
<table class="table">
<thead class="bg-light">
<tr class="text-center">
<th scope="col" class="border-0" width="10%">
<span>ID</span>
</th>
<th scope="col" class="border-0" width="30%">
<span>Username</span>
</th>
<th scope="col" class="border-0" width="30%">
<span>Actions</span>
</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
@if($user->status == 'deleted')
<tr class="font-weight-bold text-center user-row">
<th scope="row">
<span class="text-danger" class="text-monospace">{{$user->id}}</span>
</th>
<td class="text-left">
<img src="/storage/avatars/default.png?v=3" width="28px" class="rounded-circle mr-2" style="border:1px solid #ccc">
<span title="{{$user->username}}" data-toggle="tooltip" data-placement="bottom">
<span class="text-danger">{{$user->username}}</span>
</span>
</td>
<td>
<span class="font-weight-bold small">
<span class="text-danger">Account Deleted</span>
</span>
</td>
</tr>
@else
<tr class="font-weight-bold text-center user-row">
<th scope="row">
<span class="text-monospace">{{$user->id}}</span>
</th>
<td class="text-left">
<img src="{{$user->profile->avatarUrl()}}" width="28px" class="rounded-circle mr-2" style="border:1px solid #ccc">
<span title="{{$user->username}}" data-toggle="tooltip" data-placement="bottom">
<span>{{$user->username}}</span>
@if($user->is_admin)
<i class="text-danger fas fa-certificate" title="Admin"></i>
@endif
</span>
</td>
<td>
<span class="action-row font-weight-lighter">
<a href="{{$user->url()}}" class="pr-2 text-muted small font-weight-bold" title="View Profile" data-toggle="tooltip" data-placement="bottom">
Profile
</a>
<a href="/i/admin/users/edit/{{$user->id}}" class="pr-2 text-muted small font-weight-bold" title="Edit Profile" data-toggle="tooltip" data-placement="bottom">
Edit
</a>
<a href="/i/admin/users/show/{{$user->id}}" class="pr-2 text-muted small font-weight-bold" title="Profile Review" data-toggle="tooltip" data-placement="bottom">
Review
</a>
<a href="#" class="text-muted action-btn small font-weight-bold" title="Delete Profile" data-toggle="tooltip" data-placement="bottom" data-id="{{$user->id}}" data-action="delete">
Delete
</a>
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="d-flex justify-content-center mt-5 small">
{{$users->links()}}
</div>
<a href="/i/admin/users/modlogs/{{$user->id}}" class="pr-2 text-muted small font-weight-bold" title="Moderation Logs" data-toggle="tooltip" data-placement="bottom">
Mod Logs
</a>
</span>
</td>
</tr>
@endif
@endforeach
</tbody>
</table>
</div>
<div class="d-flex justify-content-center mt-5 small">
{{$users->links()}}
</div>
@endsection
@push('styles')
<style type="text/css">
.jqstooltip {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
border: 0 !important;
border-radius: 2px;
max-width: 20px;
}
.user-row .action-row {
display: none;
}
.user-row:hover {
background-color: #eff8ff;
}
.user-row:hover .action-row {
display: block;
}
.user-row:hover .last-active {
display: none;
}
.user-row:hover {
background-color: #eff8ff;
}
.user-row:hover .action-row {
display: block;
}
.user-row:hover .last-active {
display: none;
}
</style>
@endpush
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('.human-size').each(function(d,a) {
let el = $(a);
let size = el.data('bytes');
el.text(filesize(size, {round: 0}));
});
$(document).on('click', '.action-btn', function(e) {
e.preventDefault();
let el = $(this);
let id = el.data('id');
let action = el.data('action');
switch(action) {
case 'view':
window.location.href = el.data('url');
break;
case 'edit':
let redirect = '/i/admin/users/edit/' + id;
window.location.href = redirect;
break;
case 'delete':
swal('Error', 'Sorry this action is not yet available', 'error');
break;
}
});
});
</script>
<script type="text/javascript">
$(document).ready(function() {
$('.human-size').each(function(d,a) {
let el = $(a);
let size = el.data('bytes');
el.text(filesize(size, {round: 0}));
});
});
</script>
@endpush

View file

@ -0,0 +1,112 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between align-items-center">
<span><a href="/i/admin/users/show/{{$user->id}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/show/{{$user->id}}">
<span class="font-weight-bold">Overview</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/edit/{{$user->id}}">
<span class="font-weight-bold">Edit</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modtools/{{$user->id}}">
<span class="font-weight-bold">Mod Tools</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modlogs/{{$user->id}}">
<span class="font-weight-bold">Mod Logs</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/delete/{{$user->id}}">
<span class="text-danger font-weight-bold">Delete Account</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row mb-3">
<div class="col-12 col-md-8 offset-md-2">
<p class="title h4 font-weight-bold mt-2 py-2">Send Message</p>
<hr>
<div class="row">
<div class="col-12">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="post" id="messageForm">
@csrf
<div class="form-group">
<textarea class="form-control" rows="8" placeholder="Message body ..." id="message" name="message"></textarea>
<p class="help-text mb-0 small text-muted">
<span>Plain text only, html will not be rendered.</span>
<span class="float-right msg-counter"><span class="msg-count">0</span>/500</span>
</p>
</div>
<p class="float-right">
<button type="button" class="btn btn-primary py-1 font-weight-bold" onclick="submitWarning()"><i class="fas fa-message"></i> SEND</button>
</p>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$('#message').on('keyup change paste submit', function(e) {
let len = e.target.value.length;
$('.msg-count').text(len);
});
function submitWarning() {
let msg = document.querySelector('#message');
if(msg.value.length < 5) {
swal('Oops!', 'Your message must be longer than 5 characters.', 'error');
return;
}
if(msg.value.length > 500) {
swal('Oops!', 'Your message must be shorter than 500 characters.', 'error');
return;
}
swal({
title: "Are you sure?",
text: "Are you sure you want to send this message to {{$user->username}}?",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((sendMessage) => {
if (sendMessage) {
$('#messageForm').submit();
} else {
return;
}
});
}
</script>
@endpush

View file

@ -0,0 +1,159 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between align-items-center">
<span><a href="/i/admin/users/show/{{$user->id}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/show/{{$user->id}}">
<span class="font-weight-bold">Overview</span>
</a>
<a class="dropdown-item" href="/i/admin/users/activity/{{$user->id}}">
<span class="font-weight-bold">Activity</span>
</a>
<a class="dropdown-item" href="/i/admin/users/message/{{$user->id}}">
<span class="font-weight-bold">Send Message</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/edit/{{$user->id}}">
<span class="font-weight-bold">Edit</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modtools/{{$user->id}}">
<span class="font-weight-bold">Mod Tools</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/delete/{{$user->id}}">
<span class="text-danger font-weight-bold">Delete Account</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row mb-3">
<div class="col-12 col-md-8 offset-md-2">
<p class="title h4 font-weight-bold mt-2 py-2">Moderation Logs</p>
<hr>
<div class="row">
<div class="col-12">
<div class="card card-body shadow-none border mb-3">
<form method="post">
@csrf
<div class="form-group">
<textarea class="form-control" name="message" id="message" rows="4" style="resize: none;" placeholder="Send a message to other admins and mods, they will be notified"></textarea>
@if ($errors->any())
@foreach ($errors->all() as $error)
<p class="invalid-feedback mb-0" style="display:block;">
<strong>{{ $error }}</strong>
</p>
@endforeach
@endif
</div>
<div>
<span class="small text-muted font-weight-bold">
<span class="msg-count">0</span>/500
</span>
<span class="float-right">
<button class="btn btn-primary btn-sm py-1 font-weight-bold">SEND</button>
</span>
</div>
</form>
</div>
@if($logs->count() > 0)
<div class="list-group">
@foreach($logs as $log)
<div class="list-group-item">
@if($log->message != null)
<div class="d-flex justify-content-between">
<div class="mr-3">
<img src="{{$log->admin->profile->avatarUrl()}}" width="40px" height="40px" class="border p-1 rounded-circle">
</div>
<div style="flex-grow: 1;">
@if($log->user_id != Auth::id())
<div class="p-3 bg-primary rounded">
<p class="mb-0 text-white" style="font-weight: 600;">{{$log->message}}</p>
</div>
@else
<div class="p-3 bg-white border rounded">
<p class="mb-0 text-dark" style="font-weight: 600;">{{$log->message}}</p>
</div>
@endif
<div class="d-flex justify-content-between small text-muted font-weight-bold mb-0 pt-2">
<span class="mr-4">
&commat;{{$log->user_username}}
</span>
<span>
{{$log->created_at->diffForHumans()}}
</span>
</div>
</div>
@if($log->user_id == Auth::id())
<div class="align-self-top ml-2">
<form method="post" action="/i/admin/users/modlogs/{{$user->id}}/delete">
@csrf
<input type="hidden" name="mid" value="{{$log->id}}">
<button type="submit" class="btn btn-text">
<i class="fas fa-times text-lighter"></i>
</button>
</form>
</div>
@endif
</div>
@else
<div class="d-flex justify-content-between align-items-center">
<div class="mr-3">
<img src="{{$log->admin->profile->avatarUrl()}}" width="40px" height="40px" class="border p-1 rounded-circle">
</div>
<div style="flex-grow: 1;">
<p class="small text-muted font-weight-bold mb-0">{{$log->created_at->diffForHumans()}}</p>
<p class="lead mb-0">{{$log->actionToText()}}</p>
<p class="small text-muted font-weight-bold mb-0">
by: {{$log->user_username}}
</p>
</div>
<div>
<i class="fas fa-chevron-right fa-lg text-lighter"></i>
</div>
</div>
@endif
</div>
@endforeach
</div>
<div class="d-flex justify-content-center mt-3">
{{$logs->links()}}
</div>
@else
<div class="card card-body border shadow-none text-center">
No Activity found
</div>
@endif
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$('#message').on('keyup change paste submit', function(e) {
let len = e.target.value.length;
$('.msg-count').text(len);
});
</script>
@endpush

View file

@ -0,0 +1,90 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between align-items-center">
<span><a href="/i/admin/users/show/{{$user->id}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/show/{{$user->id}}">
<span class="font-weight-bold">Overview</span>
</a>
<a class="dropdown-item" href="/i/admin/users/activity/{{$user->id}}">
<span class="font-weight-bold">Activity</span>
</a>
<a class="dropdown-item" href="/i/admin/users/message/{{$user->id}}">
<span class="font-weight-bold">Send Message</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/edit/{{$user->id}}">
<span class="font-weight-bold">Edit</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modlogs/{{$user->id}}">
<span class="font-weight-bold">Mod Logs</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/delete/{{$user->id}}">
<span class="text-danger font-weight-bold">Delete Account</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row mb-3">
<div class="col-12 col-md-8 offset-md-2">
<p class="title h4 font-weight-bold mt-2 py-2">Mod Tools</p>
<hr>
<div class="row">
<div class="col-12 col-md-6">
<form method="post" action="/i/admin/users/moderation/update" class="pb-3">
@csrf
<input type="hidden" name="action" value="cw">
<input type="hidden" name="profile_id" value="{{$profile->id}}">
<button class="btn btn-outline-{{$profile->cw ? 'secondary' : 'primary'}} py-0 font-weight-bold">
{{$profile->cw ? 'Remove CW Enforcement' : 'Enforce CW'}}
</button>
<p class="help-text text-muted font-weight-bold small">Adds a CW to every post made by this account.</p>
</form>
</div>
<div class="col-12 col-md-6">
<form method="post" action="/i/admin/users/moderation/update" class="pb-3">
@csrf
<input type="hidden" name="action" value="unlisted">
<input type="hidden" name="profile_id" value="{{$profile->id}}">
<button class="btn btn-outline-{{$profile->unlisted ? 'secondary' : 'primary'}} py-0 font-weight-bold">
{{$profile->unlisted ? 'Remove Unlisting' : 'Unlisted Posts'}}
</button>
<p class="help-text text-muted font-weight-bold small">Removes account from public/network timelines.</p>
</form>
</div>
<div class="col-12 col-md-6">
<form method="post" action="/i/admin/users/moderation/update" class="pb-3">
@csrf
<input type="hidden" name="action" value="no_autolink">
<input type="hidden" name="profile_id" value="{{$profile->id}}">
<button class="btn btn-outline-{{$profile->no_autolink ? 'secondary' : 'primary'}} py-0 font-weight-bold">
{{$profile->no_autolink ? 'Remove No Autolinking' : 'No Autolinking'}}
</button>
<p class="help-text text-muted font-weight-bold small">Do not transform mentions, hashtags or urls into HTML.</p>
</form>
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,121 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title d-flex justify-content-between align-items-center">
<span><a href="{{route('admin.users')}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
<span class="text-center">
<h3 class="font-weight-bold mb-0">&commat;{{$profile->username}}</h3>
<p class="mb-0 small text-muted text-uppercase font-weight-bold">
<span>{{$profile->statuses()->count()}} Posts</span>
<span class="px-1">|</span>
<span>{{$profile->followers()->count()}} Followers</span>
<span class="px-1">|</span>
<span>{{$profile->following()->count()}} Following</span>
</p>
</span>
<span>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm font-weight-bold dropdown-toggle" type="button" id="userActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userActions">
<a class="dropdown-item" href="/i/admin/users/activity/{{$user->id}}">
<span class="font-weight-bold">Activity</span>
</a>
<a class="dropdown-item" href="/i/admin/users/message/{{$user->id}}">
<span class="font-weight-bold">Send Message</span>
</a>
<a class="dropdown-item" href="{{$profile->url()}}">
<span class="font-weight-bold">View Profile</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/edit/{{$user->id}}">
<span class="font-weight-bold">Edit</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modtools/{{$user->id}}">
<span class="font-weight-bold">Mod Tools</span>
</a>
<a class="dropdown-item" href="/i/admin/users/modlogs/{{$user->id}}">
<span class="font-weight-bold">Mod Logs</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/i/admin/users/delete/{{$user->id}}">
<span class="text-danger font-weight-bold">Delete Account</span>
</a>
</div>
</div>
</span>
</div>
<hr>
<div class="row mb-3">
<div class="col-12 col-md-4">
<div class="card shadow-none border">
<div class="card-body text-center">
<img src="{{$profile->avatarUrl()}}" class="box-shadow rounded-circle" width="128px" height="128px">
<p class="mt-3 mb-0 lead">
<span class="font-weight-bold">{{$profile->name}}</span>
</p>
@if($user->is_admin == true)
<p class="mb-0">
<span class="badge badge-danger badge-sm">ADMIN</span>
</p>
@endif
<p class="mb-0 text-center text-muted">
Joined {{$profile->created_at->diffForHumans()}}
</p>
</div>
<table class="table mb-0">
<tbody>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">bookmarks</th>
<td class="text-right font-weight-bold">{{$profile->bookmarks()->count()}}</td>
</tr>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">collections</th>
<td class="text-right font-weight-bold">{{$profile->collections()->count()}}</td>
</tr>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">likes</th>
<td class="text-right font-weight-bold">{{$profile->likes()->count()}}</td>
</tr>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">reports</th>
<td class="text-right font-weight-bold">{{$profile->reports()->count()}}</td>
</tr>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">reported</th>
<td class="text-right font-weight-bold">{{$profile->reported()->count()}}</td>
</tr>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">Active stories</th>
<td class="text-right font-weight-bold">{{$profile->stories()->count()}}</td>
</tr>
<tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">storage used</th>
<td class="text-right font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}<span class="text-muted"> / {{PrettyNumber::size(config('pixelfed.max_account_size') * 1000)}}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-md-8">
<p class="title h4 font-weight-bold mt-2 py-2">Recent Posts</p>
<hr>
<div class="row">
@foreach($profile->statuses()->whereHas('media')->latest()->take(9)->get() as $item)
<div class="col-12 col-md-4 col-sm-6 px-0" style="margin-bottom: 1px;">
<a href="{{$item->url()}}">
<img src="{{$item->thumb(true)}}" width="200px" height="200px">
</a>
</div>
@endforeach
@if($profile->statuses()->whereHas('media')->count() == 0)
<div class="col-12">
<div class="card card-body border shadow-none bg-transparent">
<p class="text-center mb-0 text-muted">No statuses found</p>
</div>
</div>
@endif
</div>
</div>
</div>
@endsection

View file

@ -1,7 +1,7 @@
@component('mail::message')
# Email Confirmation
Hello <b>&commat;{{$verify->user->username}}</b>, please confirm your email address.
Hello <b>{{ '@' . $verify->user->username}}</b>, please confirm your email address.
If you did not create this account, please disregard this email.

View file

@ -0,0 +1,20 @@
@component('mail::message')
# Message from {{ config('pixelfed.domain.app') }}:
@component('mail::panel')
{{$msg}}
@endcomponent
<br>
Regards,<br>
{{ config('pixelfed.domain.app') }}
@component('mail::subcopy')
Please do not reply to this email, this address is not monitored.
@endcomponent
@endcomponent

View file

@ -1,41 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="container px-0 mt-md-4">
<div class="col-12 col-md-8 offset-md-2">
<div class="card shadow-none border">
<div class="card-body">
<p class="mb-0">
<img class="img-thumbnail mr-2" src="{{$profile->avatarUrl()}}" width="24px" height="24px" style="border-radius:24px;">
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$status->profile->url()}}">{{ str_limit($status->profile->username, 15)}}</a></bdi></span>
<span class="comment-text">{!! $status->rendered ?? e($status->caption) !!} <a href="{{$status->url()}}" class="text-dark small font-weight-bold float-right pl-2">{{$status->created_at->diffForHumans(null, true, true ,true)}}</a></span>
</p>
<p class="text-center font-weight-bold text-muted small text-uppercase pt-3">All Comments</p>
<div class="comments">
@foreach($replies as $item)
<p class="mb-2">
<span class="font-weight-bold pr-1">
<img class="img-thumbnail mr-2" src="{{$item->profile->avatarUrl()}}" width="24px" height="24px" style="border-radius:24px;">
<bdi><a class="text-dark" href="{{$item->profile->url()}}">{{ str_limit($item->profile->username, 15)}}</a></bdi>
</span>
<span class="comment-text">
{!! $item->rendered ?? e($item->caption) !!}
<a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right pl-2">
{{$item->created_at->diffForHumans(null, true, true ,true)}}
</a>
</span>
</p>
@endforeach
</div>
</div>
</div>
<div class="mt-2 d-flex justify-content-center">
{{ $replies->links() }}
</div>
</div>
</div>
@endsection

View file

@ -16,7 +16,19 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::get('profiles/edit/{id}', 'AdminController@profileShow');
Route::redirect('users', '/users/list');
Route::get('users/list', 'AdminController@users')->name('admin.users');
Route::get('users/edit/{id}', 'AdminController@editUser');
Route::get('users/show/{id}', 'AdminController@userShow');
Route::get('users/edit/{id}', 'AdminController@userEdit');
Route::post('users/edit/{id}', 'AdminController@userEditSubmit');
Route::get('users/activity/{id}', 'AdminController@userActivity');
Route::get('users/message/{id}', 'AdminController@userMessage');
Route::post('users/message/{id}', 'AdminController@userMessageSend');
Route::get('users/modtools/{id}', 'AdminController@userModTools');
Route::get('users/modlogs/{id}', 'AdminController@userModLogs');
Route::post('users/modlogs/{id}', 'AdminController@userModLogsMessage');
Route::post('users/modlogs/{id}/delete', 'AdminController@userModLogDelete');
Route::get('users/delete/{id}', 'AdminController@userDelete');
Route::post('users/delete/{id}', 'AdminController@userDeleteProcess');
Route::post('users/moderation/update', 'AdminController@userModerate');
Route::get('media', 'AdminController@media')->name('admin.media');
Route::redirect('media/list', '/i/admin/media');
Route::get('media/show/{id}', 'AdminController@mediaShow');