Browse Source

Merge pull request #2033 from pixelfed/staging

Improved Admin Dashboard Security
daniel 5 years ago
parent
commit
cdb8784d81
31 changed files with 1449 additions and 317 deletions
  1. 6 0
      CHANGELOG.md
  2. 13 1
      app/Http/Controllers/AccountController.php
  3. 284 0
      app/Http/Controllers/Admin/AdminUserController.php
  4. 19 32
      app/Http/Controllers/AdminController.php
  5. 1 1
      app/Http/Controllers/Api/ApiV1Controller.php
  6. 4 0
      app/Http/Controllers/Api/BaseApiController.php
  7. 3 3
      app/Http/Controllers/CollectionController.php
  8. 1 5
      app/Http/Controllers/CommentController.php
  9. 1 1
      app/Http/Controllers/NewsroomController.php
  10. 10 2
      app/Http/Controllers/TimelineController.php
  11. 6 0
      app/Http/Middleware/DangerZone.php
  12. 37 0
      app/Mail/AdminMessage.php
  13. 48 0
      app/ModLog.php
  14. 98 0
      app/Services/ModLogService.php
  15. 4 3
      app/Transformer/Api/StoryItemTransformer.php
  16. 3 6
      app/Transformer/Api/StoryTransformer.php
  17. 5 0
      app/User.php
  18. 2 0
      app/Util/Site/Config.php
  19. 40 0
      database/migrations/2020_02_14_063209_create_mod_logs_table.php
  20. 84 0
      resources/views/admin/users/activity.blade.php
  21. 74 0
      resources/views/admin/users/delete.blade.php
  22. 95 96
      resources/views/admin/users/edit.blade.php
  23. 95 124
      resources/views/admin/users/home.blade.php
  24. 112 0
      resources/views/admin/users/message.blade.php
  25. 159 0
      resources/views/admin/users/modlogs.blade.php
  26. 90 0
      resources/views/admin/users/modtools.blade.php
  27. 121 0
      resources/views/admin/users/show.blade.php
  28. 1 1
      resources/views/emails/confirm_email.blade.php
  29. 20 0
      resources/views/emails/notification/admin_message.blade.php
  30. 0 41
      resources/views/status/comments.blade.php
  31. 13 1
      routes/web.php

+ 6 - 0
CHANGELOG.md

@@ -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

+ 13 - 1
app/Http/Controllers/AccountController.php

@@ -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);

+ 284 - 0
app/Http/Controllers/Admin/AdminUserController.php

@@ -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);
+	}
+}

+ 19 - 32
app/Http/Controllers/AdminController.php

@@ -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'));
 	}

+ 1 - 1
app/Http/Controllers/Api/ApiV1Controller.php

@@ -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'
         ]);

+ 4 - 0
app/Http/Controllers/Api/BaseApiController.php

@@ -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);

+ 3 - 3
app/Http/Controllers/CollectionController.php

@@ -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 : [];
 

+ 1 - 5
app/Http/Controllers/CommentController.php

@@ -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)

+ 1 - 1
app/Http/Controllers/NewsroomController.php

@@ -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')

+ 10 - 2
app/Http/Controllers/TimelineController.php

@@ -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'));
     }
 }

+ 6 - 0
app/Http/Middleware/DangerZone.php

@@ -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 - 0
app/Mail/AdminMessage.php

@@ -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 - 0
app/ModLog.php

@@ -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;
+	}
+}

+ 98 - 0
app/Services/ModLogService.php

@@ -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;
+		}
+	}
+}

+ 4 - 3
app/Transformer/Api/StoryItemTransformer.php

@@ -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(),
         ];
     }

+ 3 - 6
app/Transformer/Api/StoryTransformer.php

@@ -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());
     }
 
 }

+ 5 - 0
app/User.php

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

+ 2 - 0
app/Util/Site/Config.php

@@ -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')

+ 40 - 0
database/migrations/2020_02_14_063209_create_mod_logs_table.php

@@ -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');
+    }
+}

+ 84 - 0
resources/views/admin/users/activity.blade.php

@@ -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

+ 74 - 0
resources/views/admin/users/delete.blade.php

@@ -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

+ 95 - 96
resources/views/admin/users/edit.blade.php

@@ -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

+ 95 - 124
resources/views/admin/users/home.blade.php

@@ -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

+ 112 - 0
resources/views/admin/users/message.blade.php

@@ -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

+ 159 - 0
resources/views/admin/users/modlogs.blade.php

@@ -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

+ 90 - 0
resources/views/admin/users/modtools.blade.php

@@ -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

+ 121 - 0
resources/views/admin/users/show.blade.php

@@ -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

+ 1 - 1
resources/views/emails/confirm_email.blade.php

@@ -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.
 

+ 20 - 0
resources/views/emails/notification/admin_message.blade.php

@@ -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
+

+ 0 - 41
resources/views/status/comments.blade.php

@@ -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

+ 13 - 1
routes/web.php

@@ -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');