Merge remote-tracking branch 'origin/staging' into jippi-fork

This commit is contained in:
Christian Winther 2024-01-26 22:57:23 +00:00
commit 36850235a8
13 changed files with 512 additions and 56 deletions

View file

@ -14,6 +14,7 @@
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
### Federation
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
@ -86,6 +87,8 @@
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)

View file

@ -71,6 +71,7 @@ class LoginController extends Controller
$this->username() => 'required|email',
'password' => 'required|string|min:6',
];
$messages = [];
if(
config('captcha.enabled') ||
@ -82,9 +83,9 @@ class LoginController extends Controller
)
) {
$rules['h-captcha-response'] = 'required|filled|captcha|min:5';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules);
$request->validate($rules, $messages);
}
/**

View file

@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use App\Models\UserEmailForgot;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use App\Mail\UserEmailForgotReminder;
use Illuminate\Support\Facades\RateLimiter;
class UserEmailForgotController extends Controller
{
public function __construct()
{
$this->middleware('guest');
abort_unless(config('security.forgot-email.enabled'), 404);
}
public function index(Request $request)
{
abort_if($request->user(), 404);
return view('auth.email.forgot');
}
public function store(Request $request)
{
$rules = [
'username' => 'required|min:2|max:15|exists:users'
];
$messages = [
'username.exists' => 'This username is no longer active or does not exist!'
];
if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'You need to complete the captcha!';
}
$randomDelay = random_int(500000, 2000000);
usleep($randomDelay);
$this->validate($request, $rules, $messages);
$check = self::checkLimits();
if(!$check) {
return redirect()->back()->withErrors([
'username' => 'Please try again later, we\'ve reached our quota and cannot process any more requests at this time.'
]);
}
$user = User::whereUsername($request->input('username'))
->whereNotNull('email_verified_at')
->whereNull('status')
->whereIsAdmin(false)
->first();
if(!$user) {
return redirect()->back()->withErrors([
'username' => 'Invalid username or account. It may not exist, or does not have a verified email, is an admin account or is disabled.'
]);
}
$exists = UserEmailForgot::whereUserId($user->id)
->where('email_sent_at', '>', now()->subHours(24))
->count();
if($exists) {
return redirect()->back()->withErrors([
'username' => 'An email reminder was recently sent to this account, please try again after 24 hours!'
]);
}
return $this->storeHandle($request, $user);
}
protected function storeHandle($request, $user)
{
UserEmailForgot::create([
'user_id' => $user->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'email_sent_at' => now()
]);
Mail::to($user->email)->send(new UserEmailForgotReminder($user));
self::getLimits(true);
return redirect()->back()->with(['status' => 'Successfully sent an email reminder!']);
}
public static function checkLimits()
{
$limits = self::getLimits();
if(
$limits['current']['hourly'] >= $limits['max']['hourly'] ||
$limits['current']['daily'] >= $limits['max']['daily'] ||
$limits['current']['weekly'] >= $limits['max']['weekly'] ||
$limits['current']['monthly'] >= $limits['max']['monthly']
) {
return false;
}
return true;
}
public static function getLimits($forget = false)
{
return [
'max' => config('security.forgot-email.limits.max'),
'current' => [
'hourly' => self::activeCount(60, $forget),
'daily' => self::activeCount(1440, $forget),
'weekly' => self::activeCount(10080, $forget),
'monthly' => self::activeCount(43800, $forget)
]
];
}
public static function activeCount($mins, $forget = false)
{
if($forget) {
Cache::forget('pf:auth:forgot-email:active-count:dur-' . $mins);
}
return Cache::remember('pf:auth:forgot-email:active-count:dur-' . $mins, 14200, function() use($mins) {
return UserEmailForgot::where('email_sent_at', '>', now()->subMinutes($mins))->count();
});
}
}

View file

@ -0,0 +1,55 @@
<?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 UserEmailForgotReminder extends Mailable
{
use Queueable, SerializesModels;
public $user;
/**
* Create a new message instance.
*/
public function __construct($user)
{
$this->user = $user;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '[' . config('pixelfed.domain.app') . '] Pixelfed Account Email Reminder',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.forgot-email.message',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserEmailForgot extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'email_sent_at' => 'datetime',
];
}

View file

@ -5,5 +5,18 @@ return [
'verify_dns' => env('PF_SECURITY_URL_VERIFY_DNS', false),
'trusted_domains' => env('PF_SECURITY_URL_TRUSTED_DOMAINS', 'pixelfed.social,pixelfed.art,mastodon.social'),
],
'forgot-email' => [
'enabled' => env('PF_AUTH_ALLOW_EMAIL_FORGOT', true),
'limits' => [
'max' => [
'hourly' => env('PF_AUTH_FORGOT_EMAIL_MAX_HOURLY', 50),
'daily' => env('PF_AUTH_FORGOT_EMAIL_MAX_DAILY', 100),
'weekly' => env('PF_AUTH_FORGOT_EMAIL_MAX_WEEKLY', 200),
'monthly' => env('PF_AUTH_FORGOT_EMAIL_MAX_MONTHLY', 500),
]
]
]
];

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_email_forgots', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id')->index();
$table->string('ip_address')->nullable();
$table->string('user_agent')->nullable();
$table->string('referrer')->nullable();
$table->timestamp('email_sent_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_email_forgots');
}
};

View file

@ -0,0 +1,127 @@
@extends('layouts.blank')
@push('styles')
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
<link rel="preload" as="image" href="{{ url('/_landing/bg.jpg')}}" />
@endpush
@section('content')
<div class="page-wrapper">
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-xl-6 col-lg-5 col-md-7 col-12">
<div class="text-center">
<a href="/">
<img src="/img/pixelfed-icon-white.svg" height="60px">
</a>
<h1 class="pt-4 pb-1">Forgot Email</h1>
<p class="font-weight-light pb-2">Recover your account by sending an email to an associated username</p>
</div>
@if(session('status'))
<div class="alert alert-success">
<div class="d-flex align-items-center font-weight-bold" style="gap:1rem;">
<i class="far fa-check-circle fa-lg" style="opacity:70%"></i>
{{ session('status') }}
</div>
</div>
@endif
@if ($errors->any())
@foreach ($errors->all() as $error)
<div class="alert alert-danger bg-danger text-white border-danger">
<div class="d-flex align-items-center font-weight-bold" style="gap:1rem;">
<i class="far fa-exclamation-triangle fa-2x" style="opacity:70%"></i>
{{ $error }}
</div>
</div>
@endforeach
@endif
<div class="card bg-glass">
<div class="card-header bg-transparent p-3 text-center font-weight-bold" style="border-bottom:1px solid #ffffff20">{{ __('Recover Email') }}</div>
<div class="card-body">
<form id="passwordReset" method="POST" action="{{ route('email.forgot') }}">
@csrf
<div class="form-group row">
<div class="col-md-12">
<label class="font-weight-bold small text-muted">Username</label>
<input
id="username"
type="text"
class="form-control form-control-lg bg-glass text-white"
name="username"
maxlength="15"
placeholder="{{ __('Your username') }}" required>
@if ($errors->has('username') )
<span class="text-danger small mb-3">
<strong>{{ $errors->first('username') }}</strong>
</span>
@endif
</div>
</div>
@if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register'))
<label class="font-weight-bold small text-muted">Captcha</label>
<div class="d-flex flex-grow-1">
{!! Captcha::display(['data-theme' => 'dark']) !!}
</div>
@if ($errors->has('h-captcha-response'))
<div class="text-danger small mb-3">
<strong>{{ $errors->first('h-captcha-response') }}</strong>
</div>
@endif
@endif
<div class="form-group row pt-4 mb-0">
<div class="col-md-12">
<button type="button" id="sbtn" class="btn btn-primary btn-block rounded-pill font-weight-bold" onclick="event.preventDefault();handleSubmit()">
{{ __('Send Email Reminder') }}
</button>
</div>
</div>
</form>
</div>
</div>
<div class="mt-3 d-flex justify-content-between align-items-center">
<a class="btn btn-link text-white font-weight-bold text-decoration-none" href="{{ route('login') }}">
<i class="far fa-long-arrow-left fa-lg mr-1"></i> {{ __('Back to Login') }}
</a>
<a href="{{ route('password.request') }}" class="text-white font-weight-bold text-decoration-none">Forgot password?</a>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
function handleSubmit() {
let username = document.getElementById('username');
username.classList.add('disabled');
let btn = document.getElementById('sbtn');
btn.classList.add('disabled');
btn.setAttribute('disabled', 'disabled');
btn.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"><span class="sr-only">Loading...</span></div>';
document.getElementById('passwordReset').submit()
}
</script>
@endpush
@push('styles')
<style>
.bg-glass:focus {
background: rgba(255, 255, 255, 0.05) !important;
box-shadow: none !important;
border-color: rgba(255, 255, 255, 0.3);
}
</style>
@endpush

View file

@ -4,16 +4,28 @@
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="">
<div class="card-header bg-transparent p-3 text-center font-weight-bold h3">{{ __('Login') }}</div>
<div class="card shadow-none border">
<div class="card-header bg-transparent p-3">
<h4 class="font-weight-bold mb-0 text-center">
Account Login
</h4>
</div>
@if ($errors->any())
@foreach ($errors->all() as $error)
<div class="alert alert-danger m-3">
<span class="font-weight-bold small"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</span>
</div>
@endforeach
@endif
<div class="card-body">
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="form-group row">
<div class="form-group row mb-0">
<div class="col-md-12">
<label for="email" class="small font-weight-bold text-muted mb-0">Email Address</label>
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" placeholder="{{__('Email')}}" required autofocus>
@if ($errors->has('email'))
@ -21,12 +33,19 @@
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
<div class="help-text small text-right mb-0">
<a href="{{ route('email.forgot') }}" class="small text-muted font-weight-bold">
{{ __('Forgot Email') }}
</a>
</div>
</div>
</div>
<div class="form-group row">
<div class="form-group row mb-0">
<div class="col-md-12">
<label for="password" class="small font-weight-bold text-muted mb-0">Password</label>
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{__('Password')}}" required>
@if ($errors->has('password'))
@ -34,6 +53,12 @@
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
<p class="help-text small text-right mb-0">
<a href="{{ route('password.request') }}" class="small text-muted font-weight-bold">
{{ __('Forgot Password') }}
</a>
</p>
</div>
</div>
@ -64,14 +89,9 @@
</div>
@endif
<div class="form-group row mb-4">
<div class="col-md-12">
<button type="submit" class="btn btn-primary btn-block btn-lg font-weight-bold">
{{ __('Login') }}
</button>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg font-weight-bold rounded-pill">
{{ __('Login') }}
</button>
</form>
@if(
@ -91,20 +111,38 @@
</form>
@endif
@if(config_cache('pixelfed.open_registration'))
<hr>
<p class="text-center font-weight-bold">
@if(config_cache('pixelfed.open_registration'))
<p class="text-center font-weight-bold mb-0">
<a href="/register">Register</a>
<span class="px-1">·</span>
@endif
<a href="{{ route('password.request') }}">
{{ __('Forgot Password') }}
</a>
</p>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener("DOMContentLoaded", function() {
function getQueryParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
const email = getQueryParam('email');
if (email) {
const emailInput = document.getElementById('email');
if (emailInput) {
emailInput.value = email;
const passwordInput = document.getElementById('password');
if (passwordInput) {
passwordInput.focus();
}
}
}
});
</script>
@endpush

View file

@ -9,7 +9,7 @@
<div class="page-wrapper">
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="col-xl-6 col-lg-5 col-md-7 col-12">
<div class="text-center">
<a href="/">
<img src="/img/pixelfed-icon-white.svg" height="60px">
@ -19,9 +19,9 @@
</div>
@if(session('status') || $errors->has('email'))
<div class="alert alert-info small">
<div class="d-flex align-items-center font-weight-bold" style="gap:0.5rem;">
<i class="far fa-exclamation-triangle fa-lg" style="opacity:20%"></i>
<div class="alert alert-info">
<div class="d-flex align-items-center font-weight-bold" style="gap:1rem;">
<i class="far fa-exclamation-triangle fa-2x" style="opacity:70%"></i>
{{ session('status') ?? $errors->first('email') }}
</div>
@ -39,7 +39,13 @@
<div class="form-group row">
<div class="col-md-12">
<label class="font-weight-bold small text-muted">Email</label>
<input id="email" type="email" class="form-control" name="email" placeholder="{{ __('E-Mail Address') }}" required>
<input
id="email"
type="email"
class="form-control form-control-lg bg-glass text-white"
name="email"
placeholder="{{ __('E-Mail Address') }}"
required>
@if ($errors->has('email') && $errors->first('email') === 'The email must be a valid email address.')
<span class="text-danger small mb-3">
<strong>{{ $errors->first('email') }}</strong>
@ -76,7 +82,7 @@
<i class="far fa-long-arrow-left fa-lg mr-1"></i> {{ __('Back to Login') }}
</a>
<a href="#" class="text-white font-weight-bold text-decoration-none" onclick="event.preventDefault();forgotUsername()">Forgot email?</a>
<a href="{{route('email.forgot')}}" class="text-white font-weight-bold text-decoration-none">Forgot email?</a>
</div>
</div>
</div>
@ -86,29 +92,6 @@
@push('scripts')
<script type="text/javascript">
function forgotUsername() {
swal({
title: 'Forgot email?',
text: 'Contact the instance admins to assist you in recovering your account.',
icon: 'info',
buttons: {
contact: {
text: "Contact Admins",
value: "contact",
className: "bg-danger"
},
cancel: "Close",
},
})
.then((value) => {
switch(value) {
case 'contact':
window.location.href = '/site/contact';
break;
}
});
}
function handleSubmit() {
let email = document.getElementById('email');
email.classList.add('disabled');
@ -121,3 +104,13 @@
}
</script>
@endpush
@push('styles')
<style>
.bg-glass:focus {
background: rgba(255, 255, 255, 0.05) !important;
box-shadow: none !important;
border-color: rgba(255, 255, 255, 0.3);
}
</style>
@endpush

View file

@ -9,7 +9,7 @@
<div class="page-wrapper">
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="col-xl-6 col-lg-5 col-md-7 col-12">
<div class="text-center">
<a href="/">
<img src="/img/pixelfed-icon-white.svg" height="60px">
@ -41,14 +41,14 @@
<label class="font-weight-bold small text-muted">Email</label>
<input
id="email"
type="email"
class="form-control {{ $errors->has('email') ? ' is-invalid' : '' }}"
type="text"
class="form-control form-control-lg bg-dark bg-glass text-white{{ $errors->has('email') ? ' is-invalid' : '' }}"
name="email"
value="{{ $email ?? old('email') }}"
placeholder="{{ __('E-Mail Address') }}"
required
disabled
style="opacity: 20%;">
style="opacity:.5">
@if ($errors->has('email'))
<span class="invalid-feedback">
@ -67,7 +67,7 @@
<input
id="password"
type="password"
class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}"
class="form-control form-control-lg bg-glass text-white{{ $errors->has('password') ? ' is-invalid' : '' }}"
name="password"
placeholder="{{ __('Password') }}"
minlength="{{config('pixelfed.min_password_length')}}"
@ -93,7 +93,7 @@
<input
id="password-confirm"
type="password"
class="form-control{{ $errors->has('password_confirmation') ? ' is-invalid' : '' }}"
class="form-control form-control-lg bg-glass text-white{{ $errors->has('password_confirmation') ? ' is-invalid' : '' }}"
name="password_confirmation"
placeholder="{{ __('Confirm Password') }}"
minlength="{{config('pixelfed.min_password_length')}}"
@ -152,3 +152,13 @@
}
</script>
@endpush
@push('styles')
<style>
.bg-glass:focus {
background: rgba(255, 255, 255, 0.05) !important;
box-shadow: none !important;
border-color: rgba(255, 255, 255, 0.3);
}
</style>
@endpush

View file

@ -0,0 +1,33 @@
@component('mail::message')
Hello,
You recently requested to know the email address associated with your username [**{{'@' . $user->username}}**]({{$user->url()}}) on [**{{config('pixelfed.domain.app')}}**]({{config('app.url')}}).
We're here to assist! Simply tap on the Login button below.
<x-mail::button :url="url('/login?email=' . urlencode($user->email))" color="success">
Login to my <strong>{{'@' . $user->username}}</strong> account
</x-mail::button>
----
<br>
The email address linked to your username is:
<x-mail::panel>
<p>
<strong>{{$user->email}}</strong>
</p>
</x-mail::panel>
You can use this email address to log in to your account.
<small>If needed, you can [reset your password]({{ route('password.request')}}). For security reasons, we recommend keeping your account information, including your email address, updated and secure. If you did not make this request or if you have any other questions or concerns, please feel free to [contact our support team]({{route('site.contact')}}).</small>
Thank you for being a part of our community!
Best regards,<br>
<a href="{{ config('app.url') }}"><strong>{{ config('pixelfed.domain.app') }}</strong></a>
<br>
<hr>
<p style="font-size:10pt;">This is an automated message, please be aware that replies to this email cannot be monitored or responded to.</p>
@endcomponent

View file

@ -203,6 +203,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister');
Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore');
Route::get('auth/forgot/email', 'UserEmailForgotController@index')->name('email.forgot');
Route::post('auth/forgot/email', 'UserEmailForgotController@store')->middleware('throttle:10,900,forgotEmail');
Route::get('discover', 'DiscoverController@home')->name('discover');
Route::group(['prefix' => 'api'], function () {