diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index b7f567d7e..f7af111cf 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -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' + ]); + } + } } diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 97cc1ff5e..8f04879eb 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -19,7 +19,8 @@ class AdminController extends Controller public function __construct() { - return $this->middleware('admin'); + $this->middleware('admin'); + $this->middleware('twofactor'); } public function home() diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php new file mode 100644 index 000000000..591d3e451 --- /dev/null +++ b/app/Http/Controllers/Settings/HomeSettings.php @@ -0,0 +1,153 @@ +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'); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php new file mode 100644 index 000000000..dd01feb63 --- /dev/null +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -0,0 +1,127 @@ +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'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/SecuritySettings.php b/app/Http/Controllers/Settings/SecuritySettings.php new file mode 100644 index 000000000..99547b73b --- /dev/null +++ b/app/Http/Controllers/Settings/SecuritySettings.php @@ -0,0 +1,139 @@ +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); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 670bf3b2c..9b6c85f73 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -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'); - } } diff --git a/app/Http/Controllers/TimelineController.php b/app/Http/Controllers/TimelineController.php index 1ce714b9f..58665fb0a 100644 --- a/app/Http/Controllers/TimelineController.php +++ b/app/Http/Controllers/TimelineController.php @@ -14,6 +14,7 @@ class TimelineController extends Controller public function __construct() { $this->middleware('auth'); + $this->middleware('twofactor'); } public function personal() diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index b90d197a4..eb8a2a4f7 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, ]; } diff --git a/app/Http/Middleware/TwoFactorAuth.php b/app/Http/Middleware/TwoFactorAuth.php new file mode 100644 index 000000000..9eb742e61 --- /dev/null +++ b/app/Http/Middleware/TwoFactorAuth.php @@ -0,0 +1,32 @@ +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); + } +} diff --git a/app/ImportJob.php b/app/ImportJob.php index 52b1187d7..76a440d43 100644 --- a/app/ImportJob.php +++ b/app/ImportJob.php @@ -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}"); diff --git a/app/User.php b/app/User.php index 4e014f046..7fba2c170 100644 --- a/app/User.php +++ b/app/User.php @@ -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. diff --git a/app/Util/Lexer/RestrictedNames.php b/app/Util/Lexer/RestrictedNames.php index 1bd67e81f..0bd2648b6 100644 --- a/app/Util/Lexer/RestrictedNames.php +++ b/app/Util/Lexer/RestrictedNames.php @@ -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', diff --git a/resources/lang/cs/auth.php b/resources/lang/cs/auth.php new file mode 100644 index 000000000..a6568920d --- /dev/null +++ b/resources/lang/cs/auth.php @@ -0,0 +1,19 @@ + '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.', + +]; diff --git a/resources/lang/cs/navmenu.php b/resources/lang/cs/navmenu.php new file mode 100644 index 000000000..7ca3c49b5 --- /dev/null +++ b/resources/lang/cs/navmenu.php @@ -0,0 +1,14 @@ + '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', + +]; diff --git a/resources/lang/cs/notification.php b/resources/lang/cs/notification.php new file mode 100644 index 000000000..d93d21e65 --- /dev/null +++ b/resources/lang/cs/notification.php @@ -0,0 +1,10 @@ + '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.', + +]; diff --git a/resources/lang/cs/pagination.php b/resources/lang/cs/pagination.php new file mode 100644 index 000000000..865f9f0cb --- /dev/null +++ b/resources/lang/cs/pagination.php @@ -0,0 +1,19 @@ + '« Předchozí', + 'next' => 'Další »', + +]; diff --git a/resources/lang/cs/passwords.php b/resources/lang/cs/passwords.php new file mode 100644 index 000000000..869237f27 --- /dev/null +++ b/resources/lang/cs/passwords.php @@ -0,0 +1,22 @@ + '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.", + +]; diff --git a/resources/lang/cs/profile.php b/resources/lang/cs/profile.php new file mode 100644 index 000000000..f6fa1fee2 --- /dev/null +++ b/resources/lang/cs/profile.php @@ -0,0 +1,12 @@ + '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í.', +]; diff --git a/resources/lang/cs/timeline.php b/resources/lang/cs/timeline.php new file mode 100644 index 000000000..9516c0305 --- /dev/null +++ b/resources/lang/cs/timeline.php @@ -0,0 +1,7 @@ + 'Vaše časová osa je prázdná.', + +]; diff --git a/resources/lang/cs/validation.php b/resources/lang/cs/validation.php new file mode 100644 index 000000000..fc23045ba --- /dev/null +++ b/resources/lang/cs/validation.php @@ -0,0 +1,122 @@ + ':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' => [], + +]; diff --git a/resources/views/auth/checkpoint.blade.php b/resources/views/auth/checkpoint.blade.php new file mode 100644 index 000000000..24c4ae775 --- /dev/null +++ b/resources/views/auth/checkpoint.blade.php @@ -0,0 +1,49 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+
+ +

Verify 2FA Code to continue

+
+
+
+
+ @csrf + +
+ +
+ + + @if ($errors->has('code')) + + {{ $errors->first('code') }} + + @endif +
+
+ + @if(config('pixelfed.recaptcha')) +
+ {!! Recaptcha::render() !!} +
+ @endif + +
+
+ + +
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/layouts/partial/nav.blade.php b/resources/views/layouts/partial/nav.blade.php index 9a773930c..7a4ef5d9a 100644 --- a/resources/views/layouts/partial/nav.blade.php +++ b/resources/views/layouts/partial/nav.blade.php @@ -24,7 +24,7 @@ -{{-- --}} - {{-- - --}} + --}} + -{{-- - - --}} \ No newline at end of file diff --git a/resources/views/settings/security.blade.php b/resources/views/settings/security.blade.php index 44f752557..2002b3a61 100644 --- a/resources/views/settings/security.blade.php +++ b/resources/views/settings/security.blade.php @@ -3,11 +3,27 @@ @section('section')
-

Security Settings

+

Security


-
- Coming Soon -
+ +
+
+
+

Two-factor authentication

+ @if($user->{'2fa_enabled'}) + Enabled + @endif +
+
+ @if($user->{'2fa_enabled'}) + @include('settings.security.2fa.partial.edit-panel') + @else + @include('settings.security.2fa.partial.disabled-panel') + @endif +
+ + @include('settings.security.2fa.partial.log-panel') +
@endsection \ No newline at end of file diff --git a/resources/views/settings/security/2fa/edit.blade.php b/resources/views/settings/security/2fa/edit.blade.php new file mode 100644 index 000000000..8b895da07 --- /dev/null +++ b/resources/views/settings/security/2fa/edit.blade.php @@ -0,0 +1,82 @@ +@extends('settings.template') + +@section('section') + +
+

Edit Two-Factor Authentication

+
+ +
+ +

+ To register a new device, you have to remove any active devices. +

+ +
+
+ Authenticator App +
+
+ +

+ Added {{$user->{'2fa_setup_at'}->diffForHumans()}} +

+
+ +
+ +@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/settings/security/2fa/partial/disabled-panel.blade.php b/resources/views/settings/security/2fa/partial/disabled-panel.blade.php new file mode 100644 index 000000000..9c1f1be14 --- /dev/null +++ b/resources/views/settings/security/2fa/partial/disabled-panel.blade.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/resources/views/settings/security/2fa/partial/edit-panel.blade.php b/resources/views/settings/security/2fa/partial/edit-panel.blade.php new file mode 100644 index 000000000..b9149e0ca --- /dev/null +++ b/resources/views/settings/security/2fa/partial/edit-panel.blade.php @@ -0,0 +1,30 @@ +

Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. Learn more.

+
+
+ + Two-factor methods + +
+ +
+
+ + Recovery Options + +
+ +
\ No newline at end of file diff --git a/resources/views/settings/security/2fa/partial/log-panel.blade.php b/resources/views/settings/security/2fa/partial/log-panel.blade.php new file mode 100644 index 000000000..7eb8aaf4e --- /dev/null +++ b/resources/views/settings/security/2fa/partial/log-panel.blade.php @@ -0,0 +1,42 @@ +
+

Account Log

+
+ +
\ No newline at end of file diff --git a/resources/views/settings/security/2fa/recovery-codes.blade.php b/resources/views/settings/security/2fa/recovery-codes.blade.php new file mode 100644 index 000000000..47f37af29 --- /dev/null +++ b/resources/views/settings/security/2fa/recovery-codes.blade.php @@ -0,0 +1,22 @@ +@extends('settings.template') + +@section('section') + +
+

Two-Factor Authentication Recovery Codes

+
+ +
+ +

+ Each code can only be used once. +

+ +

+ + +@endsection \ No newline at end of file diff --git a/resources/views/settings/security/2fa/setup.blade.php b/resources/views/settings/security/2fa/setup.blade.php new file mode 100644 index 000000000..4024df05d --- /dev/null +++ b/resources/views/settings/security/2fa/setup.blade.php @@ -0,0 +1,134 @@ +@extends('settings.template') + +@section('section') + +
+

Setup Two-Factor Authentication

+
+
+
+ We only support Two-Factor Authentication via TOTP mobile apps. +
+
+
+ Step 1: Install compatible 2FA mobile app +
+
+
+

You will need to install a compatible mobile app, we recommend the following apps:

+ +
+
+ +
+ +
+
+

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.

+
+
+ +
+
+
+
+ + +
+ +
+
+
+
+
+ +
+
+ Step 3: Download Backup Codes +
+
+
+

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.

+ + + @foreach($backups as $code) +

{{$code}}

+ @endforeach +
+
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index c641ca5e5..852967954 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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(); -}); diff --git a/routes/console.php b/routes/console.php index 75dd0cded..42cca27da 100644 --- a/routes/console.php +++ b/routes/console.php @@ -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'); diff --git a/routes/web.php b/routes/web.php index 194e3199d..bca457676 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');