mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-18 20:41:27 +00:00
commit
ee6348a873
33 changed files with 1164 additions and 275 deletions
|
@ -17,6 +17,7 @@ use Carbon\Carbon;
|
|||
use Illuminate\Http\Request;
|
||||
use Mail;
|
||||
use Redis;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
@ -301,4 +302,28 @@ class AccountController extends Controller
|
|||
->withErrors(['password' => __('auth.failed')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function twoFactorCheckpoint(Request $request)
|
||||
{
|
||||
return view('auth.checkpoint');
|
||||
}
|
||||
|
||||
public function twoFactorVerify(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'code' => 'required|string|max:32'
|
||||
]);
|
||||
$user = Auth::user();
|
||||
$code = $request->input('code');
|
||||
$google2fa = new Google2FA();
|
||||
$verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
|
||||
if($verify) {
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return redirect('/');
|
||||
} else {
|
||||
return redirect()->back()->withErrors([
|
||||
'code' => 'Invalid code'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@ class AdminController extends Controller
|
|||
|
||||
public function __construct()
|
||||
{
|
||||
return $this->middleware('admin');
|
||||
$this->middleware('admin');
|
||||
$this->middleware('twofactor');
|
||||
}
|
||||
|
||||
public function home()
|
||||
|
|
153
app/Http/Controllers/Settings/HomeSettings.php
Normal file
153
app/Http/Controllers/Settings/HomeSettings.php
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait HomeSettings
|
||||
{
|
||||
|
||||
public function home()
|
||||
{
|
||||
$id = Auth::user()->profile->id;
|
||||
$storage = [];
|
||||
$used = Media::whereProfileId($id)->sum('size');
|
||||
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
|
||||
$storage['used'] = $used;
|
||||
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
|
||||
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
|
||||
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
|
||||
|
||||
return view('settings.home', compact('storage'));
|
||||
}
|
||||
|
||||
public function homeUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
|
||||
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
|
||||
'website' => 'nullable|url',
|
||||
'email' => 'nullable|email',
|
||||
]);
|
||||
|
||||
$changes = false;
|
||||
$name = $request->input('name');
|
||||
$bio = $request->input('bio');
|
||||
$website = $request->input('website');
|
||||
$email = $request->input('email');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$validate = config('pixelfed.enforce_email_verification');
|
||||
|
||||
if ($user->email != $email) {
|
||||
$changes = true;
|
||||
$user->email = $email;
|
||||
|
||||
if ($validate) {
|
||||
$user->email_verified_at = null;
|
||||
// Prevent old verifications from working
|
||||
EmailVerification::whereUserId($user->id)->delete();
|
||||
}
|
||||
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'account.edit.email';
|
||||
$log->message = 'Email changed';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
|
||||
// Only allow email to be updated if not yet verified
|
||||
if (!$validate || !$changes && $user->email_verified_at) {
|
||||
if ($profile->name != $name) {
|
||||
$changes = true;
|
||||
$user->name = $name;
|
||||
$profile->name = $name;
|
||||
}
|
||||
|
||||
if (!$profile->website || $profile->website != $website) {
|
||||
$changes = true;
|
||||
$profile->website = $website;
|
||||
}
|
||||
|
||||
if (!$profile->bio || !$profile->bio != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = $bio;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes === true) {
|
||||
$user->save();
|
||||
$profile->save();
|
||||
|
||||
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
|
||||
}
|
||||
|
||||
return redirect('/settings/home');
|
||||
}
|
||||
|
||||
public function password()
|
||||
{
|
||||
return view('settings.password');
|
||||
}
|
||||
|
||||
public function passwordUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'current' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
|
||||
$current = $request->input('current');
|
||||
$new = $request->input('password');
|
||||
$confirm = $request->input('password_confirmation');
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (password_verify($current, $user->password) && $new === $confirm) {
|
||||
$user->password = bcrypt($new);
|
||||
$user->save();
|
||||
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'account.edit.password';
|
||||
$log->message = 'Password changed';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
|
||||
return redirect('/settings/home')->with('status', 'Password successfully updated!');
|
||||
}
|
||||
|
||||
return redirect('/settings/home')->with('error', 'There was an error with your request!');
|
||||
}
|
||||
|
||||
public function email()
|
||||
{
|
||||
return view('settings.email');
|
||||
}
|
||||
|
||||
public function avatar()
|
||||
{
|
||||
return view('settings.avatar');
|
||||
}
|
||||
|
||||
}
|
127
app/Http/Controllers/Settings/PrivacySettings.php
Normal file
127
app/Http/Controllers/Settings/PrivacySettings.php
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait PrivacySettings
|
||||
{
|
||||
|
||||
public function privacy()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$is_private = Auth::user()->profile->is_private;
|
||||
$settings['is_private'] = (bool) $is_private;
|
||||
|
||||
return view('settings.privacy', compact('settings'));
|
||||
}
|
||||
|
||||
public function privacyStore(Request $request)
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$profile = Auth::user()->profile;
|
||||
$fields = [
|
||||
'is_private',
|
||||
'crawlable',
|
||||
'show_profile_follower_count',
|
||||
'show_profile_following_count',
|
||||
];
|
||||
foreach ($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if ($field == 'is_private') {
|
||||
if ($form == 'on') {
|
||||
$profile->{$field} = true;
|
||||
$settings->show_guests = false;
|
||||
$settings->show_discover = false;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$field} = false;
|
||||
$profile->save();
|
||||
}
|
||||
} elseif ($field == 'crawlable') {
|
||||
if ($form == 'on') {
|
||||
$settings->{$field} = false;
|
||||
} else {
|
||||
$settings->{$field} = true;
|
||||
}
|
||||
} else {
|
||||
if ($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
}
|
||||
$settings->save();
|
||||
}
|
||||
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function mutedUsers()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$ids = (new UserFilter())->mutedUserIds($pid);
|
||||
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
|
||||
return view('settings.privacy.muted', compact('users'));
|
||||
}
|
||||
|
||||
public function mutedUsersUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|integer|min:1'
|
||||
]);
|
||||
$fid = $request->input('profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
DB::transaction(function () use ($fid, $pid) {
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
->whereFilterableId($fid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('mute')
|
||||
->firstOrFail();
|
||||
$filter->delete();
|
||||
});
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function blockedUsers()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$ids = (new UserFilter())->blockedUserIds($pid);
|
||||
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
|
||||
return view('settings.privacy.blocked', compact('users'));
|
||||
}
|
||||
|
||||
|
||||
public function blockedUsersUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|integer|min:1'
|
||||
]);
|
||||
$fid = $request->input('profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
DB::transaction(function () use ($fid, $pid) {
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
->whereFilterableId($fid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->firstOrFail();
|
||||
$filter->delete();
|
||||
});
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function blockedInstances()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
return view('settings.privacy.blocked-instances');
|
||||
}
|
||||
}
|
139
app/Http/Controllers/Settings/SecuritySettings.php
Normal file
139
app/Http/Controllers/Settings/SecuritySettings.php
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
|
||||
trait SecuritySettings
|
||||
{
|
||||
|
||||
public function security()
|
||||
{
|
||||
$sessions = DB::table('sessions')
|
||||
->whereUserId(Auth::id())
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
$activity = AccountLog::whereUserId(Auth::id())
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
return view('settings.security', compact('sessions', 'activity', 'user'));
|
||||
}
|
||||
|
||||
public function securityTwoFactorSetup(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if($user->{'2fa_enabled'} && $user->{'2fa_secret'}) {
|
||||
return redirect(route('account.security'));
|
||||
}
|
||||
$backups = $this->generateBackupCodes();
|
||||
$google2fa = new Google2FA();
|
||||
$key = $google2fa->generateSecretKey(32);
|
||||
$qrcode = $google2fa->getQRCodeInline(
|
||||
config('pixelfed.domain.app'),
|
||||
$user->email,
|
||||
$key,
|
||||
500
|
||||
);
|
||||
$user->{'2fa_secret'} = $key;
|
||||
$user->{'2fa_backup_codes'} = json_encode($backups);
|
||||
$user->save();
|
||||
return view('settings.security.2fa.setup', compact('user', 'qrcode', 'backups'));
|
||||
}
|
||||
|
||||
protected function generateBackupCodes()
|
||||
{
|
||||
$keys = [];
|
||||
for ($i=0; $i < 11; $i++) {
|
||||
$key = str_random(24);
|
||||
$keys[] = $key;
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public function securityTwoFactorSetupStore(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if($user->{'2fa_enabled'} && $user->{'2fa_secret'}) {
|
||||
abort(403, 'Two factor auth is already setup.');
|
||||
}
|
||||
$this->validate($request, [
|
||||
'code' => 'required|integer'
|
||||
]);
|
||||
$code = $request->input('code');
|
||||
$google2fa = new Google2FA();
|
||||
$verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
|
||||
if($verify) {
|
||||
$user->{'2fa_enabled'} = true;
|
||||
$user->{'2fa_setup_at'} = Carbon::now();
|
||||
$user->save();
|
||||
return response()->json(['msg'=>'success']);
|
||||
} else {
|
||||
return response()->json(['msg'=>'fail'], 403);
|
||||
}
|
||||
}
|
||||
|
||||
public function securityTwoFactorEdit(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if(!$user->{'2fa_enabled'} || !$user->{'2fa_secret'}) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('settings.security.2fa.edit', compact('user'));
|
||||
}
|
||||
|
||||
public function securityTwoFactorRecoveryCodes(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if(!$user->{'2fa_enabled'} || !$user->{'2fa_secret'} || !$user->{'2fa_backup_codes'}) {
|
||||
abort(403);
|
||||
}
|
||||
$codes = json_decode($user->{'2fa_backup_codes'}, true);
|
||||
return view('settings.security.2fa.recovery-codes', compact('user', 'codes'));
|
||||
}
|
||||
|
||||
public function securityTwoFactorUpdate(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if(!$user->{'2fa_enabled'} || !$user->{'2fa_secret'} || !$user->{'2fa_backup_codes'}) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|string|max:12'
|
||||
]);
|
||||
|
||||
if($request->action !== 'remove') {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$user->{'2fa_enabled'} = false;
|
||||
$user->{'2fa_secret'} = null;
|
||||
$user->{'2fa_backup_codes'} = null;
|
||||
$user->{'2fa_setup_at'} = null;
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'msg' => 'Successfully removed 2fa device'
|
||||
], 200);
|
||||
}
|
||||
}
|
|
@ -3,135 +3,27 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
|
||||
use Auth;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Settings\{
|
||||
HomeSettings,
|
||||
PrivacySettings,
|
||||
SecuritySettings
|
||||
};
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
use HomeSettings,
|
||||
PrivacySettings,
|
||||
SecuritySettings;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
$id = Auth::user()->profile->id;
|
||||
$storage = [];
|
||||
$used = Media::whereProfileId($id)->sum('size');
|
||||
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
|
||||
$storage['used'] = $used;
|
||||
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
|
||||
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
|
||||
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
|
||||
|
||||
return view('settings.home', compact('storage'));
|
||||
}
|
||||
|
||||
public function homeUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
|
||||
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
|
||||
'website' => 'nullable|url',
|
||||
'email' => 'nullable|email',
|
||||
]);
|
||||
|
||||
$changes = false;
|
||||
$name = $request->input('name');
|
||||
$bio = $request->input('bio');
|
||||
$website = $request->input('website');
|
||||
$email = $request->input('email');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$validate = config('pixelfed.enforce_email_verification');
|
||||
|
||||
if ($user->email != $email) {
|
||||
$changes = true;
|
||||
$user->email = $email;
|
||||
|
||||
if ($validate) {
|
||||
$user->email_verified_at = null;
|
||||
// Prevent old verifications from working
|
||||
EmailVerification::whereUserId($user->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow email to be updated if not yet verified
|
||||
if (!$validate || !$changes && $user->email_verified_at) {
|
||||
if ($profile->name != $name) {
|
||||
$changes = true;
|
||||
$user->name = $name;
|
||||
$profile->name = $name;
|
||||
}
|
||||
|
||||
if (!$profile->website || $profile->website != $website) {
|
||||
$changes = true;
|
||||
$profile->website = $website;
|
||||
}
|
||||
|
||||
if (!$profile->bio || !$profile->bio != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = $bio;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes === true) {
|
||||
$user->save();
|
||||
$profile->save();
|
||||
|
||||
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
|
||||
}
|
||||
|
||||
return redirect('/settings/home');
|
||||
}
|
||||
|
||||
public function password()
|
||||
{
|
||||
return view('settings.password');
|
||||
}
|
||||
|
||||
public function passwordUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'current' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
|
||||
$current = $request->input('current');
|
||||
$new = $request->input('password');
|
||||
$confirm = $request->input('password_confirmation');
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (password_verify($current, $user->password) && $new === $confirm) {
|
||||
$user->password = bcrypt($new);
|
||||
$user->save();
|
||||
|
||||
return redirect('/settings/home')->with('status', 'Password successfully updated!');
|
||||
}
|
||||
|
||||
return redirect('/settings/home')->with('error', 'There was an error with your request!');
|
||||
}
|
||||
|
||||
public function email()
|
||||
{
|
||||
return view('settings.email');
|
||||
}
|
||||
|
||||
public function avatar()
|
||||
{
|
||||
return view('settings.avatar');
|
||||
}
|
||||
|
||||
public function accessibility()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
|
@ -167,70 +59,6 @@ class SettingsController extends Controller
|
|||
return view('settings.notifications');
|
||||
}
|
||||
|
||||
public function privacy()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$is_private = Auth::user()->profile->is_private;
|
||||
$settings['is_private'] = (bool) $is_private;
|
||||
|
||||
return view('settings.privacy', compact('settings'));
|
||||
}
|
||||
|
||||
public function privacyStore(Request $request)
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$profile = Auth::user()->profile;
|
||||
$fields = [
|
||||
'is_private',
|
||||
'crawlable',
|
||||
'show_profile_follower_count',
|
||||
'show_profile_following_count',
|
||||
];
|
||||
foreach ($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if ($field == 'is_private') {
|
||||
if ($form == 'on') {
|
||||
$profile->{$field} = true;
|
||||
$settings->show_guests = false;
|
||||
$settings->show_discover = false;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$field} = false;
|
||||
$profile->save();
|
||||
}
|
||||
} elseif ($field == 'crawlable') {
|
||||
if ($form == 'on') {
|
||||
$settings->{$field} = false;
|
||||
} else {
|
||||
$settings->{$field} = true;
|
||||
}
|
||||
} else {
|
||||
if ($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
}
|
||||
$settings->save();
|
||||
}
|
||||
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function security()
|
||||
{
|
||||
$sessions = DB::table('sessions')
|
||||
->whereUserId(Auth::id())
|
||||
->limit(20)
|
||||
->get();
|
||||
$activity = AccountLog::whereUserId(Auth::id())
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return view('settings.security', compact('sessions', 'activity'));
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return view('settings.applications');
|
||||
|
@ -255,64 +83,5 @@ class SettingsController extends Controller
|
|||
{
|
||||
return view('settings.developers');
|
||||
}
|
||||
|
||||
public function mutedUsers()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$ids = (new UserFilter())->mutedUserIds($pid);
|
||||
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
|
||||
return view('settings.privacy.muted', compact('users'));
|
||||
}
|
||||
|
||||
public function mutedUsersUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|integer|min:1'
|
||||
]);
|
||||
$fid = $request->input('profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
DB::transaction(function () use ($fid, $pid) {
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
->whereFilterableId($fid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('mute')
|
||||
->firstOrFail();
|
||||
$filter->delete();
|
||||
});
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function blockedUsers()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$ids = (new UserFilter())->blockedUserIds($pid);
|
||||
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
|
||||
return view('settings.privacy.blocked', compact('users'));
|
||||
}
|
||||
|
||||
|
||||
public function blockedUsersUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|integer|min:1'
|
||||
]);
|
||||
$fid = $request->input('profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
DB::transaction(function () use ($fid, $pid) {
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
->whereFilterableId($fid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->firstOrFail();
|
||||
$filter->delete();
|
||||
});
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function blockedInstances()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
return view('settings.privacy.blocked-instances');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class TimelineController extends Controller
|
|||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('twofactor');
|
||||
}
|
||||
|
||||
public function personal()
|
||||
|
|
|
@ -61,6 +61,7 @@ class Kernel extends HttpKernel
|
|||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'twofactor' => \App\Http\Middleware\TwoFactorAuth::class,
|
||||
'validemail' => \App\Http\Middleware\EmailVerificationCheck::class,
|
||||
];
|
||||
}
|
||||
|
|
32
app/Http/Middleware/TwoFactorAuth.php
Normal file
32
app/Http/Middleware/TwoFactorAuth.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Auth;
|
||||
use Closure;
|
||||
|
||||
class TwoFactorAuth
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if($request->user()) {
|
||||
$user = $request->user();
|
||||
$enabled = (bool) $user->{'2fa_enabled'};
|
||||
if($enabled != false) {
|
||||
$checkpoint = 'i/auth/checkpoint';
|
||||
if($request->session()->has('2fa.session.active') !== true && !$request->is($checkpoint))
|
||||
{
|
||||
return redirect('/i/auth/checkpoint');
|
||||
}
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,11 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class ImportJob extends Model
|
||||
{
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id');
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return url("/i/import/job/{$this->uuid}/{$this->stage}");
|
||||
|
|
|
@ -16,7 +16,7 @@ class User extends Authenticatable
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at', 'email_verified_at'];
|
||||
protected $dates = ['deleted_at', 'email_verified_at', '2fa_setup_at'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
|
@ -113,6 +113,7 @@ class RestrictedNames
|
|||
public static $reserved = [
|
||||
// Reserved for instance admin
|
||||
'admin',
|
||||
'administrator',
|
||||
|
||||
// Static Assets
|
||||
'assets',
|
||||
|
@ -126,6 +127,7 @@ class RestrictedNames
|
|||
'api',
|
||||
'auth',
|
||||
'css',
|
||||
'checkpoint',
|
||||
'c',
|
||||
'i',
|
||||
'dashboard',
|
||||
|
|
19
resources/lang/cs/auth.php
Normal file
19
resources/lang/cs/auth.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'Tyto přihlašovací údaje se neshodují s našemi záznamy.',
|
||||
'throttle' => 'Příliš mnoho pokusů o přihlášení. Prosím zkuste to znovu za :seconds sekund.',
|
||||
|
||||
];
|
14
resources/lang/cs/navmenu.php
Normal file
14
resources/lang/cs/navmenu.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'viewMyProfile' => 'Zobrazit můj profil',
|
||||
'myTimeline' => 'Moje časová osa',
|
||||
'publicTimeline' => 'Veřejná časová osa',
|
||||
'remoteFollow' => 'Vzdálené sledování',
|
||||
'settings' => 'Nastavení',
|
||||
'admin' => 'Administrace',
|
||||
'logout' => 'Odhlásit',
|
||||
'directMessages' => 'Přímé zprávy',
|
||||
|
||||
];
|
10
resources/lang/cs/notification.php
Normal file
10
resources/lang/cs/notification.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'likedPhoto' => 'si oblíbil/a vaši fotku.',
|
||||
'startedFollowingYou' => 'vás začal/a sledovat.',
|
||||
'commented' => 'okomentoval/a vaši fotku.',
|
||||
'mentionedYou' => 'vás zmínil/a.',
|
||||
|
||||
];
|
19
resources/lang/cs/pagination.php
Normal file
19
resources/lang/cs/pagination.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Předchozí',
|
||||
'next' => 'Další »',
|
||||
|
||||
];
|
22
resources/lang/cs/passwords.php
Normal file
22
resources/lang/cs/passwords.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
|
||||
'password' => 'Hesla musí být alespoň šest znaků dlouhá a shodovat se s potvrzením.',
|
||||
'reset' => 'Vaše heslo bylo obnoveno!',
|
||||
'sent' => 'Poslali jsme vám e-mailem odkaz pro obnovu hesla!',
|
||||
'token' => 'Tento token pro obnovu hesla je neplatný.',
|
||||
'user' => "Nemůžeme najít uživatele s touto e-mailovou adresou.",
|
||||
|
||||
];
|
12
resources/lang/cs/profile.php
Normal file
12
resources/lang/cs/profile.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'emptyTimeline' => 'Tento uživatel ještě nemá žádné příspěvky!',
|
||||
'emptyFollowers' => 'Tento uživatel ještě nemá žádné sledovatele!',
|
||||
'emptyFollowing' => 'Tento uživatel ještě nikoho nesleduje!',
|
||||
'emptySaved' => 'Ještě jste neuložil/a žádné příspěvky!',
|
||||
'savedWarning' => 'Pouze vy můžete vidět, co máte uložené',
|
||||
'privateProfileWarning' => 'Tento účet je soukromý',
|
||||
'alreadyFollow' => 'Již uživatele :username sledujete?',
|
||||
'loginToSeeProfile' => 'pro zobrazení jeho/jejích fotek a videí.',
|
||||
];
|
7
resources/lang/cs/timeline.php
Normal file
7
resources/lang/cs/timeline.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'emptyPersonalTimeline' => 'Vaše časová osa je prázdná.',
|
||||
|
||||
];
|
122
resources/lang/cs/validation.php
Normal file
122
resources/lang/cs/validation.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => ':attribute musí být akceptován.',
|
||||
'active_url' => ':attribute není platná URL adresa.',
|
||||
'after' => ':attribute musí být datum po :date.',
|
||||
'after_or_equal' => ':attribute musí být datum po nebo rovný datu :date.',
|
||||
'alpha' => ':attribute musí obsahovat pouze písmena.',
|
||||
'alpha_dash' => ':attribute musí obsahovat pouze písmena, číslice a podtržítka.',
|
||||
'alpha_num' => ':attribute musí obsahovat pouze písmena a číslice.',
|
||||
'array' => ':attribute musí být pole.',
|
||||
'before' => ':attribute musí být datum před :date.',
|
||||
'before_or_equal' => ':attribute musí být datum před nebo rovný datu :date.',
|
||||
'between' => [
|
||||
'numeric' => ':attribute musí být mezi :min a :max.',
|
||||
'file' => ':attribute musí být mezi :min a :max kilobyty.',
|
||||
'string' => ':attribute musí být mezi :min a :max znaky.',
|
||||
'array' => ':attribute musí mít mezi :min a :max položkami.',
|
||||
],
|
||||
'boolean' => 'Pole :attribute musí být true nebo false.',
|
||||
'confirmed' => 'Potvrzení :attribute se neshoduje.',
|
||||
'date' => ':attribute není platné datum.',
|
||||
'date_format' => ':attribute se neshoduje s formátem :format.',
|
||||
'different' => ':attribute a :other musí být jiné.',
|
||||
'digits' => ':attribute musí mít :digits číslic.',
|
||||
'digits_between' => ':attribute musí mít mezi :min a :max číslicemi.',
|
||||
'dimensions' => ':attribute má neplatné rozměry obrázku.',
|
||||
'distinct' => 'Pole :attribute má duplicitní hodnotu.',
|
||||
'email' => ':attribute musí být platná e-mailová adresa.',
|
||||
'exists' => 'Zvolený :attribute je neplatný.',
|
||||
'file' => ':attribute musí být soubor.',
|
||||
'filled' => 'Pole :attribute musí mít hodnotu.',
|
||||
'image' => ':attribute musí být obrázek.',
|
||||
'in' => 'Zvolený :attribute je neplatný.',
|
||||
'in_array' => 'Pole :attribute neexistuje v :other.',
|
||||
'integer' => ':attribute musí být celé číslo.',
|
||||
'ip' => ':attribute musí být platná IP adresa.',
|
||||
'ipv4' => ':attribute musí být platná IPv4 adresa.',
|
||||
'ipv6' => ':attribute musí být platná IPv6 adresa.',
|
||||
'json' => ':attribute musí být platný řetězec JSON.',
|
||||
'max' => [
|
||||
'numeric' => ':attribute nesmí být větší než :max.',
|
||||
'file' => ':attribute nesmí být větší než :max kilobytů.',
|
||||
'string' => ':attribute nesmí být větší než :max znaků.',
|
||||
'array' => ':attribute nesmí mít více než :max položek.',
|
||||
],
|
||||
'mimes' => ':attribute musí být soubor typu: :values.',
|
||||
'mimetypes' => ':attribute musí být soubor typu: :values.',
|
||||
'min' => [
|
||||
'numeric' => ':attribute musí být alespoň :min.',
|
||||
'file' => ':attribute musí být alespoň :min kilobytů.',
|
||||
'string' => ':attribute musí být alespoň :min znaků.',
|
||||
'array' => ':attribute musí mít alespoň :min položek.',
|
||||
],
|
||||
'not_in' => 'Zvolený :attribute je neplatný.',
|
||||
'not_regex' => 'Formát :attribute je neplatný.',
|
||||
'numeric' => ':attribute musí být číslo.',
|
||||
'present' => 'Pole :attribute musí být přítomné.',
|
||||
'regex' => 'Formát :attribute je neplatný.',
|
||||
'required' => 'Pole :attribute je vyžadováno.',
|
||||
'required_if' => 'Pole :attribute je vyžadováno, pokud je :other :value.',
|
||||
'required_unless' => 'Pole :attribute je vyžadováno, pokud není :other v :values.',
|
||||
'required_with' => 'Pole :attribute je vyžadováno, pokud je přítomno :values.',
|
||||
'required_with_all' => 'Pole :attribute je vyžadováno, pokud je přítomno :values.',
|
||||
'required_without' => 'Pole :attribute je vyžadováno, pokud není přítomno :values.',
|
||||
'required_without_all' => 'Pole :attribute je vyžadováno, pokud není přítomno žádné z :values.',
|
||||
'same' => ':attribute a :other se musí shodovat.',
|
||||
'size' => [
|
||||
'numeric' => ':attribute musí být :size.',
|
||||
'file' => ':attribute musí být :size kilobytů.',
|
||||
'string' => ':attribute musí být :size znaků.',
|
||||
'array' => ':attribute musí obsahovat :size položek.',
|
||||
],
|
||||
'string' => ':attribute musí být řetězec.',
|
||||
'timezone' => ':attribute musí být platná zóna.',
|
||||
'unique' => ':attribute je již zabráno.',
|
||||
'uploaded' => 'Nahrávání :attribute selhalo.',
|
||||
'url' => 'Formát :attribute je neplatný.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap attribute place-holders
|
||||
| with something more reader friendly such as E-Mail Address instead
|
||||
| of "email". This simply helps us make messages a little cleaner.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
49
resources/views/auth/checkpoint.blade.php
Normal file
49
resources/views/auth/checkpoint.blade.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-5">
|
||||
<div class="text-center">
|
||||
<img src="/img/pixelfed-icon-color.svg" height="60px">
|
||||
<p class="font-weight-light h3 py-4">Verify 2FA Code to continue</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="form-group row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<input id="code" type="code" class="form-control{{ $errors->has('code') ? ' is-invalid' : '' }}" name="code" placeholder="{{__('Two-Factor Authentication Code')}}" required autocomplete="off">
|
||||
|
||||
@if ($errors->has('code'))
|
||||
<span class="invalid-feedback">
|
||||
<strong>{{ $errors->first('code') }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(config('pixelfed.recaptcha'))
|
||||
<div class="row my-3">
|
||||
{!! Recaptcha::render() !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group row mb-0">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" class="btn btn-success btn-block font-weight-bold">
|
||||
{{ __('Verify') }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
|
@ -24,7 +24,7 @@
|
|||
</li>
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link nav-notification" href="{{route('notifications')}}" title="Notifications" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-heart fa-lg text"></i>
|
||||
<i class="fas fa-inbox fa-lg text"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item px-2">
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<li class="nav-item pl-3 {{request()->is('settings/home')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings')}}">Profile</a>
|
||||
</li>
|
||||
{{-- <li class="nav-item pl-3 {{request()->is('settings/avatar')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.avatar')}}">Avatar</a>
|
||||
</li> --}}
|
||||
<li class="nav-item pl-3 {{request()->is('settings/password')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.password')}}">Password</a>
|
||||
</li>
|
||||
|
@ -14,18 +11,18 @@
|
|||
</li>
|
||||
<li class="nav-item pl-3 {{request()->is('settings/notifications')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.notifications')}}">Notifications</a>
|
||||
</li>
|
||||
--}}
|
||||
</li> --}}
|
||||
|
||||
<li class="nav-item pl-3 {{request()->is('settings/privacy*')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.privacy')}}">Privacy</a>
|
||||
</li>
|
||||
{{-- <li class="nav-item pl-3 {{request()->is('settings/security')?'active':''}}">
|
||||
<li class="nav-item pl-3 {{request()->is('settings/security*')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.security')}}">Security</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{{-- <li class="nav-item">
|
||||
<hr>
|
||||
</li>
|
||||
<li class="nav-item pl-3 {{request()->is('settings/import*')?'active':''}}">
|
||||
<li class="nav-item pl-3 {{request()->is('*import*')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.import')}}">Import</a>
|
||||
</li>
|
||||
<li class="nav-item pl-3 {{request()->is('settings/data-export')?'active':''}}">
|
||||
|
@ -36,10 +33,10 @@
|
|||
<hr>
|
||||
</li>
|
||||
<li class="nav-item pl-3 {{request()->is('settings/applications')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.applications')}}">Applications</a>
|
||||
<a class="nav-link font-weight-light text-muted" href="#">Applications</a>
|
||||
</li>
|
||||
<li class="nav-item pl-3 {{request()->is('settings/developers')?'active':''}}">
|
||||
<a class="nav-link font-weight-light text-muted" href="{{route('settings.developers')}}">Developers</a>
|
||||
<a class="nav-link font-weight-light text-muted" href="#">Developers</a>
|
||||
</li> --}}
|
||||
</ul>
|
||||
</div>
|
|
@ -3,11 +3,27 @@
|
|||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Security Settings</h3>
|
||||
<h3 class="font-weight-bold">Security</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="alert alert-danger">
|
||||
Coming Soon
|
||||
|
||||
<section class="pt-4">
|
||||
<div class="mb-4 pb-4">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="font-weight-bold mb-0">Two-factor authentication</h4>
|
||||
@if($user->{'2fa_enabled'})
|
||||
<a class="btn btn-success btn-sm font-weight-bold" href="#">Enabled</a>
|
||||
@endif
|
||||
</div>
|
||||
<hr>
|
||||
@if($user->{'2fa_enabled'})
|
||||
@include('settings.security.2fa.partial.edit-panel')
|
||||
@else
|
||||
@include('settings.security.2fa.partial.disabled-panel')
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@include('settings.security.2fa.partial.log-panel')
|
||||
</section>
|
||||
|
||||
@endsection
|
82
resources/views/settings/security/2fa/edit.blade.php
Normal file
82
resources/views/settings/security/2fa/edit.blade.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
@extends('settings.template')
|
||||
|
||||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Edit Two-Factor Authentication</h3>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="lead pb-3">
|
||||
To register a new device, you have to remove any active devices.
|
||||
</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-light font-weight-bold">
|
||||
Authenticator App
|
||||
</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<i class="fas fa-lock fa-3x text-success"></i>
|
||||
<p class="font-weight-bold mb-0">
|
||||
Added {{$user->{'2fa_setup_at'}->diffForHumans()}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer bg-white text-right">
|
||||
<a class="btn btn-outline-secondary btn-sm px-4 font-weight-bold mr-3" href="{{route('settings.security.2fa.recovery')}}">View Recovery Codes</a>
|
||||
<a class="btn btn-outline-danger btn-sm px-4 font-weight-bold remove-device" href="#">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
$(document).on('click', '.remove-device', function(e) {
|
||||
e.preventDefault();
|
||||
swal({
|
||||
title: 'Confirm Device Removal',
|
||||
text: 'Are you sure you want to remove this two-factor authentication device from your account?',
|
||||
icon: 'warning',
|
||||
button: {
|
||||
text: 'Confirm Removal',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
})
|
||||
.then((value) => {
|
||||
if(value == true) {
|
||||
swal({
|
||||
title: 'Are you really sure?',
|
||||
text: 'Are you really sure you want to remove this two-factor authentication device from your account?',
|
||||
icon: 'warning',
|
||||
button: {
|
||||
text: 'Confirm Removal',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
})
|
||||
.then((value) => {
|
||||
if(value == true) {
|
||||
axios.post('/settings/security/2fa/edit', {
|
||||
action: 'remove'
|
||||
})
|
||||
.then(function(res) {
|
||||
window.location.href = '/settings/security';
|
||||
})
|
||||
.catch(function(res) {
|
||||
swal(
|
||||
'Oops!',
|
||||
'Something went wrong. Please try again.',
|
||||
'error'
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
|
@ -0,0 +1,18 @@
|
|||
<ul class="list-group">
|
||||
<li class="list-group-item bg-light">
|
||||
<div class="text-center py-5 px-4">
|
||||
<p class="text-muted">
|
||||
<i class="fas fa-lock fa-2x"></i>
|
||||
</p>
|
||||
<p class="text-muted h4 font-weight-bold">
|
||||
Two factor authentication is not enabled yet.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. <a href="#">Learn more</a>.
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<a class="btn btn-success font-weight-bold" href="{{route('settings.security.2fa.setup')}}">Enable two-factor authentication</a>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
|
@ -0,0 +1,30 @@
|
|||
<p>Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. <a href="#">Learn more</a>.</p>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<span class="font-weight-bold">
|
||||
Two-factor methods
|
||||
</span>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center py-2">
|
||||
<div>Authenticator App</div>
|
||||
<div><a class="btn btn-secondary btn-sm font-weight-bold" href="{{route('settings.security.2fa.edit')}}">Edit</a></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div><div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<span class="font-weight-bold">
|
||||
Recovery Options
|
||||
</span>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center py-2">
|
||||
<div>Recovery Codes</div>
|
||||
<div><a class="btn btn-secondary btn-sm font-weight-bold" href="{{route('settings.security.2fa.recovery')}}">View</a></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,42 @@
|
|||
<div class="mb-4 pb-4">
|
||||
<h4 class="font-weight-bold">Account Log</h4>
|
||||
<hr>
|
||||
<ul class="list-group" style="max-height: 400px;overflow-y: scroll;">
|
||||
@if($activity->count() == 0)
|
||||
<p class="alert alert-info font-weight-bold">No activity logs found!</p>
|
||||
@endif
|
||||
@foreach($activity as $log)
|
||||
<li class="list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-body">
|
||||
<span class="my-0 font-weight-bold text-muted">
|
||||
{{$log->action}} - <span class="font-weight-normal">{{$log->message}}</span>
|
||||
</span>
|
||||
<span class="mb-0 text-muted float-right">
|
||||
{{$log->created_at->diffForHumans(null, false, false, false)}}
|
||||
<span class="pl-2" data-toggle="collapse" href="#log-details-{{$log->id}}" role="button" aria-expanded="false" aria-controls="log-details-{{$log->id}}">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</span>
|
||||
</span>
|
||||
<div class="collapse" id="log-details-{{$log->id}}">
|
||||
<div class="py-2">
|
||||
<p class="mb-0">
|
||||
<span class="font-weight-bold">IP Address:</span>
|
||||
<span>
|
||||
{{$log->ip_address}}
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<span class="font-weight-bold">User Agent:</span>
|
||||
<span>
|
||||
{{$log->user_agent}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
@extends('settings.template')
|
||||
|
||||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Two-Factor Authentication Recovery Codes</h3>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="lead pb-3">
|
||||
Each code can only be used once.
|
||||
</p>
|
||||
|
||||
<p class="lead"></p>
|
||||
<ul class="list-group">
|
||||
@foreach($codes as $code)
|
||||
<li class="list-group-item"><code>{{$code}}</code></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
@endsection
|
134
resources/views/settings/security/2fa/setup.blade.php
Normal file
134
resources/views/settings/security/2fa/setup.blade.php
Normal file
|
@ -0,0 +1,134 @@
|
|||
@extends('settings.template')
|
||||
|
||||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Setup Two-Factor Authentication</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="alert alert-info font-weight-light mb-3">
|
||||
We only support Two-Factor Authentication via TOTP mobile apps.
|
||||
</div>
|
||||
<section class="step-one pb-5">
|
||||
<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step1" aria-expanded="true" aria-controls="step1" data-step="1">
|
||||
Step 1: Install compatible 2FA mobile app <i class="float-right fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="collapse show" id="step1">
|
||||
<p>You will need to install a compatible mobile app, we recommend the following apps:</p>
|
||||
<ul>
|
||||
<li><a href="https://1password.com/downloads/" rel="nooopener nofollow">1Password</a></li>
|
||||
<li><a href="https://authy.com/download/" rel="nooopener nofollow">Authy</a></li>
|
||||
<li><a href="https://lastpass.com/auth/" rel="nooopener nofollow">LastPass Authenticator</a></li>
|
||||
<li>
|
||||
Google Authenticator
|
||||
<a class="small" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_CA" rel="nooopener nofollow">
|
||||
(android)
|
||||
</a>
|
||||
<a class="small" href="https://itunes.apple.com/ca/app/google-authenticator/id388497605?mt=8" rel="nooopener nofollow">
|
||||
(iOS)
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="https://www.microsoft.com/en-us/account/authenticator" rel="nooopener nofollow">Microsoft Authenticator</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step-two pb-5">
|
||||
<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step2" aria-expanded="false" aria-controls="step2" data-step="2">
|
||||
Step 2: Scan QR Code and confirm <i class="float-right fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="collapse" id="step2">
|
||||
<p>Please scan the QR code and then enter the 6 digit code in the form below. Keep in mind the code changes every 30 seconds, and is only good for 1 minute.</p>
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<img src="{{$qrcode}}">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="confirm-code">
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold small">Code</label>
|
||||
<input type="text" name="code" id="verifyCode" class="form-control" placeholder="Code" autocomplete="off">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary font-weight-bold">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step-three pb-5">
|
||||
<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step3" aria-expanded="true" aria-controls="step3" data-step="3">
|
||||
Step 3: Download Backup Codes <i class="float-right fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="collapse" id="step3">
|
||||
<p>Please store the following codes in a safe place, each backup code can be used only once if you do not have access to your 2FA mobile app.</p>
|
||||
|
||||
<code>
|
||||
@foreach($backups as $code)
|
||||
<p class="mb-0">{{$code}}</p>
|
||||
@endforeach
|
||||
</code>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('#step3').addClass('d-none');
|
||||
window.twoFactor = {};
|
||||
window.twoFactor.validated = false;
|
||||
|
||||
$(document).on('click', 'div[data-toggle=collapse]', function(e) {
|
||||
let el = $(this);
|
||||
let step = el.data('step');
|
||||
|
||||
switch(step) {
|
||||
case 1:
|
||||
$('#step2').collapse('hide');
|
||||
$('#step3').collapse('hide');
|
||||
break;
|
||||
case 2:
|
||||
$('#step1').collapse('hide');
|
||||
$('#step3').collapse('hide');
|
||||
break;
|
||||
case 3:
|
||||
if(twoFactor.validated == false) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
} else {
|
||||
$('#step3').removeClass('d-none');
|
||||
$('#step1').collapse('hide');
|
||||
$('#step2').collapse('hide');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('submit', '#confirm-code', function(e) {
|
||||
e.preventDefault();
|
||||
let el = $(this);
|
||||
let code = $('#verifyCode').val();
|
||||
if(code.length < 5) {
|
||||
swal('Oops!', 'You need to enter a valid code', 'error');
|
||||
return;
|
||||
}
|
||||
axios.post(window.location.href, {
|
||||
code: code
|
||||
}).then((res) => {
|
||||
twoFactor.validated = true;
|
||||
$('#step3').removeClass('d-none');
|
||||
$('#step3').collapse('show');
|
||||
$('#step1').collapse('hide');
|
||||
$('#step2').collapse('hide');
|
||||
}).catch((res) => {
|
||||
swal('Oops!', 'That was an invalid code, please try again.', 'error');
|
||||
return;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
|
@ -12,7 +12,3 @@ use Illuminate\Http\Request;
|
|||
| is assigned the "api" middleware group. Enjoy building your API!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware('auth:api')->get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
|
|
|
@ -12,7 +12,3 @@ use Illuminate\Foundation\Inspiring;
|
|||
| simple approach to interacting with each command's IO methods.
|
||||
|
|
||||
*/
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->describe('Display an inspiring quote');
|
||||
|
|
|
@ -18,7 +18,7 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
|
|||
Route::get('media/list', 'AdminController@media')->name('admin.media');
|
||||
});
|
||||
|
||||
Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(function () {
|
||||
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor'])->group(function () {
|
||||
Route::get('/', 'SiteController@home')->name('timeline.personal');
|
||||
Route::post('/', 'StatusController@store')->middleware('throttle:500,1440');
|
||||
|
||||
|
@ -31,10 +31,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
|
|||
|
||||
Route::get('discover', 'DiscoverController@home')->name('discover');
|
||||
|
||||
Route::get('search/hashtag/{tag}', function ($tag) {
|
||||
return redirect('/discover/tags/'.$tag);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'api'], function () {
|
||||
Route::get('search/{tag}', 'SearchController@searchAPI')
|
||||
->where('tag', '[A-Za-z0-9]+');
|
||||
|
@ -64,12 +60,15 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
|
|||
Route::post('follow', 'FollowerController@store')->middleware('throttle:250,1440');
|
||||
Route::post('bookmark', 'BookmarkController@store')->middleware('throttle:250,1440');
|
||||
Route::get('lang/{locale}', 'SiteController@changeLocale');
|
||||
|
||||
Route::get('verify-email', 'AccountController@verifyEmail');
|
||||
Route::post('verify-email', 'AccountController@sendVerifyEmail')->middleware('throttle:10,1440');
|
||||
Route::get('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail')->middleware('throttle:10,1440');
|
||||
|
||||
Route::get('auth/sudo', 'AccountController@sudoMode');
|
||||
Route::post('auth/sudo', 'AccountController@sudoModeVerify');
|
||||
Route::get('auth/checkpoint', 'AccountController@twoFactorCheckpoint');
|
||||
Route::post('auth/checkpoint', 'AccountController@twoFactorVerify');
|
||||
|
||||
Route::group(['prefix' => 'report'], function () {
|
||||
Route::get('/', 'ReportController@showForm')->name('report.form');
|
||||
|
@ -97,7 +96,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
|
|||
|
||||
Route::group(['prefix' => 'settings'], function () {
|
||||
Route::redirect('/', '/settings/home');
|
||||
Route::get('home', 'SettingsController@home')->name('settings');
|
||||
Route::get('home', 'SettingsController@home')
|
||||
->name('settings');
|
||||
Route::post('home', 'SettingsController@homeUpdate')->middleware('throttle:25,1440');
|
||||
Route::get('avatar', 'SettingsController@avatar')->name('settings.avatar');
|
||||
Route::post('avatar', 'AvatarController@store')->middleware('throttle:5,1440');
|
||||
|
@ -112,7 +112,34 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
|
|||
Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users');
|
||||
Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate')->middleware('throttle:100,1440');
|
||||
Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances');
|
||||
Route::get('security', 'SettingsController@security')->name('settings.security');
|
||||
|
||||
Route::group(['prefix' => 'security', 'middleware' => 'dangerzone'], function() {
|
||||
Route::get(
|
||||
'/',
|
||||
'SettingsController@security'
|
||||
)->name('settings.security');
|
||||
Route::get(
|
||||
'2fa/setup',
|
||||
'SettingsController@securityTwoFactorSetup'
|
||||
)->name('settings.security.2fa.setup');
|
||||
Route::post(
|
||||
'2fa/setup',
|
||||
'SettingsController@securityTwoFactorSetupStore'
|
||||
);
|
||||
Route::get(
|
||||
'2fa/edit',
|
||||
'SettingsController@securityTwoFactorEdit'
|
||||
)->name('settings.security.2fa.edit');
|
||||
Route::post(
|
||||
'2fa/edit',
|
||||
'SettingsController@securityTwoFactorUpdate'
|
||||
);
|
||||
Route::get(
|
||||
'2fa/recovery-codes',
|
||||
'SettingsController@securityTwoFactorRecoveryCodes'
|
||||
)->name('settings.security.2fa.recovery');
|
||||
});
|
||||
|
||||
Route::get('applications', 'SettingsController@applications')->name('settings.applications');
|
||||
Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport');
|
||||
Route::get('developers', 'SettingsController@developers')->name('settings.developers');
|
||||
|
|
Loading…
Reference in a new issue