update: use fortify for authentication

This commit is contained in:
Roardom
2023-06-13 21:37:26 +00:00
parent 531628124e
commit 326adb5c08
32 changed files with 1175 additions and 852 deletions

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Actions\Fortify;
use App\Models\Group;
use App\Models\Invite;
use App\Models\PrivateMessage;
use App\Models\User;
use App\Repositories\ChatRepository;
use App\Rules\EmailBlacklist;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
public function __construct(private readonly ChatRepository $chatRepository)
{
}
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'username' => 'required|alpha_dash|string|between:3,25|unique:users',
'password' => [
'required',
'confirmed',
$this->passwordRules(),
],
'email' => [
'required',
'string',
'email',
'max:70',
'unique:users',
Rule::when(config('email-blacklist.enabled') === true, fn () => new EmailBlacklist()),
],
'captcha' => [
Rule::excludeIf(config('captcha.enabled') === false),
Rule::when(config('captcha.enabled') === true, 'hiddencaptcha'),
],
'code' => 'required',
])->validate();
// Make sure open reg is off and invite code exists and has not been used already
$invite = Invite::query()->where('code', '=', $input['code'])->first();
if (config('other.invite-only') === true && ($invite === null || $invite->accepted_by !== null)) {
return to_route('registrationForm', ['code' => $input['code']])
->withErrors(trans('auth.invalid-key'));
}
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id'));
$user = User::create([
'username' => $input['username'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
'passkey' => md5(random_bytes(60)),
'rsskey' => md5(random_bytes(60)),
'uploaded' => config('other.default_upload'),
'downloaded' => config('other.default_download'),
'style' => config('other.default_style', 0),
'locale' => config('app.locale'),
'group_id' => $validatingGroup[0],
]);
if ($invite !== null) {
$invite->update([
'accepted_by' => $user->id,
'accepted_at' => new Carbon(),
]);
}
// Select A Random Welcome Message
$profileUrl = href_profile($user);
$welcomeArray = [
sprintf('[url=%s]%s[/url], Welcome to ', $profileUrl, $user->username).config('other.title').'! Hope you enjoy the community :rocket:',
sprintf("[url=%s]%s[/url], We've been expecting you :space_invader:", $profileUrl, $user->username),
sprintf("[url=%s]%s[/url] has arrived. Party's over. :cry:", $profileUrl, $user->username),
sprintf("It's a bird! It's a plane! Nevermind, it's just [url=%s]%s[/url].", $profileUrl, $user->username),
sprintf('Ready player [url=%s]%s[/url].', $profileUrl, $user->username),
sprintf('A wild [url=%s]%s[/url] appeared.', $profileUrl, $user->username),
'Welcome to '.config('other.title').sprintf(' [url=%s]%s[/url]. We were expecting you ( ͡° ͜ʖ ͡°)', $profileUrl, $user->username),
];
$this->chatRepository->systemMessage(
$welcomeArray[array_rand($welcomeArray)]
);
// Send Welcome PM
PrivateMessage::create([
'sender_id' => 1,
'receiver_id' => $user->id,
'subject' => config('welcomepm.subject'),
'message' => config('welcomepm.message'),
]);
return to_route('login')
->withSuccess(trans('auth.register-thanks'));
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Validation\Rules\Password as Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
*/
protected function passwordRules(): array
{
return [
'required',
'string',
Password::min(12)->mixedCase()->letters()->numbers()->uncompromised(),
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Actions\Fortify;
use App\Models\Group;
use App\Models\User;
use App\Models\UserActivation;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::query()->where('slug', '=', 'user')->pluck('id'));
if ($user->group_id === $validatingGroup[0]) {
$user->group_id = $memberGroup[0];
}
$user->active = true;
$user->save();
UserActivation::query()->where('user_id', '=', $user->id)->delete();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}

View File

@@ -1,53 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Group;
use App\Models\UserActivation;
use App\Services\Unit3dAnnounce;
/**
* @see \Tests\Feature\Http\Controllers\Auth\ActivationControllerTest
*/
class ActivationController extends Controller
{
public function activate($token): \Illuminate\Http\RedirectResponse
{
$bannedGroup = cache()->rememberForever('banned_group', fn () => Group::where('slug', '=', 'banned')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::where('slug', '=', 'user')->pluck('id'));
$activation = UserActivation::with('user')->where('token', '=', $token)->firstOrFail();
if ($activation->user->id && $activation->user->group->id != $bannedGroup[0]) {
$activation->user->active = 1;
$activation->user->can_upload = 1;
$activation->user->can_download = 1;
$activation->user->can_request = 1;
$activation->user->can_comment = 1;
$activation->user->can_invite = 1;
$activation->user->group_id = $memberGroup[0];
$activation->user->save();
$activation->delete();
Unit3dAnnounce::addUser($activation->user);
return to_route('login')
->withSuccess(trans('auth.activation-success'));
}
return to_route('login')
->withErrors(trans('auth.activation-error'));
}
}

View File

@@ -1,40 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails;
public function __construct()
{
$this->middleware('guest');
}
protected function validateEmail(Request $request): void
{
if (! config('captcha.enabled')) {
$request->validate(['email' => 'required|email']);
} else {
$request->validate([
'email' => 'required|email',
'captcha' => 'hiddencaptcha',
]);
}
}
}

View File

@@ -1,69 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Notifications\UsernameReminder;
use Illuminate\Http\Request;
/**
* @see \Tests\Feature\Http\Controllers\Auth\ForgotUsernameControllerTest
*/
class ForgotUsernameController extends Controller
{
/**
* Forgot Username Form.
*/
public function showForgotUsernameForm(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return view('auth.username');
}
/**
* Send Username Reminder.
*/
public function sendUsernameReminder(Request $request): \Illuminate\Http\RedirectResponse
{
$email = $request->get('email');
if (! config('captcha.enabled')) {
$v = validator($request->all(), [
'email' => 'required',
]);
} else {
$v = validator($request->all(), [
'email' => 'required',
'captcha' => 'hiddencaptcha',
]);
}
if ($v->fails()) {
return to_route('username.request')
->withErrors($v->errors());
}
$user = User::where('email', '=', $email)->first();
if (empty($user)) {
return to_route('username.request')
->withErrors(trans('email.no-email-found'));
}
//send username reminder notification
$user->notify(new UsernameReminder());
return to_route('login')
->withSuccess(trans('email.username-sent'));
}
}

View File

@@ -1,137 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Group;
use App\Services\Unit3dAnnounce;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
use AuthenticatesUsers;
// Upon Successful Login
protected string $redirectTo = '/';
// Max Attempts Until Lockout
public int $maxAttempts = 3;
// Minutes Lockout
public int $decayMinutes = 60;
/**
* LoginController Constructor.
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'logout']);
}
public function username(): string
{
return 'username';
}
/**
* Validate The User Login Request.
*
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function validateLogin(Request $request): void
{
if (config('captcha.enabled')) {
$this->validate($request, [
$this->username() => 'required|string',
'password' => 'required|string',
'captcha' => 'hiddencaptcha',
]);
} else {
$this->validate($request, [
$this->username() => 'required|string',
'password' => 'required|string',
]);
}
}
protected function authenticated(Request $request, $user): \Illuminate\Http\RedirectResponse
{
$bannedGroup = cache()->rememberForever('banned_group', fn () => Group::where('slug', '=', 'banned')->pluck('id'));
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::where('slug', '=', 'validating')->pluck('id'));
$disabledGroup = cache()->rememberForever('disabled_group', fn () => Group::where('slug', '=', 'disabled')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::where('slug', '=', 'user')->pluck('id'));
if ($user->active == 0 || $user->group_id == $validatingGroup[0]) {
$this->guard()->logout();
$request->session()->invalidate();
return to_route('login')
->withErrors(trans('auth.not-activated'));
}
if ($user->group_id == $bannedGroup[0]) {
$this->guard()->logout();
$request->session()->invalidate();
return to_route('login')
->withErrors(trans('auth.banned'));
}
if ($user->group_id == $disabledGroup[0]) {
$user->group_id = $memberGroup[0];
$user->can_upload = 1;
$user->can_download = 1;
$user->can_comment = 1;
$user->can_invite = 1;
$user->can_request = 1;
$user->can_chat = 1;
$user->disabled_at = null;
$user->save();
cache()->forget('user:'.$user->passkey);
Unit3dAnnounce::addUser($user);
return to_route('home.index')
->withSuccess(trans('auth.welcome-restore'));
}
if (auth()->viaRemember() && $user->group_id == $disabledGroup[0]) {
$user->group_id = $memberGroup[0];
$user->can_upload = 1;
$user->can_download = 1;
$user->can_comment = 1;
$user->can_invite = 1;
$user->can_request = 1;
$user->can_chat = 1;
$user->disabled_at = null;
$user->save();
cache()->forget('user:'.$user->passkey);
Unit3dAnnounce::addUser($user);
return to_route('home.index')
->withSuccess(trans('auth.welcome-restore'));
}
if ($user->read_rules == 0) {
return redirect()->to(config('other.rules_url'))
->withWarning(trans('auth.require-rules'));
}
return redirect()->intended()
->withSuccess(trans('auth.welcome'));
}
}

View File

@@ -1,184 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Jobs\SendActivationMail;
use App\Models\Group;
use App\Models\Invite;
use App\Models\PrivateMessage;
use App\Models\User;
use App\Models\UserActivation;
use App\Models\UserNotification;
use App\Models\UserPrivacy;
use App\Repositories\ChatRepository;
use App\Rules\EmailBlacklist;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class RegisterController extends Controller
{
/**
* RegisterController Constructor.
*/
public function __construct(private readonly ChatRepository $chatRepository)
{
}
/**
* Registration Form.
*/
public function registrationForm($code = null): \Illuminate\Contracts\View\Factory|\Illuminate\View\View|\Illuminate\Http\RedirectResponse
{
// Make sure open reg is off, invite code is not present and application signups enabled
if ($code === 'null' && config('other.invite-only') == 1 && config('other.application_signups')) {
return to_route('application.create')
->withInfo(trans('auth.allow-invite-appl'));
}
// Make sure open reg is off and invite code is not present
if ($code === 'null' && config('other.invite-only') == 1) {
return to_route('login')
->withWarning(trans('auth.allow-invite'));
}
return view('auth.register', ['code' => $code]);
}
public function register(Request $request, $code = null): \Illuminate\Http\RedirectResponse
{
// Make sure open reg is off and invite code exist and has not been used already
$key = Invite::where('code', '=', $code)->first();
if (config('other.invite-only') == 1 && (! $key || $key->accepted_by !== null)) {
return to_route('registrationForm', ['code' => $code])
->withErrors(trans('auth.invalid-key'));
}
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::where('slug', '=', 'validating')->pluck('id'));
$user = new User();
$user->username = $request->input('username');
$user->email = $request->input('email');
$user->password = Hash::make($request->input('password'));
$user->passkey = md5(random_bytes(60).$user->password);
$user->rsskey = md5(random_bytes(60).$user->password);
$user->uploaded = config('other.default_upload');
$user->downloaded = config('other.default_download');
$user->style = config('other.default_style', 0);
$user->locale = config('app.locale');
$user->group_id = $validatingGroup[0];
if (config('email-blacklist.enabled')) {
if (! config('captcha.enabled')) {
$v = validator($request->all(), [
'username' => 'required|alpha_dash|string|between:3,25|unique:users',
'password' => 'required|string|between:8,16',
'email' => [
'required',
'string',
'email',
'max:70',
'unique:users',
new EmailBlacklist(),
],
]);
} else {
$v = validator($request->all(), [
'username' => 'required|alpha_dash|string|between:3,25|unique:users',
'password' => 'required|string|between:8,16',
'email' => [
'required',
'string',
'email',
'max:70',
'unique:users',
new EmailBlacklist(),
],
'captcha' => 'hiddencaptcha',
]);
}
} elseif (! config('captcha.enabled')) {
$v = validator($request->all(), [
'username' => 'required|alpha_dash|string|between:3,25|unique:users',
'password' => 'required|string|between:8,16',
'email' => 'required|string|email|max:70|unique:users',
]);
} else {
$v = validator($request->all(), [
'username' => 'required|alpha_dash|string|between:3,25|unique:users',
'password' => 'required|string|between:6,16',
'email' => 'required|string|email|max:70|unique:users',
'captcha' => 'hiddencaptcha',
]);
}
if ($v->fails()) {
return to_route('registrationForm', ['code' => $code])
->withErrors($v->errors());
}
$user->save();
$userPrivacy = new UserPrivacy();
$userPrivacy->setDefaultValues();
$userPrivacy->user_id = $user->id;
$userPrivacy->save();
$userNotification = new UserNotification();
$userNotification->setDefaultValues();
$userNotification->user_id = $user->id;
$userNotification->save();
if ($key) {
// Update The Invite Record
$key->accepted_by = $user->id;
$key->accepted_at = new Carbon();
$key->save();
}
// Handle The Activation System
$token = hash_hmac('sha256', $user->username.$user->email.Str::random(16), config('app.key'));
$userActivation = new UserActivation();
$userActivation->user_id = $user->id;
$userActivation->token = $token;
$userActivation->save();
dispatch(new SendActivationMail($user, $token));
// Select A Random Welcome Message
$profileUrl = href_profile($user);
$welcomeArray = [
sprintf('[url=%s]%s[/url], Welcome to ', $profileUrl, $user->username).config('other.title').'! Hope you enjoy the community :rocket:',
sprintf("[url=%s]%s[/url], We've been expecting you :space_invader:", $profileUrl, $user->username),
sprintf("[url=%s]%s[/url] has arrived. Party's over. :cry:", $profileUrl, $user->username),
sprintf("It's a bird! It's a plane! Nevermind, it's just [url=%s]%s[/url].", $profileUrl, $user->username),
sprintf('Ready player [url=%s]%s[/url].', $profileUrl, $user->username),
sprintf('A wild [url=%s]%s[/url] appeared.', $profileUrl, $user->username),
'Welcome to '.config('other.title').sprintf(' [url=%s]%s[/url]. We were expecting you ( ͡° ͜ʖ ͡°)', $profileUrl, $user->username),
];
$selected = random_int(0, \count($welcomeArray) - 1);
$this->chatRepository->systemMessage(
$welcomeArray[$selected]
);
// Send Welcome PM
$privateMessage = new PrivateMessage();
$privateMessage->sender_id = 1;
$privateMessage->receiver_id = $user->id;
$privateMessage->subject = config('welcomepm.subject');
$privateMessage->message = config('welcomepm.message');
$privateMessage->save();
return to_route('login')
->withSuccess(trans('auth.register-thanks'));
}
}

View File

@@ -1,51 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Group;
use App\Models\UserActivation;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Str;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
protected string $redirectTo = '/';
public function __construct()
{
$this->middleware('guest');
}
protected function resetPassword($user, $password): void
{
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::where('slug', '=', 'validating')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::where('slug', '=', 'user')->pluck('id'));
$user->password = bcrypt($password);
$user->remember_token = Str::random(60);
if ($user->group_id === $validatingGroup[0]) {
$user->group_id = $memberGroup[0];
}
$user->active = true;
$user->save();
UserActivation::where('user_id', '=', $user->id)->delete();
$this->guard()->login($user);
}
}

View File

@@ -18,18 +18,22 @@ use App\Helpers\Linkify;
use App\Helpers\StringHelper;
use App\Traits\UsersOnlineTrait;
use Assada\Achievements\Achiever;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use voku\helper\AntiXSS;
class User extends Authenticatable
{
use Achiever;
use HasFactory;
use MustVerifyEmail;
use Notifiable;
use SoftDeletes;
use TwoFactorAuthenticatable;
use UsersOnlineTrait;
/**

View File

@@ -1,55 +0,0 @@
<?php
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class UsernameReminder extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* UsernameReminderEmail constructor.
*/
public function __construct()
{
// nothing special to do
}
/**
* Get the notification's delivery channels.
*/
public function via($notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail($notifiable): MailMessage
{
return (new MailMessage())
->subject(trans('common.your').' '.config('app.name').' '.trans('common.username'))
->greeting(trans('common.contact-header').', '.$notifiable->username)
->line(trans('email.username-reminder').' '.$notifiable->username)
->action('Login as '.$notifiable->username, route('login'))
->line(trans('email.thanks').' '.config('app.name'));
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\Group;
use App\Models\User;
use App\Models\UserActivation;
use App\Services\Unit3dAnnounce;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\LoginResponse;
use Laravel\Fortify\Contracts\RegisterViewResponse;
use Laravel\Fortify\Contracts\VerifyEmailResponse;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// Handle redirects after successful login
$this->app->instance(LoginResponse::class, new class () implements LoginResponse {
public function toResponse($request): \Illuminate\Http\RedirectResponse
{
$user = $request->user();
// Check if user is disabled
$disabledGroup = cache()->rememberForever('disabled_group', fn () => Group::query()->where('slug', '=', 'disabled')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::query()->where('slug', '=', 'user')->pluck('id'));
if ($user->group_id == $disabledGroup[0]) {
$user->group_id = $memberGroup[0];
$user->can_upload = 1;
$user->can_download = 1;
$user->can_comment = 1;
$user->can_invite = 1;
$user->can_request = 1;
$user->can_chat = 1;
$user->disabled_at = null;
$user->save();
return to_route('home.index')
->withSuccess('auth.welcome-restore');
}
// Check if user has read the rules
if ($request->user()->read_rules == 0) {
return redirect()->to(config('other.rules_url'))
->withWarning(trans('auth.require-rules'));
}
// Redirect to home page
return redirect()->intended()
->withSuccess(trans('auth.welcome'));
}
});
// Handle redirects before the registration form is shown
$this->app->instance(RegisterViewResponse::class, new class () implements RegisterViewResponse {
public function toResponse($request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
{
// Make sure open reg is off, invite code is not present and application signups enabled
if (! $request->has('code') && config('other.invite-only') && config('other.application_signups')) {
return to_route('application.create')
->withInfo(trans('auth.allow-invite-appl'));
}
// Make sure open reg is off and invite code is not present
if (! $request->has('code') && config('other.invite-only')) {
return to_route('login')
->withWarning(trans('auth.allow-invite'));
}
return view('auth.register', ['code' => $request->query('code')]);
}
});
$this->app->instance(VerifyEmailResponse::class, new class () implements VerifyEmailResponse {
public function toResponse($request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
{
$bannedGroup = cache()->rememberForever('banned_group', fn () => Group::query()->where('slug', '=', 'banned')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::query()->where('slug', '=', 'user')->pluck('id'));
$activation = UserActivation::with('user')->where('token', '=', $request->token)->firstOrFail();
if ($activation->user->id && $activation->user->group->id != $bannedGroup[0]) {
$activation->user->active = 1;
$activation->user->can_upload = 1;
$activation->user->can_download = 1;
$activation->user->can_request = 1;
$activation->user->can_comment = 1;
$activation->user->can_invite = 1;
$activation->user->group_id = $memberGroup[0];
$activation->user->save();
$activation->delete();
Unit3dAnnounce::addUser($activation->user);
return to_route('login')
->withSuccess(trans('auth.activation-success'));
}
return to_route('login')
->withErrors(trans('auth.activation-error'));
}
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::loginView(fn () => view('auth.login'));
Fortify::requestPasswordResetLinkView(fn () => view('auth.passwords.email'));
Fortify::resetPasswordView(fn (Request $request) => view('auth.passwords.reset', ['request' => $request]));
Fortify::confirmPasswordView(fn () => view('auth.confirm-password'));
Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge'));
Fortify::verifyEmailView(fn () => view('auth.verify-email'));
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::authenticateUsing(function (Request $request): User|\Illuminate\Database\Eloquent\ModelNotFoundException {
$request->validate([
'username' => 'required|string',
'password' => 'required|string',
'captcha' => Rule::when(config('captcha.enabled'), 'hiddencaptcha')
]);
$user = User::query()->where('username', $request->username)->sole();
// Check if user is activated
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id'));
if ($user->active == 0 || $user->group_id == $validatingGroup[0]) {
$request->session()->invalidate();
throw ValidationException::withMessages([
Fortify::username() => trans('auth.not-activated'),
]);
}
// Check if user is banned
$bannedGroup = cache()->rememberForever('banned_group', fn () => Group::query()->where('slug', '=', 'banned')->pluck('id'));
if ($user->group_id == $bannedGroup[0]) {
$request->session()->invalidate();
throw ValidationException::withMessages([
Fortify::username() => trans('auth.banned'),
]);
}
return $user;
});
RateLimiter::for('login', function (Request $request) {
$email = (string) $request->email;
return Limit::perMinute(5)->by($email.$request->ip());
});
RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by($request->session()->get('login.id')));
}
}

View File

@@ -26,9 +26,9 @@
"hootlex/laravel-moderation": "^1.1",
"intervention/image": "^2.7.2",
"joypixels/assets": "^6.6",
"laravel/fortify": "^1.17",
"laravel/framework": "^10.11",
"laravel/tinker": "^2.8.1",
"laravel/ui": "^4.2.2",
"league/flysystem-sftp-v3": "^3.15",
"livewire/livewire": "^2.12.3",
"marcreichel/igdb-laravel": "3.7.0",

284
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cc01f23993de5cb950743a26db1036bb",
"content-hash": "e8f113daa066c3707475dfedac0529c9",
"packages": [
{
"name": "appstract/laravel-opcache",
@@ -127,6 +127,60 @@
},
"time": "2022-09-06T10:55:37+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phly/keep-a-changelog": "^2.1",
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
},
"time": "2022-12-07T17:46:57+00:00"
},
{
"name": "bjeavons/zxcvbn-php",
"version": "1.3.1",
@@ -237,6 +291,56 @@
],
"time": "2023-01-15T23:15:59+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.4",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
"reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.4"
},
"time": "2023-03-01T18:44:03+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
@@ -2049,6 +2153,70 @@
},
"time": "2021-07-08T22:11:29+00:00"
},
{
"name": "laravel/fortify",
"version": "v1.17.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
"reference": "e99f7cb135bb6e05e4c49e9224c9c9a33c27cfa0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/fortify/zipball/e99f7cb135bb6e05e4c49e9224c9c9a33c27cfa0",
"reference": "e99f7cb135bb6e05e4c49e9224c9c9a33c27cfa0",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0",
"ext-json": "*",
"illuminate/support": "^8.82|^9.0|^10.0",
"php": "^7.3|^8.0",
"pragmarx/google2fa": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^6.0|^7.0|^8.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Fortify\\FortifyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Fortify\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Backend controllers and scaffolding for Laravel authentication.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
"time": "2023-06-02T12:58:20+00:00"
},
{
"name": "laravel/framework",
"version": "v10.11.0",
@@ -2377,68 +2545,6 @@
},
"time": "2023-02-15T16:40:09+00:00"
},
{
"name": "laravel/ui",
"version": "v4.2.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/ui.git",
"reference": "a58ec468db4a340b33f3426c778784717a2c144b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/ui/zipball/a58ec468db4a340b33f3426c778784717a2c144b",
"reference": "a58ec468db4a340b33f3426c778784717a2c144b",
"shasum": ""
},
"require": {
"illuminate/console": "^9.21|^10.0",
"illuminate/filesystem": "^9.21|^10.0",
"illuminate/support": "^9.21|^10.0",
"illuminate/validation": "^9.21|^10.0",
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Ui\\UiServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Ui\\": "src/",
"Illuminate\\Foundation\\Auth\\": "auth-backend/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel UI utilities and presets.",
"keywords": [
"laravel",
"ui"
],
"support": {
"source": "https://github.com/laravel/ui/tree/v4.2.2"
},
"time": "2023-05-09T19:47:28+00:00"
},
{
"name": "league/commonmark",
"version": "2.4.0",
@@ -3899,6 +4005,58 @@
],
"time": "2023-03-05T17:13:09+00:00"
},
{
"name": "pragmarx/google2fa",
"version": "v8.0.1",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1.0|^2.0",
"php": "^7.1|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^7.5.15|^8.5|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
"keywords": [
"2fa",
"Authentication",
"Two Factor Authentication",
"google2fa"
],
"support": {
"issues": "https://github.com/antonioribeiro/google2fa/issues",
"source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1"
},
"time": "2022-06-13T21:57:56+00:00"
},
{
"name": "predis/predis",
"version": "v2.1.2",

View File

@@ -219,6 +219,7 @@ return [
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],

147
config/fortify.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
use App\Providers\RouteServiceProvider;
use Laravel\Fortify\Features;
return [
/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/
'passwords' => 'users',
/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/
'username' => 'username',
'email' => 'email',
/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/
'home' => RouteServiceProvider::HOME,
/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/
'prefix' => '',
'domain' => null,
/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],
/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/
'views' => true,
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),
],
];

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Fortify\Fortify;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')
->after('password')
->nullable();
$table->text('two_factor_recovery_codes')
->after('two_factor_secret')
->nullable();
if (Fortify::confirmsTwoFactorAuthentication()) {
$table->timestamp('two_factor_confirmed_at')
->after('two_factor_recovery_codes')
->nullable();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(array_merge([
'two_factor_secret',
'two_factor_recovery_codes',
], Fortify::confirmsTwoFactorAuthentication() ? [
'two_factor_confirmed_at',
] : []));
});
}
};

View File

@@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::drop('user_activations');
Schema::table('users', function (Blueprint $table): void {
$table->timestamp('email_verified_at')->nullable();
});
}
};

View File

@@ -0,0 +1,31 @@
<title>Password Confirmation - {{ config('other.title') }}</title>
<section class="panelV2">
<h2 class="panel__heading">Password Confirmation</h2>
<div class="panel__body">
<form
class="form"
action="{{ route('auth.confirm-password') }}"
method="POST"
>
@csrf
@method('PATCH')
<p>Please confirm your password before continuing.</p>
<p class="form__group">
<input
type="password"
class="form__text"
id="password"
name="password"
>
<label class="form__label" for="password">Password</label>
@error('error')
<span class="form__hint">{{ $error }}</span>
@enderror
</p>
<p class="form__group">
<button class="form__button form__button--filled">
{{ __('common.confirm') }}
</button>
</p>
</form>
</section>

View File

@@ -52,7 +52,7 @@
<a href="{{ route('login') }}">
<h2 class="active">{{ __('auth.login') }} </h2>
</a>
<a href="{{ route('registrationForm', ['code' => 'null']) }}">
<a href="{{ route('register') }}">
<h2 class="inactive underlineHover">{{ __('auth.signup') }} </h2>
</a>
@@ -99,9 +99,6 @@
<a href="{{ route('password.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-password') }} </h2>
</a>
<a href="{{ route('username.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-username') }} </h2>
</a>
</div>
</div>
</div>

View File

@@ -48,7 +48,7 @@
<a href="{{ route('login') }}">
<h2 class="inactive underlineHover">{{ __('auth.login') }}</h2>
</a>
<a href="{{ route('registrationForm', ['code' => 'null']) }}">
<a href="{{ route('register') }}">
<h2 class="inactive underlineHover">{{ __('auth.signup') }}</h2>
</a>
@@ -65,6 +65,12 @@
@hiddencaptcha
@endif
@if (session('status'))
<div class="form__hint">
{{ session('status') }}
</div>
@endif
<button type="submit" class="fadeIn fourth">{{ __('common.submit') }}</button>
</form>
@@ -72,9 +78,6 @@
<a href="{{ route('password.request') }}">
<h2 class="active">{{ __('auth.lost-password') }} </h2>
</a>
<a href="{{ route('username.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-username') }} </h2>
</a>
</div>
</div>
</div>

View File

@@ -48,7 +48,7 @@
<a href="{{ route('login') }}">
<h2 class="inactive underlineHover">{{ __('auth.login') }} </h2>
</a>
<a href="{{ route('registrationForm', ['code' => 'null']) }}">
<a href="{{ route('register') }}">
<h2 class="inactive underlineHover">{{ __('auth.signup') }} </h2>
</a>
@@ -58,7 +58,7 @@
<form class="form-horizontal" role="form" method="POST" action="{{ route('password.request') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<input type="hidden" name="token" value="{{ request()->route('token') }}">
<div class="row">
<div class="form-group">
<label for="email"></label><input type="email" id="email" class="fadeIn third" name="email"
@@ -87,9 +87,6 @@
<a href="{{ route('password.request') }}">
<h2 class="active">{{ __('auth.lost-password') }} </h2>
</a>
<a href="{{ route('username.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-username') }} </h2>
</a>
</div>
</div>
</div>

View File

@@ -31,7 +31,7 @@
</div>
@endif
<div class="wrapper fadeInDown">
@if (config('other.invite-only') == true && !$code)
@if (config('other.invite-only') == true && ! request()->has('code'))
<div class="alert alert-info">
{{ __('auth.need-invite') }}
</div>
@@ -53,7 +53,7 @@
<a href="{{ route('login') }}">
<h2 class="inactive underlineHover">{{ __('auth.login') }} </h2>
</a>
<a href="{{ route('registrationForm', ['code' => $code]) }}">
<a href="{{ route('register', ['code' => request()->query('code')]) }}">
<h2 class="active">{{ __('auth.signup') }} </h2>
</a>
@@ -61,7 +61,7 @@
<img src="{{ url('/img/icon.svg') }}" id="icon" alt="{{ __('auth.user-icon') }}"/>
</div>
<form role="form" method="POST" action="{{ route('register', ['code' => $code]) }}">
<form role="form" method="POST" action="{{ route('register', ['code' => request()->query('code')]) }}">
@csrf
<label for="username"></label><input type="text" id="username" class="fadeIn second" name="username"
placeholder="{{ __('auth.username') }}" required autofocus>
@@ -79,9 +79,6 @@
<a href="{{ route('password.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-password') }} </h2>
</a>
<a href="{{ route('username.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-username') }} </h2>
</a>
@if (config('email-white-blacklist.enabled') == 'block')
<br>
<a href="{{ route('public.email') }}">

View File

@@ -0,0 +1,102 @@
@extends('layout.default')
@section('title')
<title>{{ __('auth.title') }} - {{ config('other.title') }}</title>
@endsection
@section('meta')
<meta name="description" content="{{ __('auth.title') }} - {{ config('other.title') }}">
@endsection
@section('stylesheets')
<link rel="stylesheet" href="{{ mix('css/main/twostep.css') }}" crossorigin="anonymous">
@endsection
@section('breadcrumbs')
<li class="breadcrumb--active">
{{ __('auth.title') }}
</li>
@endsection
@php
switch ($remainingAttempts) {
case 0:
case 1:
$remainingAttemptsClass = 'danger';
break;
case 2:
$remainingAttemptsClass = 'warning';
break;
case 3:
$remainingAttemptsClass = 'info';
break;
default:
$remainingAttemptsClass = 'success';
break;
}
@endphp
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel verification-form-panel">
<div class="panel__heading text-center" id="verification_status_title">
<h3>
{{ __('auth.title') }}
</h3>
<p class="text-center">
<em>
{{ __('auth.subtitle') }}
</em>
</p>
</div>
<div class="panel__body">
<form
class="form"
action="{{ route('two-factor.login') }}"
method="POST"
>
@csrf
<p class="form__group">
<input
id="code"
class="form__text"
autofocus
name="code"
type="text"
/>
<label class="form__label form__label--floating">
{{ __('auth.code') }}
</label>
@error('error')
<span class="form__hint">{{ $error }}</span>
@enderror
</p>
<p class="form__group">
<input
id="code"
class="form__text"
autofocus
name="receover_code"
type="text"
/>
<label class="form__label form__label--floating">
{{ __('auth.recovery-code') }}
</label>
</p>
<p class="form__group">
<button class="form__button form__button--filled">
{{ __('common.submit') }}
</button>
</p>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -1,117 +0,0 @@
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="UTF-8">
<title>{{ __('auth.lost-username') }} - {{ config('other.title') }}</title>
@section('meta')
<meta name="description"
content="{{ __('auth.login-now-on') }} {{ config('other.title') }} . {{ __('auth.not-a-member') }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="{{ __('auth.login') }}">
<meta property="og:site_name" content="{{ config('other.title') }}">
<meta property="og:type" content="website">
<meta property="og:image" content="{{ url('/img/og.png') }}">
<meta property="og:description" content="{{ config('unit3d.powered-by') }}">
<meta property="og:url" content="{{ url('/') }}">
<meta property="og:locale" content="{{ config('app.locale') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
@show
<link rel="shortcut icon" href="{{ url('/favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url('/favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" href="{{ mix('css/main/login.css') }}" crossorigin="anonymous">
</head>
<body>
@if ($errors->any())
<div id="ERROR_COPY" style="display: none;">
@foreach ($errors->all() as $error)
{{ $error }}<br>
@endforeach
</div>
@endif
<div class="wrapper fadeInDown">
<svg viewBox="0 0 800 100" class="sitebanner">
<symbol id="s-text">
<text text-anchor="middle" x="50%" y="50%" dy=".35em">
{{ config('other.title') }}
</text>
</symbol>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
</svg>
<div id="formContent">
<a href="{{ route('login') }}">
<h2 class="inactive underlineHover">{{ __('auth.login') }}</h2>
</a>
<a href="{{ route('registrationForm', ['code' => 'null']) }}">
<h2 class="inactive underlineHover">{{ __('auth.signup') }}</h2>
</a>
<div class="fadeIn first">
<img src="{{ url('/img/icon.svg') }}" id="icon" alt="{{ __('auth.user-icon') }}"/>
</div>
<form class="form-horizontal" role="form" method="POST" action="{{ route('username.email') }}">
@csrf
<label for="email"></label><input type="email" id="email" class="fadeIn third" name="email"
placeholder="{{ __('auth.email') }}" required autofocus>
@if (config('captcha.enabled') == true)
@hiddencaptcha
@endif
<button type="submit" class="fadeIn fourth">{{ __('common.submit') }}</button>
</form>
<div id="formFooter">
<a href="{{ route('password.request') }}">
<h2 class="inactive underlineHover">{{ __('auth.lost-password') }} </h2>
</a>
<a href="{{ route('username.request') }}">
<h2 class="active">{{ __('auth.lost-username') }} </h2>
</a>
</div>
</div>
</div>
<script src="{{ mix('js/app.js') }}" crossorigin="anonymous"></script>
@foreach (['warning', 'success', 'info'] as $key)
@if (Session::has($key))
<script nonce="{{ HDVinnie\SecureHeaders\SecureHeaders::nonce('script') }}">
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000
})
Toast.fire({
icon: '{{ $key }}',
title: '{{ Session::get($key) }}'
})
</script>
@endif
@endforeach
@if (Session::has('errors'))
<script nonce="{{ HDVinnie\SecureHeaders\SecureHeaders::nonce('script') }}">
Swal.fire({
title: '<strong style=" color: rgb(17,17,17);">Error</strong>',
icon: 'error',
html: document.getElementById('ERROR_COPY').innerHTML,
showCloseButton: true,
})
</script>
@endif
</body>
</html>

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="UTF-8">
<title>{{ __('auth.verify-email') }} - {{ config('other.title') }}</title>
@section('meta')
<meta name="description" content="{{ __('auth.login-now-on') }} {{ config('other.title') }} . {{ __('auth.not-a-member') }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="{{ __('auth.login') }}">
<meta property="og:site_name" content="{{ config('other.title') }}">
<meta property="og:type" content="website">
<meta property="og:image" content="{{ url('/img/og.png') }}">
<meta property="og:description" content="{{ config('unit3d.powered-by') }}">
<meta property="og:url" content="{{ url('/') }}">
<meta property="og:locale" content="{{ config('app.locale') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
@show
<link rel="shortcut icon" href="{{ url('/favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url('/favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" href="{{ mix('css/main/login.css') }}" crossorigin="anonymous">
</head>
<body>
<div class="wrapper fadeInDown">
<svg viewBox="0 0 800 100" class="sitebanner">
<symbol id="s-text">
<text text-anchor="middle" x="50%" y="50%" dy=".35em">
{{ config('other.title') }}
</text>
</symbol>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
<use xlink:href="#s-text" class="text"></use>
</svg>
<div id="formContent">
<a href="{{ route('login') }}">
<h2 class="inactive underlineHover">{{ __('auth.login') }} </h2>
</a>
<a href="{{ route('register', ['code' => request()->query('code')]) }}">
<h2 class="active">{{ __('auth.signup') }} </h2>
</a>
<div class="fadeIn first">
<img src="{{ url('/img/icon.svg') }}" id="icon" alt="{{ __('auth.user-icon') }}"/>
</div>
<p>
Almost done...
<br>
We'll send you an email shortly. Open it up to activate your account.
</p>
<div id="formFooter">
<a href="{{ route('verification.send') }}">
<h2 class="active">Resend confirmation email</h2>
</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,7 +1,7 @@
@component('mail::message')
# {{ __('email.invite-header') }} {{ config('other.title') }} !
**{{ __('email.invite-message') }}:** {{ __('email.invite-invited') }} {{ config('other.title') }}. {{ $invite->custom }}
@component('mail::button', ['url' => route('register', $invite->code), 'color' => 'blue'])
@component('mail::button', ['url' => route('register', ['code' => $invite->code]), 'color' => 'blue'])
{{ __('email.invite-signup') }}
@endcomponent
<p>{{ __('email.register-footer') }}</p>

View File

@@ -25,8 +25,42 @@
@endsection
@section('main')
@if (session('status') == 'two-factor-authentication-enabled')
<section class="panelV2">
<h2 class="panel__heading">{{ __('common.configuration') }}</h2>
<div class="panel__body">
Please finish configuring two factor authentication:
{!! auth()->user()->twoFactorQrCodeSvg() !!}
<form
class="form"
action="{{ route('two-factor.confirm') }}"
method="POST"
>
<p class="form__group">
<input
type="text"
class="form__checkbox"
id="code"
name="code"
/>
</p>
<p class="form__group">
<button class="form__button form__button--filled">
{{ __('common.save') }}
</button>
</p>
</form>
Recovery codes:
<ul>
@foreach(auth()->user()->recoveryCodes() as $code)
<li>$code</li>
@endforeach
</ul>
</div>
</section>
@endif
<section class="panelV2">
<h2 class="panel__heading">Two Step Authentication</h2>
<h2 class="panel__heading">Email-based Two Step Authentication</h2>
<div class="panel__body">
<form
class="form"
@@ -35,7 +69,7 @@
>
@csrf
@method('PATCH')
<p>Currently, only email-based two step authentication is supported. Upon enabling, you will receive an email including a code to your registered email address.</p>
<p>Upon enabling, you will receive an email including a code to your registered email address.</p>
<p>Token-based two factor authentication is planned for a future update.</p>
<p class="form__group">
<input type="hidden" name="twostep" value="0">
@@ -57,4 +91,41 @@
</form>
</div>
</section>
<section class="panelV2">
<h2 class="panel__heading">Enable Two Factor Authentication</h2>
<div class="panel__body">
<form
class="form"
action="{{ route('two-factor.enable') }}"
method="POST"
>
@csrf
<p>Requires password confirmation</p>
<p>Upon enabling, you will be required to enter a valid two factor authentication code.</p>
<p class="form__group">
<button class="form__button form__button--filled">
{{ __('common.enable') }}
</button>
</p>
</form>
</div>
</section>
<section class="panelV2">
<h2 class="panel__heading">Disable Two Factor Authentication</h2>
<form
class="form"
action="{{ route('two-factor.disable') }}"
method="POST"
>
@csrf
@method('DELETE')
<p>Requires password confirmation.</p>
<p class="form__group">
<button class="form__button form__button--filled">
{{ __('common.disable') }}
</button>
</p>
</form>
</div>
</section>
@endsection

View File

@@ -38,30 +38,13 @@ Route::group(['middleware' => 'language'], function (): void {
|---------------------------------------------------------------------------------
*/
Route::group(['before' => 'auth', 'middleware' => 'guest'], function (): void {
// Activation
Route::get('/activate/{token}', [App\Http\Controllers\Auth\ActivationController::class, 'activate'])->name('activate');
// Application Signup
Route::get('/application', [App\Http\Controllers\Auth\ApplicationController::class, 'create'])->name('application.create');
Route::post('/application', [App\Http\Controllers\Auth\ApplicationController::class, 'store'])->name('application.store');
// Authentication
Route::get('login', [App\Http\Controllers\Auth\LoginController::class, 'showLoginForm'])->name('login');
Route::post('login', [App\Http\Controllers\Auth\LoginController::class, 'login'])->name('');
// Forgot Username
Route::get('username/reminder', [App\Http\Controllers\Auth\ForgotUsernameController::class, 'showForgotUsernameForm'])->name('username.request');
Route::post('username/reminder', [App\Http\Controllers\Auth\ForgotUsernameController::class, 'sendUsernameReminder'])->name('username.email');
// Password Reset
Route::post('password/email', [App\Http\Controllers\Auth\ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('password/reset', [App\Http\Controllers\Auth\ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('password/reset', [App\Http\Controllers\Auth\ResetPasswordController::class, 'reset'])->name('');
Route::get('/password/reset/{token}', [App\Http\Controllers\Auth\ResetPasswordController::class, 'showResetForm'])->name('password.reset');
// Registration
Route::get('/register/{code?}', [App\Http\Controllers\Auth\RegisterController::class, 'registrationForm'])->name('registrationForm');
Route::post('/register/{code?}', [App\Http\Controllers\Auth\RegisterController::class, 'register'])->name('register');
// This redirect must be kept until all invite emails that use the old syntax have expired
// Hack so that Fortify can be used (allows query parameters but not route parameters)
Route::get('/register/{code?}', fn (string $code) => to_route('register', ['code' => $code]));
});
/*
@@ -69,9 +52,8 @@ Route::group(['middleware' => 'language'], function (): void {
| Website (When Authorized) (Alpha Ordered)
|---------------------------------------------------------------------------------
*/
Route::group(['middleware' => ['auth', 'twostep', 'banned']], function (): void {
Route::group(['middleware' => ['auth', 'twostep', 'banned', 'verified']], function (): void {
// General
Route::post('/logout', [App\Http\Controllers\Auth\LoginController::class, 'logout'])->name('logout');
Route::get('/', [App\Http\Controllers\HomeController::class, 'index'])->name('home.index');
// Articles System

View File

@@ -1,38 +0,0 @@
<?php
namespace Tests\Feature\Http\Controllers\Auth;
use App\Models\User;
use Tests\TestCase;
/**
* @see \App\Http\Controllers\Auth\ForgotUsernameController
*/
class ForgotUsernameControllerTest extends TestCase
{
/**
* @test
*/
public function send_username_reminder_returns_an_ok_response(): void
{
config(['captcha.enabled' => false]);
$user = User::factory()->create();
$this->post(route('username.email'), [
'email' => $user->email,
])
->assertRedirect(route('login'))
->assertSessionHas('success', trans('email.username-sent'));
}
/**
* @test
*/
public function show_forgot_username_form_returns_an_ok_response(): void
{
$this->get(route('username.request'))
->assertOk()
->assertViewIs('auth.username');
}
}