Merge pull request #662 from pixelfed/frontend-ui-refactor

v0.7.0
This commit is contained in:
daniel 2018-12-23 17:55:16 -07:00 committed by GitHub
commit e679e9ae49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 572 additions and 231 deletions

View file

@ -0,0 +1,110 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\{Profile, User};
use DB;
class FixUsernames extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'fix:usernames';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fix invalid usernames';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if(version_compare(config('pixelfed.version'), '0.7.2') !== -1) {
$this->info('This command is only for versions lower than 0.7.2');
return;
}
$this->info('Collecting data ...');
$affected = collect([]);
$users = User::chunk(100, function($users) use($affected) {
foreach($users as $user) {
$val = str_replace(['-', '_'], '', $user->username);
if(!ctype_alnum($val)) {
$this->info('Found invalid username: ' . $user->username);
$affected->push($user);
}
}
});
if($affected->count() > 0) {
$this->info('Found: ' . $affected->count() . ' affected usernames');
$opts = [
'Random replace (assigns random username)',
'Best try replace (assigns alpha numeric username)',
'Manual replace (manually set username)'
];
foreach($affected as $u) {
$old = $u->username;
$opt = $this->choice('Select fix method:', $opts, 0);
switch ($opt) {
case $opts[0]:
$new = "user_" . str_random(6);
$this->info('New username: ' . $new);
break;
case $opts[1]:
$new = filter_var($old, FILTER_SANITIZE_STRING|FILTER_FLAG_STRIP_LOW);
if(strlen($new) < 6) {
$new = $new . '_' . str_random(4);
}
$this->info('New username: ' . $new);
break;
case $opts[2]:
$new = $this->ask('Enter new username:');
$this->info('New username: ' . $new);
break;
default:
$new = "user_" . str_random(6);
break;
}
DB::transaction(function() use($u, $new) {
$profile = $u->profile;
$profile->username = $new;
$u->username = $new;
$u->save();
$profile->save();
});
$this->info('Selected: ' . $opt);
}
$this->info('Fixed ' . $affected->count() . ' usernames!');
}
}
}

View file

@ -26,7 +26,7 @@ class Follower extends Model
public function permalink($append = null)
{
$path = $this->actor->permalink("/follow/{$this->id}{$append}");
$path = $this->actor->permalink("#accepts/follows/{$this->id}{$append}");
return url($path);
}

View file

@ -3,15 +3,16 @@
namespace App\Http\Controllers;
use App\Media;
use App\Like;
use App\Profile;
use App\Report;
use App\Status;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Admin\{
AdminReportController
};
use Jackiedo\DotenvEditor\DotenvEditor;
use App\Http\Controllers\Admin\AdminReportController;
use App\Util\Lexer\PrettyNumber;
class AdminController extends Controller
{
@ -30,15 +31,16 @@ class AdminController extends Controller
public function users(Request $request)
{
$stats = [];
$users = User::orderBy('id', 'desc')->paginate(10);
$col = $request->query('col') ?? 'id';
$dir = $request->query('dir') ?? 'desc';
$stats = $this->collectUserStats($request);
$users = User::withCount('statuses')->orderBy($col, $dir)->paginate(10);
return view('admin.users.home', compact('users', 'stats'));
}
public function editUser(Request $request, $id)
{
$user = User::find($id);
$user = User::findOrFail($id);
$profile = $user->profile;
return view('admin.users.edit', compact('user', 'profile'));
}
@ -98,7 +100,7 @@ class AdminController extends Controller
'remote' => Profile::whereNotNull('remote_url')->count()
];
$stats['avg'] = [
'age' => 0,
'likes' => floor(Like::average('profile_id')),
'posts' => floor(Status::avg('profile_id'))
];
return $stats;

View file

@ -123,7 +123,7 @@ class BaseApiController extends Controller
public function avatarUpdate(Request $request)
{
$this->validate($request, [
'upload' => 'required|mimes:jpeg,png,gif|max:2000',
'upload' => 'required|mimes:jpeg,png,gif|max:'.config('pixelfed.max_avatar_size'),
]);
try {

View file

@ -55,7 +55,6 @@ class RegisterController extends Controller
$this->validateUsername($data['username']);
$usernameRules = [
'required',
'alpha_dash',
'min:2',
'max:15',
'unique:users',
@ -63,6 +62,10 @@ class RegisterController extends Controller
if (!ctype_alpha($value[0])) {
return $fail($attribute.' is invalid. Username must be alpha-numeric and start with a letter.');
}
$val = str_replace(['-', '_'], '', $value);
if(!ctype_alnum($val)) {
return $fail($attribute . ' is invalid. Username must be alpha-numeric.');
}
},
];

View file

@ -19,7 +19,7 @@ class AvatarController extends Controller
public function store(Request $request)
{
$this->validate($request, [
'avatar' => 'required|mimes:jpeg,png|max:2000',
'avatar' => 'required|mimes:jpeg,png|max:'.config('pixelfed.max_avatar_size'),
]);
try {

View file

@ -35,9 +35,9 @@ class CommentController extends Controller
abort(403);
}
$this->validate($request, [
'item' => 'required|integer',
'comment' => 'required|string|max:500',
]);
'item' => 'required|integer',
'comment' => 'required|string|max:500',
]);
$comment = $request->input('comment');
$statusId = $request->item;

View file

@ -36,6 +36,8 @@ class DiscoverController extends Controller
->firstOrFail();
$posts = $tag->posts()
->whereNull('url')
->whereNull('uri')
->whereHas('media')
->withCount(['likes', 'comments'])
->whereIsNsfw(false)

View file

@ -14,6 +14,7 @@ use Carbon\Carbon;
use Illuminate\Http\Request;
use League\Fractal;
use App\Util\ActivityPub\Helpers;
use App\Util\ActivityPub\HttpSignature;
class FederationController extends Controller
{
@ -113,7 +114,9 @@ class FederationController extends Controller
];
});
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
return response()->json($res, 200, [
'Access-Control-Allow-Origin' => '*'
]);
}
public function webfinger(Request $request)
@ -167,6 +170,29 @@ XML;
public function userInbox(Request $request, $username)
{
if (config('pixelfed.activitypub_enabled') == false) {
abort(403);
}
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$body = $request->getContent();
$bodyDecoded = json_decode($body, true);
$signature = $request->header('signature');
if(!$signature) {
abort(400, 'Missing signature header');
}
$signatureData = HttpSignature::parseSignatureHeader($signature);
$actor = Profile::whereKeyId($signatureData['keyId'])->first();
if(!$actor) {
$actor = Helpers::profileFirstOrNew($bodyDecoded['actor']);
}
$pkey = openssl_pkey_get_public($actor->public_key);
$inboxPath = "/users/{$profile->username}/inbox";
list($verified, $headers) = HTTPSignature::verify($pkey, $signatureData, $request->headers->all(), $inboxPath, $body);
if($verified !== 1) {
abort(400, 'Invalid signature.');
}
InboxWorker::dispatch($request->headers->all(), $profile, $bodyDecoded);
return;
}

View file

@ -15,6 +15,7 @@ use App\Http\Controllers\Settings\{
PrivacySettings,
SecuritySettings
};
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
class SettingsController extends Controller
{
@ -43,7 +44,7 @@ class SettingsController extends Controller
'optimize_screen_reader',
'high_contrast_mode',
'video_autoplay',
];
];
foreach ($fields as $field) {
$form = $request->input($field);
if ($form == 'on') {
@ -130,5 +131,26 @@ class SettingsController extends Controller
{
return view('settings.developers');
}
public function removeAccountTemporary(Request $request)
{
return view('settings.remove.temporary');
}
public function removeAccountPermanent(Request $request)
{
return view('settings.remove.permanent');
}
public function removeAccountPermanentSubmit(Request $request)
{
$user = Auth::user();
if($user->is_admin == true) {
return abort(400, 'You cannot delete an admin account.');
}
DeleteAccountPipeline::dispatch($user);
Auth::logout();
return redirect('/');
}
}

View file

@ -7,6 +7,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use DB;
use App\{
AccountLog,
Activity,
@ -57,109 +58,76 @@ class DeleteAccountPipeline implements ShouldQueue
public function handle()
{
$user = $this->user;
$this->deleteAccountLogs($user);
$this->deleteActivities($user);
$this->deleteAvatar($user);
$this->deleteBookmarks($user);
$this->deleteEmailVerification($user);
$this->deleteFollowRequests($user);
$this->deleteFollowers($user);
$this->deleteLikes($user);
$this->deleteMedia($user);
$this->deleteMentions($user);
$this->deleteNotifications($user);
// todo send Delete to every known instance sharedInbox
}
public function deleteAccountLogs($user)
{
AccountLog::chunk(200, function($logs) use ($user) {
foreach($logs as $log) {
if($log->user_id == $user->id) {
$log->delete();
DB::transaction(function() use ($user) {
AccountLog::chunk(200, function($logs) use ($user) {
foreach($logs as $log) {
if($log->user_id == $user->id) {
$log->forceDelete();
}
}
});
if($user->profile) {
$avatar = $user->profile->avatar;
if(is_file($avatar->media_path)) {
unlink($avatar->media_path);
}
if(is_file($avatar->thumb_path)) {
unlink($avatar->thumb_path);
}
$avatar->forceDelete();
}
Bookmark::whereProfileId($user->profile->id)->forceDelete();
EmailVerification::whereUserId($user->id)->forceDelete();
$id = $user->profile->id;
FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->forceDelete();
Follower::whereProfileId($id)->orWhere('following_id', $id)->forceDelete();
Like::whereProfileId($id)->forceDelete();
$medias = Media::whereUserId($user->id)->get();
foreach($medias as $media) {
$path = $media->media_path;
$thumb = $media->thumbnail_path;
if(is_file($path)) {
unlink($path);
}
if(is_file($thumb)) {
unlink($thumb);
}
$media->forceDelete();
}
Mention::whereProfileId($user->profile->id)->forceDelete();
Notification::whereProfileId($id)->orWhere('actor_id', $id)->forceDelete();
Status::whereProfileId($user->profile->id)->forceDelete();
Report::whereUserId($user->id)->forceDelete();
$this->deleteProfile($user);
});
}
public function deleteActivities($user)
{
// todo after AP
public function deleteProfile($user) {
DB::transaction(function() use ($user) {
Profile::whereUserId($user->id)->delete();
$this->deleteUser($user);
});
}
public function deleteAvatar($user)
{
$avatar = $user->profile->avatar;
public function deleteUser($user) {
if(is_file($avatar->media_path)) {
unlink($avatar->media_path);
}
if(is_file($avatar->thumb_path)) {
unlink($avatar->thumb_path);
}
$avatar->delete();
DB::transaction(function() use ($user) {
UserFilter::whereUserId($user->id)->forceDelete();
UserSetting::whereUserId($user->id)->forceDelete();
$user->forceDelete();
});
}
public function deleteBookmarks($user)
{
Bookmark::whereProfileId($user->profile->id)->delete();
}
public function deleteEmailVerification($user)
{
EmailVerification::whereUserId($user->id)->delete();
}
public function deleteFollowRequests($user)
{
$id = $user->profile->id;
FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->delete();
}
public function deleteFollowers($user)
{
$id = $user->profile->id;
Follower::whereProfileId($id)->orWhere('following_id', $id)->delete();
}
public function deleteLikes($user)
{
$id = $user->profile->id;
Like::whereProfileId($id)->delete();
}
public function deleteMedia($user)
{
$medias = Media::whereUserId($user->id)->get();
foreach($medias as $media) {
$path = $media->media_path;
$thumb = $media->thumbnail_path;
if(is_file($path)) {
unlink($path);
}
if(is_file($thumb)) {
unlink($thumb);
}
$media->delete();
}
}
public function deleteMentions($user)
{
Mention::whereProfileId($user->profile->id)->delete();
}
public function deleteNotifications($user)
{
$id = $user->profile->id;
Notification::whereProfileId($id)->orWhere('actor_id', $id)->delete();
}
public function deleteProfile($user) {}
public function deleteReports($user) {}
public function deleteStatuses($user) {}
public function deleteUser($user) {}
}

View file

@ -25,6 +25,11 @@ class AuthLogin
public function handle($event)
{
$user = $event->user;
if(!$user) {
return;
}
if (empty($user->settings)) {
DB::transaction(function() use($user) {
UserSetting::firstOrCreate([

View file

@ -16,6 +16,8 @@ class Notification extends Model
*/
protected $dates = ['deleted_at'];
protected $fillable = ['*'];
public function actor()
{
return $this->belongsTo(Profile::class, 'actor_id', 'id');

View file

@ -2,27 +2,16 @@
namespace App;
use Auth, Cache, Storage;
use App\Util\Lexer\PrettyNumber;
use Auth;
use Cache;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Storage;
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
class Profile extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $hidden = [
'private_key',
];
protected $hidden = ['private_key'];
protected $visible = ['username', 'name'];
public function user()
@ -30,26 +19,19 @@ class Profile extends Model
return $this->belongsTo(User::class);
}
public function url($suffix = '')
public function url($suffix = null)
{
if ($this->remote_url) {
return $this->remote_url;
} else {
return url($this->username.$suffix);
}
return $this->remote_url ?? url($this->username . $suffix);
}
public function localUrl($suffix = '')
public function localUrl($suffix = null)
{
return url($this->username.$suffix);
return url($this->username . $suffix);
}
public function permalink($suffix = '')
public function permalink($suffix = null)
{
if($this->remote_url) {
return $this->remote_url;
}
return url('users/'.$this->username.$suffix);
return $this->remote_url ?? url('users/' . $this->username . $suffix);
}
public function emailUrl()

View file

@ -90,6 +90,9 @@ class Status extends Model
public function url()
{
if($this->url) {
return $this->url;
}
$id = $this->id;
$username = $this->profile->username;
$path = config('app.url')."/p/{$username}/{$id}";

View file

@ -7,13 +7,13 @@ use League\Fractal;
class Like extends Fractal\TransformerAbstract
{
public function transform(LikeModel $like)
{
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'type' => 'Like',
'actor' => $like->actor->permalink(),
'object' => $like->status->url()
];
}
public function transform(LikeModel $like)
{
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'type' => 'Like',
'actor' => $like->actor->permalink(),
'object' => $like->status->url()
];
}
}

View file

@ -54,6 +54,14 @@ class User extends Authenticatable
return $this->hasOne(UserSetting::class);
}
public function statuses()
{
return $this->hasManyThrough(
Status::class,
Profile::class
);
}
public function receivesBroadcastNotificationsOn()
{
return 'App.User.'.$this->id;

View file

@ -22,6 +22,7 @@ use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Util\HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use App\Util\ActivityPub\HttpSignature;
class Helpers {
@ -215,13 +216,14 @@ class Helpers {
} else {
$reply_to = null;
}
$ts = is_array($res['published']) ? $res['published'][0] : $res['published'];
$status = new Status;
$status->profile_id = $profile->id;
$status->url = $url;
$status->uri = $url;
$status->caption = strip_tags($res['content']);
$status->rendered = Purify::clean($res['content']);
$status->created_at = Carbon::parse($res['published']);
$status->created_at = Carbon::parse($ts);
$status->in_reply_to_id = $reply_to;
$status->local = false;
$status->save();
@ -307,72 +309,24 @@ class Helpers {
public static function sendSignedObject($senderProfile, $url, $body)
{
$profile = $senderProfile;
$keyId = $profile->keyId();
$payload = json_encode($body);
$headers = HttpSignature::sign($senderProfile, $url, $body);
$date = new \DateTime('UTC');
$date = $date->format('D, d M Y H:i:s \G\M\T');
$host = parse_url($url, PHP_URL_HOST);
$path = parse_url($url, PHP_URL_PATH);
$headers = [
'date' => $date,
'host' => $host,
'content-type' => 'application/activity+json',
];
$context = new Context([
'keys' => [$profile->keyId() => $profile->private_key],
'algorithm' => 'rsa-sha256',
'headers' => ['(request-target)', 'date', 'host', 'content-type'],
]);
$handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
$client = new Client(['handler' => $handlerStack]);
$response = $client->request('POST', $url, ['headers' => $headers, 'json' => $body]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
return;
}
private static function _headersToSigningString($headers) {
return implode("\n", array_map(function($k, $v){
return strtolower($k).': '.$v;
}, array_keys($headers), $headers));
}
public static function validateSignature($request, $payload = null)
{
$date = Carbon::parse($request['date']);
$min = Carbon::now()->subHours(13);
$max = Carbon::now()->addHours(13);
if($date->gt($min) == false || $date->lt($max) == false) {
return false;
}
$json = json_encode($payload);
$digest = base64_encode(hash('sha256', $json, true));
$parts = explode(',', $request['signature']);
$signatureData = [];
foreach($parts as $part) {
if(preg_match('/(.+)="(.+)"/', $part, $match)) {
$signatureData[$match[1]] = $match[2];
}
}
$actor = $payload['actor'];
$profile = self::profileFirstOrNew($actor, true);
if(!$profile) {
return false;
}
$publicKey = $profile->public_key;
$path = $request['path'];
$host = $request['host'];
$signingString = "(request-target): post {$path}".PHP_EOL.
"host: {$host}".PHP_EOL.
"date: {$request['date']}".PHP_EOL.
"digest: {$request['digest']}".PHP_EOL.
"content-type: {$request['contentType']}";
$verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
return (bool) $verified;
}
public static function fetchPublicKey()

View file

@ -0,0 +1,121 @@
<?php
namespace App\Util\ActivityPub;
use Log;
use App\Profile;
use \DateTime;
class HttpSignature {
/*
* source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
* thanks aaronpk!
*/
public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) {
if($body) {
$digest = self::_digest($body);
}
$user = $profile;
$headers = self::_headersToSign($url, $body ? $digest : false);
$headers = array_merge($headers, $addlHeaders);
$stringToSign = self::_headersToSigningString($headers);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
$key = openssl_pkey_get_private($user->private_key);
openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
$signature = base64_encode($signature);
$signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
unset($headers['(request-target)']);
$headers['Signature'] = $signatureHeader;
return self::_headersToCurlArray($headers);
}
public static function parseSignatureHeader($signature) {
$parts = explode(',', $signature);
$signatureData = [];
foreach($parts as $part) {
if(preg_match('/(.+)="(.+)"/', $part, $match)) {
$signatureData[$match[1]] = $match[2];
}
}
if(!isset($signatureData['keyId'])) {
return [
'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData))
];
}
if(!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
return [
'error' => 'keyId is not a URL: '.$signatureData['keyId']
];
}
if(!isset($signatureData['headers']) || !isset($signatureData['signature'])) {
return [
'error' => 'Signature is missing headers or signature parts'
];
}
return $signatureData;
}
public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) {
$digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
$headersToSign = [];
foreach(explode(' ',$signatureData['headers']) as $h) {
if($h == '(request-target)') {
$headersToSign[$h] = 'post '.$path;
} elseif($h == 'digest') {
$headersToSign[$h] = $digest;
} elseif(isset($inputHeaders[$h][0])) {
$headersToSign[$h] = $inputHeaders[$h][0];
}
}
$signingString = self::_headersToSigningString($headersToSign);
$verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
return [$verified, $signingString];
}
private static function _headersToSigningString($headers) {
return implode("\n", array_map(function($k, $v){
return strtolower($k).': '.$v;
}, array_keys($headers), $headers));
}
private static function _headersToCurlArray($headers) {
return array_map(function($k, $v){
return "$k: $v";
}, array_keys($headers), $headers);
}
private static function _digest($body) {
if(is_array($body)) {
$body = json_encode($body);
}
return base64_encode(hash('sha256', $body, true));
}
protected static function _headersToSign($url, $digest = false) {
$date = new DateTime('UTC');
$headers = [
'(request-target)' => 'post '.parse_url($url, PHP_URL_PATH),
'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
'Host' => parse_url($url, PHP_URL_HOST),
'Content-Type' => 'application/activity+json',
];
if($digest) {
$headers['Digest'] = 'SHA-256='.$digest;
}
return $headers;
}
}

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.6.1',
'version' => '0.7.1',
/*
|--------------------------------------------------------------------------
@ -107,6 +107,16 @@ return [
*/
'max_photo_size' => env('MAX_PHOTO_SIZE', 15000),
/*
|--------------------------------------------------------------------------
| Avatar file size limit
|--------------------------------------------------------------------------
|
| Update the max avatar size, in KB.
|
*/
'max_avatar_size' => (int) env('MAX_AVATAR_SIZE', 2000),
/*
|--------------------------------------------------------------------------
| Caption limit

View file

@ -70,9 +70,9 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item font-weight-ultralight text-truncate" href="{{Auth::user()->url()}}">
<img class="img-thumbnail rounded-circle pr-1" src="{{Auth::user()->profile->avatarUrl()}}" width="32px">
<img class="rounded-circle box-shadow mr-1" src="{{Auth::user()->profile->avatarUrl()}}" width="26px" height="26px">
&commat;{{Auth::user()->username}}
<p class="small mb-0 text-muted">{{__('navmenu.viewMyProfile')}}</p>
<p class="small mb-0 text-muted text-center">{{__('navmenu.viewMyProfile')}}</p>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="{{route('timeline.personal')}}">

View file

@ -3,7 +3,7 @@
<div class="row">
<div class="col-12 col-md-4 d-flex">
<div class="profile-avatar mx-auto">
<img class="img-thumbnail" src="{{$user->avatarUrl()}}" style="border-radius:100%;" width="172px">
<img class="rounded-circle box-shadow" src="{{$user->avatarUrl()}}" width="172px" height="172px">
</div>
</div>
<div class="col-12 col-md-8 d-flex align-items-center">

View file

@ -3,7 +3,7 @@
<div class="row">
<div class="col-12 col-md-4 d-flex">
<div class="profile-avatar mx-auto">
<img class="img-thumbnail" src="{{$user->avatarUrl()}}" style="border-radius:100%;" width="172px">
<img class="rounded-circle box-shadow" src="{{$user->avatarUrl()}}" width="172px" height="172px">
</div>
</div>
<div class="col-12 col-md-8 d-flex align-items-center">
@ -36,24 +36,27 @@
</form>
</span>
@endif
{{-- TODO: Implement action dropdown
<span class="pl-4">
{{-- <span class="pl-4">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="icon-options"></i>
<button class="btn btn-link text-muted dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="text-decoration: none;">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#">Report User</a>
<a class="dropdown-item" href="#">Block User</a>
<a class="dropdown-item font-weight-bold" href="#">Report User</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="#">Mute User</a>
<a class="dropdown-item font-weight-bold" href="#">Block User</a>
<a class="dropdown-item font-weight-bold mute-users" href="#">Mute User & User Followers</a>
<a class="dropdown-item font-weight-bold" href="#">Block User & User Followers</a>
</div>
</div>
</span>
--}}
--}}
</div>
<div class="profile-stats pb-3 d-inline-flex lead">
<div class="font-weight-light pr-5">
<a class="text-dark" href="{{$user->url()}}">
<span class="font-weight-bold">{{$user->statuses()->whereNull('reblog_of_id')->whereNull('in_reply_to_id')->count()}}</span>
<span class="font-weight-bold">{{$user->statusCount()}}</span>
Posts
</a>
</div>
@ -74,13 +77,13 @@
</div>
@endif
</div>
<p class="lead mb-0">
<span class="font-weight-bold">{{$user->name}}</span>
<p class="lead mb-0 d-flex align-items-center">
<span class="font-weight-bold pr-3">{{$user->name}}</span>
@if($user->remote_url)
<span class="badge badge-info">REMOTE PROFILE</span>
<span class="btn btn-outline-secondary btn-sm py-0">REMOTE PROFILE</span>
@endif
</p>
<p class="mb-0 lead">{{$user->bio}}</p>
<div class="mb-0 lead" v-pre>{!!str_limit($user->bio, 127)!!}</div>
<p class="mb-0"><a href="{{$user->website}}" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{str_limit($user->website, 30)}}</a></p>
</div>
</div>

View file

@ -10,11 +10,12 @@
@csrf
<div class="form-group row">
<div class="col-sm-3">
<img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" class="rounded-circle img-thumbnail float-right">
<img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right">
</div>
<div class="col-sm-9">
<p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p>
<p><a href="#" class="font-weight-bold change-profile-photo">Change Profile Photo</a></p>
<p class="mb-0"><a href="#" class="font-weight-bold change-profile-photo">Change Profile Photo</a></p>
<p><span class="small font-weight-bold">Max avatar size: <span id="maxAvatarSize"></span></span></p>
</div>
</div>
<div class="form-group row">
@ -60,6 +61,25 @@
</p>
</div>
</div>
<div class="pt-5">
<p class="font-weight-bold text-muted text-center">Storage Usage</p>
</div>
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Storage Used</label>
<div class="col-sm-9">
<div class="progress mt-2">
<div class="progress-bar" role="progressbar" style="width: {{$storage['percentUsed']}}%" aria-valuenow="{{$storage['percentUsed']}}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="help-text">
<span class="small text-muted">
{{$storage['percentUsed']}}% used
</span>
<span class="small text-muted float-right">
{{$storage['usedPretty']}} / {{$storage['limitPretty']}}
</span>
</div>
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-12 text-right">
@ -96,6 +116,8 @@
$('.bio-counter').html(val);
});
$('#maxAvatarSize').text(filesize({{config('pixelfed.max_avatar_size') * 1024}}, {round: 0}));
$(document).on('click', '.change-profile-photo', function(e) {
e.preventDefault();
swal({
@ -103,7 +125,7 @@
content: {
element: 'input',
attributes: {
placeholder: 'Upload your photo',
placeholder: 'Upload your photo.',
type: 'file',
name: 'photoUpload',
id: 'photoUploadInput'

View file

@ -0,0 +1,51 @@
@extends('settings.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold">Delete Your Account</h3>
</div>
<hr>
<div class="mt-3">
<p>Hi <span class="font-weight-bold">{{Auth::user()->username}}</span>,</p>
<p>We're sorry to hear you'd like to delete your account.</p>
<p class="pb-1">If you're just looking to take a break, you can always <a href="{{route('settings.remove.temporary')}}">temporarily disable</a> your account instead.</p>
<p class="">When you press the button below, your photos, comments, likes, friendships and all other data will be removed permanently and will not be recoverable. If you decide to create another Pixelfed account in the future, you cannot sign up with the same username again on this instance.</p>
<div class="alert alert-danger my-5">
<span class="font-weight-bold">Warning:</span> Some remote servers may contain your public data (statuses, avatars, ect) and will not be deleted until federation support is launched.
</div>
<p>
<form method="post">
@csrf
<div class="custom-control custom-checkbox mb-3">
<input type="checkbox" class="custom-control-input" id="confirm-check">
<label class="custom-control-label font-weight-bold" for="confirm-check">I confirm that this action is not reversible, and will result in the permanent deletion of my account.</label>
</div>
<button type="submit" class="btn btn-danger font-weight-bold py-0 delete-btn" disabled="">Permanently delete my account</button>
</form>
</p>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('#confirm-check').on('change', function() {
let el = $(this);
let state = el.prop('checked');
if(state == true) {
$('.delete-btn').removeAttr('disabled');
} else {
$('.delete-btn').attr('disabled', '');
}
});
});
</script>
@endpush

View file

@ -16,7 +16,7 @@
<div class="collapse" id="collapse1">
<div>
To create an account using a web browser:
<ol>
<ol class="font-weight-light">
<li>Go to <a href="{{route('settings')}}">{{route('settings')}}</a>.</li>
<li>You should see the <span class="font-weight-bold">Name</span>, <span class="font-weight-bold">Website</span>, and <span class="font-weight-bold">Bio</span> fields.</li>
<li>Change the desired fields, and then click the <span class="font-weight-bold">Submit</span> button.</li>
@ -45,7 +45,7 @@
<div class="collapse" id="collapse3">
<div>
To change your account visibility:
<ol>
<ol class="font-weight-light">
<li>Go to <a href="{{route('settings.privacy')}}">{{route('settings.privacy')}}</a>.</li>
<li>Check the <span class="font-weight-bold">Private Account</span> checkbox.</li>
<li>The confirmation modal will popup and ask you if you want to keep existing followers and disable new follow requests</li>
@ -99,7 +99,7 @@
</div>
</p> --}}
<hr>
<p class="h5 text-muted font-weight-light">Security</p>
<p class="h5 text-muted font-weight-light" id="security">Security</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#sec-collapse8" role="button" aria-expanded="false" aria-controls="sec-collapse8">
<i class="fas fa-chevron-down mr-2"></i>
@ -108,7 +108,7 @@
<div class="collapse" id="sec-collapse8">
<div>
Here are some recommendations to keep your account secure:
<ul class="font-weight-bold">
<ul class="font-weight-light">
<li>Pick a strong password, don't re-use it on other websites</li>
<li>Never share your password</li>
<li>Remember to log out on public computers or devices</li>
@ -140,4 +140,43 @@
</div>
</div>
</p>
{{-- <hr>
<p class="h5 text-muted font-weight-light" id="delete-your-account">Delete Your Account</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#del-collapse1" role="button" aria-expanded="false" aria-controls="del-collapse1">
<i class="fas fa-chevron-down mr-2"></i>
How do I temporarily disable my account?
</a>
<div class="collapse" id="del-collapse1">
<div>
<p>If you temporarily disable your account, your profile, photos, comments and likes will be hidden until you reactivate it by logging back in. To temporarily disable your account:</p>
<ol class="font-weight-light">
<li>Log into <a href="{{config('app.url')}}">{{config('pixelfed.domain.app')}}</a></li>
<li>Tap or click the <i class="far fa-user text-dark"></i> menu and select <span class="font-weight-bold text-dark"><i class="fas fa-cog pr-1"></i> Settings</span></li>
<li>Scroll down and click on the <span class="font-weight-bold">Temporarily Disable Account</span> link.</li>
<li>Follow the instructions on the next page.</li>
</ol>
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#del-collapse2" role="button" aria-expanded="false" aria-controls="del-collapse2">
<i class="fas fa-chevron-down mr-2"></i>
How do I delete my account?
</a>
<div class="collapse" id="del-collapse2">
<div>
<div class="bg-light p-3 mb-4">
<p class="mb-0">When you delete your account, your profile, photos, videos, comments, likes and followers will be permanently removed. If you'd just like to take a break, you can <a href="{{route('settings.remove.temporary')}}">temporarily disable</a> your account instead.</p>
</div>
<p>After you delete your account, you can't sign up again with the same username on this instance or add that username to another account on this instance, and we can't reactivate deleted accounts.</p>
<p>To permanently delete your account:</p>
<ol class="font-weight-light">
<li>Go to <a href="{{route('settings.remove.permanent')}}">the <span class="font-weight-bold">Delete Your Account</span> page</a>. If you're not logged into pixelfed on the web, you'll be asked to log in first. You can't delete your account from within a mobile app.</li>
<li>Confirm your account password.</li>
<li>On the <span class="font-weight-bold">Delete Your Account</span> page click or tap on the <span>Permanently Delete My Account</span> button.</li>
</ol>
</div>
</div>
</p> --}}
@endsection

View file

@ -132,6 +132,14 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate')->middleware('throttle:100,1440');
Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances');
// Todo: Release in 0.7.2
// Route::group(['prefix' => 'remove', 'middleware' => 'dangerzone'], function() {
// Route::get('request/temporary', 'SettingsController@removeAccountTemporary')->name('settings.remove.temporary');
// Route::post('request/temporary', 'SettingsController@removeAccountTemporarySubmit');
// Route::get('request/permanent', 'SettingsController@removeAccountPermanent')->name('settings.remove.permanent');
// Route::post('request/permanent', 'SettingsController@removeAccountPermanentSubmit');
// });
Route::group(['prefix' => 'security', 'middleware' => 'dangerzone'], function() {
Route::get(
'/',