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

Add 2FA
This commit is contained in:
daniel 2018-09-16 19:58:37 -06:00 committed by GitHub
commit ee6348a873
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1164 additions and 275 deletions

View file

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

View file

@ -19,7 +19,8 @@ class AdminController extends Controller
public function __construct()
{
return $this->middleware('admin');
$this->middleware('admin');
$this->middleware('twofactor');
}
public function home()

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

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

View 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);
}
}

View file

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

View file

@ -14,6 +14,7 @@ class TimelineController extends Controller
public function __construct()
{
$this->middleware('auth');
$this->middleware('twofactor');
}
public function personal()

View file

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

View 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);
}
}

View file

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

View file

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

View file

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

View 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.',
];

View 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',
];

View 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.',
];

View 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ší »',
];

View 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.",
];

View 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í.',
];

View file

@ -0,0 +1,7 @@
<?php
return [
'emptyPersonalTimeline' => 'Vaše časová osa je prázdná.',
];

View 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' => [],
];

View 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

View file

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

View file

@ -3,29 +3,26 @@
<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>
{{-- <li class="nav-item pl-3 {{request()->is('settings/email')?'active':''}}">
{{-- <li class="nav-item pl-3 {{request()->is('settings/email')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.email')}}">Email</a>
</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">
<hr>
{{-- <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>

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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