Merge pull request #3829 from pixelfed/staging

In-App Registration
This commit is contained in:
daniel 2022-12-01 20:32:43 -07:00 committed by GitHub
commit e7a8dec1a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 11612 additions and 1775 deletions

View file

@ -3,9 +3,11 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.4...dev)
### New Features
- Mobile App Registration ([#3829](https://github.com/pixelfed/pixelfed/pull/3829))
- Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705))
- Server Directory ([#3762](https://github.com/pixelfed/pixelfed/pull/3762))
- Manually verify email address (php artisan user:verifyemail) ([682f5f0f](https://github.com/pixelfed/pixelfed/commit/682f5f0f))
- Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36))
### Updates
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
@ -26,8 +28,7 @@
- Update landing view, add `app.name` and `app.short_description` for better customizability ([bda9d16b](https://github.com/pixelfed/pixelfed/commit/bda9d16b))
- Update Profile, fix avatarUrl paths. Fixes #3559 #3634 ([989e4249](https://github.com/pixelfed/pixelfed/commit/989e4249))
- Update InboxPipeline, bump request timeout from 5s to 60s ([bb120019](https://github.com/pixelfed/pixelfed/commit/bb120019))
- Update web routes, fix missing hom route ([a9f4ddfc](https://github.com/pixelfed/pixelfed/commit/a9f4ddfc))
- ([](https://github.com/pixelfed/pixelfed/commit/))
- Update web routes, fix missing home route ([a9f4ddfc](https://github.com/pixelfed/pixelfed/commit/a9f4ddfc))
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)

View file

@ -0,0 +1,123 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Avatar;
use App\User;
use Storage;
use App\Util\Lexer\PrettyNumber;
class AvatarStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'avatar:storage';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Manage avatar storage';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Pixelfed Avatar Storage Manager');
$this->line(' ');
$segments = [
[
'Local',
Avatar::whereNull('is_remote')->count(),
PrettyNumber::size(Avatar::whereNull('is_remote')->sum('size'))
],
[
'Remote',
Avatar::whereNotNull('is_remote')->count(),
PrettyNumber::size(Avatar::whereNotNull('is_remote')->sum('size'))
],
[
'Cached (CDN)',
Avatar::whereNotNull('cdn_url')->count(),
PrettyNumber::size(Avatar::whereNotNull('cdn_url')->sum('size'))
],
[
'Uncached',
Avatar::whereNull('is_remote')->whereNull('cdn_url')->count(),
PrettyNumber::size(Avatar::whereNull('is_remote')->whereNull('cdn_url')->sum('size'))
],
[
'------------',
'----------',
'-----'
],
[
'Total',
Avatar::count(),
PrettyNumber::size(Avatar::sum('size'))
],
];
$this->table(
['Segment', 'Count', 'Space Used'],
$segments
);
$this->line(' ');
if(config_cache('pixelfed.cloud_storage')) {
$this->info('✅ - Cloud storage configured');
$this->line(' ');
}
if(config_cache('instance.avatar.local_to_cloud')) {
$this->info('✅ - Store avatars on cloud filesystem');
$this->line(' ');
}
if(config_cache('pixelfed.cloud_storage') && config_cache('instance.avatar.local_to_cloud')) {
$disk = Storage::disk(config_cache('filesystems.cloud'));
$exists = $disk->exists('cache/avatars/default.jpg');
$state = $exists ? '✅' : '❌';
$msg = $state . ' - Cloud default avatar exists';
$this->info($msg);
}
$choice = $this->choice(
'Select action:',
[
'Upload default avatar to cloud',
'Move local avatars to cloud',
'Move cloud avatars to local'
],
0
);
return $this->handleChoice($choice);
}
protected function handleChoice($id)
{
switch ($id) {
case 'Upload default avatar to cloud':
return $this->uploadDefaultAvatar();
break;
}
}
protected function uploadDefaultAvatar()
{
$disk = Storage::disk(config_cache('filesystems.cloud'));
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
Avatar::whereMediaPath('public/avatars/default.jpg')->update(['cdn_url' => $disk->url('cache/avatars/default.jpg')]);
$this->info('Successfully uploaded default avatar to cloud storage!');
$this->info($disk->url('cache/avatars/default.jpg'));
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\EmailVerification;
use App\User;
class UserRegistrationMagicLink extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:app-magic-link {--username=} {--email=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get the app magic link for users who register in-app but have not recieved the confirmation email';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$username = $this->option('username');
$email = $this->option('email');
if(!$username && !$email) {
$this->error('Please provide the username or email as arguments');
$this->line(' ');
$this->info('Example: ');
$this->info('php artisan user:app-magic-link --username=dansup');
$this->info('php artisan user:app-magic-link --email=dansup@pixelfed.com');
return;
}
$user = User::when($username, function($q, $username) {
return $q->whereUsername($username);
})
->when($email, function($q, $email) {
return $q->whereEmail($email);
})
->first();
if(!$user) {
$this->error('We cannot find any matching accounts');
return;
}
if($user->email_verified_at) {
$this->error('User already verified email address');
return;
}
if(!$user->register_source || $user->register_source !== 'app' || !$user->app_register_token) {
$this->error('User did not register via app');
return;
}
$verify = EmailVerification::whereUserId($user->id)->first();
if(!$verify) {
$this->error('Cannot find user verification codes');
return;
}
$appUrl = 'pixelfed://confirm-account/'. $user->app_register_token . '?rt=' . $verify->random_token;
$this->line(' ');
$this->info('Magic link found! Copy the following link and send to user');
$this->line(' ');
$this->line(' ');
$this->info($appUrl);
$this->line(' ');
$this->line(' ');
return Command::SUCCESS;
}
}

View file

@ -14,12 +14,18 @@ use App\EmailVerification;
use App\Status;
use App\Report;
use App\Profile;
use App\User;
use App\Services\AccountService;
use App\Services\StatusService;
use App\Services\ProfileStatusService;
use App\Util\Lexer\RestrictedNames;
use App\Services\EmailService;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Jenssegers\Agent\Agent;
use Mail;
use App\Mail\PasswordChange;
use App\Mail\ConfirmAppEmail;
class ApiV1Dot1Controller extends Controller
{
@ -402,4 +408,146 @@ class ApiV1Dot1Controller extends Controller
return $this->json($res);
}
public function inAppRegistrationPreFlightCheck(Request $request)
{
return [
'open' => config('pixelfed.open_registration'),
'iara' => config('pixelfed.allow_app_registration')
];
}
public function inAppRegistration(Request $request)
{
abort_if($request->user(), 404);
abort_unless(config('pixelfed.open_registration'), 404);
abort_unless(config('pixelfed.allow_app_registration'), 404);
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
$this->validate($request, [
'email' => [
'required',
'string',
'email',
'max:255',
'unique:users',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
],
'username' => [
'required',
'min:2',
'max:15',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (!ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
'password' => 'required|string|min:8',
// 'avatar' => 'required|mimetypes:image/jpeg,image/png|max:15000',
// 'bio' => 'required|max:140'
]);
$email = $request->input('email');
$username = $request->input('username');
$password = $request->input('password');
if(config('database.default') == 'pgsql') {
$username = strtolower($username);
$email = strtolower($email);
}
$user = new User;
$user->name = $username;
$user->username = $username;
$user->email = $email;
$user->password = Hash::make($password);
$user->register_source = 'app';
$user->app_register_ip = $request->ip();
$user->app_register_token = Str::random(32);
$user->save();
$rtoken = Str::random(mt_rand(64, 70));
$verify = new EmailVerification();
$verify->user_id = $user->id;
$verify->email = $user->email;
$verify->user_token = $user->app_register_token;
$verify->random_token = $rtoken;
$verify->save();
$appUrl = 'pixelfed://confirm-account/'. $user->app_register_token . '?rt=' . $rtoken;
Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
return response()->json([
'success' => true,
]);
}
public function inAppRegistrationConfirm(Request $request)
{
abort_if($request->user(), 404);
abort_unless(config('pixelfed.open_registration'), 404);
abort_unless(config('pixelfed.allow_app_registration'), 404);
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
$this->validate($request, [
'user_token' => 'required',
'random_token' => 'required',
'email' => 'required'
]);
$verify = EmailVerification::whereEmail($request->input('email'))
->whereUserToken($request->input('user_token'))
->whereRandomToken($request->input('random_token'))
->first();
if(!$verify) {
return response()->json(['error' => 'Invalid tokens'], 403);
}
$user = User::findOrFail($verify->user_id);
$user->email_verified_at = now();
$user->last_active_at = now();
$user->save();
$verify->delete();
$token = $user->createToken('Pixelfed');
return response()->json([
'access_token' => $token->accessToken
]);
}
}

View file

@ -307,6 +307,7 @@ class PublicApiController extends Controller
$user = $request->user();
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
if(config('exp.cached_public_timeline') == false) {
if($min || $max) {
$dir = $min ? '>' : '<';
@ -322,6 +323,9 @@ class PublicApiController extends Controller
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereLocal(true)
->when($hideNsfw, function($q, $hideNsfw) {
return $q->where('is_nsfw', false);
})
->whereScope('public')
->orderBy('id', 'desc')
->limit($limit)
@ -365,6 +369,9 @@ class PublicApiController extends Controller
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereLocal(true)
->when($hideNsfw, function($q, $hideNsfw) {
return $q->where('is_nsfw', false);
})
->whereScope('public')
->orderBy('id', 'desc')
->limit($limit)
@ -608,6 +615,7 @@ class PublicApiController extends Controller
$amin = SnowflakeService::byDate(now()->subDays(config('federation.network_timeline_days_falloff')));
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
if(config('instance.timeline.network.cached') == false) {
if($min || $max) {
@ -621,6 +629,9 @@ class PublicApiController extends Controller
'created_at',
)
->where('id', $dir, $id)
->when($hideNsfw, function($q, $hideNsfw) {
return $q->where('is_nsfw', false);
})
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->whereNotIn('profile_id', $filtered)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
@ -648,6 +659,9 @@ class PublicApiController extends Controller
)
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->whereNotIn('profile_id', $filtered)
->when($hideNsfw, function($q, $hideNsfw) {
return $q->where('is_nsfw', false);
})
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNotNull('uri')
->whereScope('public')

View file

@ -13,6 +13,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Image as Intervention;
use Storage;
class AvatarOptimize implements ShouldQueue
{
@ -63,6 +64,13 @@ class AvatarOptimize implements ShouldQueue
$avatar->save();
Cache::forget('avatar:' . $avatar->profile_id);
$this->deleteOldAvatar($avatar->media_path, $this->current);
if(config_cache('pixelfed.cloud_storage') && config_cache('instance.avatar.local_to_cloud')) {
$this->uploadToCloud($avatar);
} else {
$avatar->cdn_url = null;
$avatar->save();
}
} catch (Exception $e) {
}
}
@ -79,4 +87,17 @@ class AvatarOptimize implements ShouldQueue
@unlink($current);
}
}
protected function uploadToCloud($avatar)
{
$base = 'cache/avatars/' . $avatar->profile_id;
$disk = Storage::disk(config('filesystems.cloud'));
$disk->deleteDirectory($base);
$path = $base . '/' . 'a' . strtolower(Str::random(random_int(3,6))) . $avatar->change_count . '.' . pathinfo($avatar->media_path, PATHINFO_EXTENSION);
$url = $disk->put($path, Storage::get($avatar->media_path));
$avatar->cdn_url = $disk->url($path);
$avatar->save();
Storage::delete($avatar->media_path);
Cache::forget('avatar:' . $avatar->profile_id);
}
}

View file

@ -166,12 +166,13 @@ class StatusEntityLexer implements ShouldQueue
if(config_cache('pixelfed.bouncer.enabled')) {
Bouncer::get($status);
}
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
if( $status->uri == null &&
$status->scope == 'public' &&
in_array($status->type, $types) &&
$status->in_reply_to_id === null &&
$status->reblog_of_id === null
$status->reblog_of_id === null &&
($hideNsfw ? $status->is_nsfw == false : true)
) {
PublicTimelineService::add($status->id);
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ConfirmAppEmail extends Mailable
{
use Queueable, SerializesModels;
public $verify;
public $appUrl;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($verify, $url)
{
$this->verify = $verify;
$this->appUrl = $url;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: 'Complete Account Registration',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
markdown: 'emails.confirm_app_email',
with: [
'verify' => $this->verify,
'appUrl' => $this->appUrl
],
);
}
/**
* Get the attachments for the message.
*
* @return array
*/
public function attachments()
{
return [];
}
}

View file

@ -75,9 +75,13 @@ class NetworkTimelineService
public static function warmCache($force = false, $limit = 100)
{
if(self::count() == 0 || $force == true) {
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
Redis::del(self::CACHE_KEY);
$ids = Status::whereNotNull('uri')
->whereScope('public')
->when($hideNsfw, function($q, $hideNsfw) {
return $q->where('is_nsfw', false);
})
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])

View file

@ -75,9 +75,13 @@ class PublicTimelineService {
public static function warmCache($force = false, $limit = 100)
{
if(self::count() == 0 || $force == true) {
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
Redis::del(self::CACHE_KEY);
$ids = Status::whereNull('uri')
->whereNull('in_reply_to_id')
->when($hideNsfw, function($q, $hideNsfw) {
return $q->where('is_nsfw', false);
})
->whereNull('reblog_of_id')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereScope('public')

View file

@ -91,4 +91,10 @@ return [
'profile' => env('INSTANCE_PROFILE_EMBEDS', true),
'post' => env('INSTANCE_POST_EMBEDS', true),
],
'hide_nsfw_on_public_feeds' => env('PF_HIDE_NSFW_ON_PUBLIC_FEEDS', false),
'avatar' => [
'local_to_cloud' => env('PF_LOCAL_AVATAR_TO_CLOUD', false)
],
];

View file

@ -276,4 +276,6 @@ return [
'media_fast_process' => env('PF_MEDIA_FAST_PROCESS', true),
'max_altext_length' => env('PF_MEDIA_MAX_ALTTEXT_LENGTH', 1000),
'allow_app_registration' => env('PF_ALLOW_APP_REGISTRATION', true),
];

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('register_source')->default('web')->nullable()->index();
$table->string('app_register_token')->nullable();
$table->string('app_register_ip')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('register_source');
$table->dropColumn('app_register_token');
$table->dropColumn('app_register_ip');
});
}
};

12831
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,11 +14,14 @@
"bootstrap": "^4.5.2",
"cross-env": "^5.2.1",
"jquery": "^3.6.0",
"laravel-echo": "^1.12.0",
"laravel-mix-make-file-hash": "^2.2.0",
"lodash": "^4.17.21",
"popper.js": "^1.16.1",
"pusher-js": "^7.1.1-beta",
"resolve-url-loader": "^5.0.0",
"sass": "^1.52.1",
"sass-loader": "^7.3.1",
"sass-loader": "^12.3.0",
"vue": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-masonry-css": "^1.0.3",
@ -36,6 +39,7 @@
"bigpicture": "^2.6.2",
"blurhash": "^1.1.3",
"bootstrap-vue": "^2.22.0",
"caniuse-lite": "^1.0.30001418",
"chart.js": "^2.7.2",
"filesize": "^3.6.1",
"hls.js": "^1.1.5",
@ -44,7 +48,6 @@
"jquery-scroll-lock": "^3.1.3",
"jquery.scrollbar": "^0.2.11",
"js-cookie": "^2.2.0",
"laravel-echo": "^1.11.7",
"laravel-mix": "^6.0.43",
"plyr": "^3.7.2",
"promise-polyfill": "8.1.0",

View file

@ -0,0 +1,18 @@
<x-mail::message>
# Complete Account Registration
Hello **{{'@'.$verify->user->username}}**,
You are moments away from finishing your new account registration!
@component('mail::button', ['url' => $appUrl])
Complete Account Registration
@endcomponent
<p style="color: #d6d3d1;font-size: 10pt">Make sure you click on the button from your mobile device, opening the link using a desktop browser won't work.</p>
<br>
<p>If you did not create this account, please disregard this email.</p>
Thanks,<br>
<a href="{{ config('app.url') }}">{{ config('pixelfed.domain.app') }}</a>
</x-mail::message>

View file

@ -149,6 +149,12 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
Route::group(['prefix' => 'directory'], function () use($middleware) {
Route::get('listing', 'PixelfedDirectoryController@get');
});
Route::group(['prefix' => 'auth'], function () use($middleware) {
Route::get('iarpfc', 'Api\ApiV1Dot1Controller@inAppRegistrationPreFlightCheck');
Route::post('iar', 'Api\ApiV1Dot1Controller@inAppRegistration');
Route::post('iarc', 'Api\ApiV1Dot1Controller@inAppRegistrationConfirm');
});
});
Route::group(['prefix' => 'live'], function() use($middleware) {