first commit

This commit is contained in:
2026-10-09 08:21:06 +02:00
parent db7456df87
commit d259c62304
3761 changed files with 896046 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Events;
abstract class Event
{
//
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Permission;
class Created extends PermissionEvent {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Permission;
class Deleted extends PermissionEvent {}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\Permission;
use App\Models\Permission;
abstract class PermissionEvent
{
public function __construct(protected Permission $permission) {}
public function getPermission(): Permission
{
return $this->permission;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Permission;
class Updated extends PermissionEvent {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Role;
class Created extends RoleEvent {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Role;
class Deleted extends RoleEvent {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Role;
class PermissionsUpdated {}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\Role;
use App\Models\Role;
abstract class RoleEvent
{
public function __construct(protected Role $role) {}
public function getRole(): Role
{
return $this->role;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Role;
class Updated extends RoleEvent {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\Settings;
class Updated {}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class Approved
{
public function __construct(protected User $approvedUser) {}
public function getApprovedUser(): User
{
return $this->approvedUser;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class Banned
{
public function __construct(protected User $bannedUser) {}
public function getBannedUser(): User
{
return $this->bannedUser;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\User;
class ChangedAvatar {}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class Created
{
public function __construct(protected User $createdUser) {}
public function getCreatedUser(): User
{
return $this->createdUser;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class Deleted
{
public function __construct(protected User $deletedUser) {}
public function getDeletedUser(): User
{
return $this->deletedUser;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\User;
class LoggedIn {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\User;
class LoggedOut {}
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class RequestedPasswordResetEmail
{
public function __construct(protected User $user) {}
public function getUser(): User
{
return $this->user;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\User;
class TwoFactorDisabled {}
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class TwoFactorDisabledByAdmin
{
public function __construct(protected User $user) {}
public function getUser(): User
{
return $this->user;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace App\Events\User;
class TwoFactorEnabled {}
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class TwoFactorEnabledByAdmin
{
public function __construct(protected User $user) {}
public function getUser(): User
{
return $this->user;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Events\User;
use App\Models\User;
class UpdatedByAdmin
{
public function __construct(protected User $updatedUser) {}
public function getUpdatedUser(): User
{
return $this->updatedUser;
}
}
@@ -0,0 +1,5 @@
<?php
namespace App\Events\User;
class UpdatedProfileDetails {}
@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
abstract class ApiController extends Controller
{
protected int $statusCode = Response::HTTP_OK;
public function getStatusCode(): int
{
return $this->statusCode;
}
public function setStatusCode($statusCode): self
{
$this->statusCode = $statusCode;
return $this;
}
protected function respondWithSuccess($statusCode = Response::HTTP_OK): JsonResponse
{
return $this->setStatusCode($statusCode)
->respondWithArray(['success' => true]);
}
protected function respondWithArray(array $array, array $headers = []): JsonResponse
{
$response = \Response::json($array, $this->statusCode, $headers);
$response->header('Content-Type', 'application/json');
return $response;
}
protected function respondWithError($message): JsonResponse
{
if ($this->statusCode === Response::HTTP_OK) {
trigger_error(
'You better have a really good reason for erroring on a 200...',
E_USER_WARNING
);
}
return $this->respondWithArray([
'message' => $message,
]);
}
public function errorForbidden(string $message = 'Forbidden'): JsonResponse
{
return $this->setStatusCode(Response::HTTP_FORBIDDEN)
->respondWithError($message);
}
public function errorInternalError(string $message = 'Internal Error'): JsonResponse
{
return $this->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR)
->respondWithError($message);
}
public function errorNotFound(string $message = 'Resource Not Found'): JsonResponse
{
return $this->setStatusCode(Response::HTTP_NOT_FOUND)
->respondWithError($message);
}
public function errorUnauthorized(string $message = 'Unauthorized'): JsonResponse
{
return $this->setStatusCode(Response::HTTP_UNAUTHORIZED)
->respondWithError($message);
}
}
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Events\User\LoggedIn;
use App\Events\User\LoggedOut;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Auth\ApiLoginRequest;
use App\Models\User;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends ApiController
{
public function __construct()
{
$this->middleware('guest')->only('login');
$this->middleware('auth')->only('logout');
}
/**
* Attempt to log the user in and generate unique JWT token on successful authentication.
*
* @throws BindingResolutionException
* @throws ValidationException
*/
public function token(ApiLoginRequest $request): JsonResponse
{
$user = $this->findUser($request);
if ($user->isBanned()) {
return $this->errorUnauthorized(trans('auth.banned'));
}
if ($user->isWaitingApproval()) {
return $this->errorUnauthorized(trans('auth.approval'));
}
Auth::setUser($user);
event(new LoggedIn);
return $this->respondWithArray([
'token' => $user->createToken($request->device_name)->plainTextToken,
]);
}
/**
* Find the user instance from the API request.
*
* @throws BindingResolutionException
* @throws ValidationException
*/
private function findUser(ApiLoginRequest $request): ?User
{
$user = User::where($request->getCredentials())->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'username' => [trans('auth.failed')],
]);
}
return $user;
}
/**
* Logout user and invalidate token.
*/
public function logout(): JsonResponse
{
event(new LoggedOut);
auth()->user()->currentAccessToken()->delete();
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Api\Auth\Password;
use App\Events\User\RequestedPasswordResetEmail;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Auth\PasswordRemindRequest;
use App\Mail\ResetPassword;
use App\Repositories\User\UserRepository;
use Illuminate\Http\JsonResponse;
use Password;
class RemindController extends ApiController
{
/**
* Send a reset link to the given user.
*/
public function index(PasswordRemindRequest $request, UserRepository $users): JsonResponse
{
$user = $users->findByEmail($request->email);
$token = Password::getRepository()->create($user);
\Mail::to($user)->send(new ResetPassword($token));
event(new RequestedPasswordResetEmail($user));
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api\Auth\Password;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Auth\PasswordResetRequest;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\Auth\CanResetPassword;
use Illuminate\Http\JsonResponse;
use Password;
class ResetController extends ApiController
{
/**
* Reset the given user's password.
*/
public function index(PasswordResetRequest $request): JsonResponse
{
$response = Password::reset($request->credentials(), function ($user, $password) {
$this->resetPassword($user, $password);
});
return match ($response) {
Password::PASSWORD_RESET, Password::INVALID_USER => $this->respondWithSuccess(),
default => $this->setStatusCode(400)
->respondWithError(trans($response)),
};
}
/**
* Reset the given user's password.
*/
protected function resetPassword(CanResetPassword $user, string $password): void
{
$user->password = $password;
$user->save();
event(new PasswordReset($user));
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Auth\RegisterRequest;
use App\Models\Role;
use App\Repositories\Role\RoleRepository;
use App\Repositories\User\UserRepository;
use App\Support\Enum\UserStatus;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\JsonResponse;
class RegistrationController extends ApiController
{
public function __construct(private readonly UserRepository $users, private readonly RoleRepository $roles) {}
public function index(RegisterRequest $request): JsonResponse
{
$role = $this->roles->findByName(Role::DEFAULT_USER_ROLE);
$user = $this->users->create(
array_merge($request->validFormData(), ['role_id' => $role->id])
);
event(new Registered($user));
return $this->setStatusCode(201)
->respondWithArray([
'requires_email_confirmation' => (bool) setting('reg_email_confirmation'),
]);
}
/**
* Verify email via email confirmation token.
*/
public function verifyEmail($token): JsonResponse
{
if (! setting('reg_email_confirmation')) {
return $this->errorNotFound();
}
if ($user = $this->users->findByConfirmationToken($token)) {
$this->users->update($user->id, [
'status' => setting('approval.enabled') ? UserStatus::WAITING_APPROVAL : UserStatus::ACTIVE,
'confirmation_token' => null,
]);
return $this->respondWithSuccess();
}
return $this->setStatusCode(400)
->respondWithError('Invalid confirmation token.');
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Events\User\LoggedIn;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Auth\Social\ApiAuthenticateRequest;
use App\Repositories\User\UserRepository;
use App\Services\Auth\Social\SocialManager;
use Auth;
use Exception;
use Illuminate\Http\JsonResponse;
use Socialite;
class SocialLoginController extends ApiController
{
public function __construct(private readonly UserRepository $users, private readonly SocialManager $socialManager) {}
public function index(ApiAuthenticateRequest $request): JsonResponse
{
try {
$socialUser = Socialite::driver($request->network)->userFromToken($request->social_token);
} catch (Exception $e) {
return $this->errorInternalError('Could not connect to specified social network.');
}
$user = $this->users->findBySocialId(
$request->network,
$socialUser->getId()
);
if (! $user) {
if (! setting('reg_enabled')) {
return $this->errorForbidden('Only users who already created an account can log in.');
}
$user = $this->socialManager->associate($socialUser, $request->network);
}
if ($user->isBanned()) {
return $this->errorForbidden(__('Your account is banned by administrators.'));
}
if ($user->isWaitingApproval()) {
return $this->errorForbidden(__('Your account is waiting approval from administrators.'));
}
Auth::setUser($user);
event(new LoggedIn);
return $this->respondWithArray([
'token' => $user->createToken($request->device_name)->plainTextToken,
]);
}
}
@@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Auth\ApiVerifyEmailRequest;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Events\Verified;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
class VerificationController extends ApiController
{
public function __construct()
{
$this->middleware('throttle:6,1')->only('resend');
}
/**
* Mark the authenticated user's email address as verified.
*
* @throws AuthorizationException
*/
public function verify(ApiVerifyEmailRequest $request): JsonResponse
{
if (! setting('reg_email_confirmation')) {
return $this->errorNotFound();
}
$this->verifySignature($request);
if ($request->user()->hasVerifiedEmail()) {
return $this->emailAlreadyVerifiedResponse();
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return $this->respondWithSuccess();
}
/**
* Verify request signature.
*
* @throws AuthorizationException
*/
private function verifySignature(ApiVerifyEmailRequest $baseRequest): void
{
$request = Request::create(
route('verification.verify', $baseRequest->only('id', 'hash')),
Request::METHOD_GET,
$baseRequest->only('expires', 'signature')
);
if (! $request->hasValidSignature()) {
throw new InvalidSignatureException;
}
if (! hash_equals((string) $baseRequest->id, (string) auth()->user()->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $baseRequest->hash, sha1(auth()->user()->getEmailForVerification()))) {
throw new AuthorizationException;
}
}
protected function emailAlreadyVerifiedResponse(): JsonResponse
{
return $this->setStatusCode(Response::HTTP_BAD_REQUEST)
->respondWithError(__('E-Mail already verified.'));
}
/**
* Resend the email verification notification.
*/
public function resend(Request $request): JsonResponse
{
if ($request->user()->hasVerifiedEmail()) {
return $this->emailAlreadyVerifiedResponse();
}
$request->user()->sendEmailVerificationNotification();
return $this->respondWithSuccess(Response::HTTP_ACCEPTED);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Authorization;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Permission\CreatePermissionRequest;
use App\Http\Requests\Permission\RemovePermissionRequest;
use App\Http\Requests\Permission\UpdatePermissionRequest;
use App\Http\Resources\PermissionResource;
use App\Models\Permission;
use App\Repositories\Permission\PermissionRepository;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
class PermissionsController extends ApiController
{
public function __construct(private readonly PermissionRepository $permissions)
{
$this->middleware('permission:permissions.manage');
}
public function index(): AnonymousResourceCollection
{
$permissions = QueryBuilder::for(Permission::class)
->allowedFilters([
AllowedFilter::partial('name'),
AllowedFilter::partial('display_name'),
AllowedFilter::exact('role', 'role_id'),
])
->allowedSorts(['name', 'created_at'])
->defaultSort('created_at')
->paginate();
return PermissionResource::collection($permissions);
}
public function store(CreatePermissionRequest $request): PermissionResource
{
$permission = $this->permissions->create(
$request->only(['name', 'display_name', 'description'])
);
return new PermissionResource($permission);
}
public function show(Permission $permission): PermissionResource
{
return new PermissionResource($permission);
}
public function update(Permission $permission, UpdatePermissionRequest $request): PermissionResource
{
$input = collect($request->all());
$permission = $this->permissions->update(
$permission->id,
$input->only(['name', 'display_name', 'description'])->toArray()
);
return new PermissionResource($permission);
}
public function destroy(Permission $permission, RemovePermissionRequest $request): JsonResponse
{
$this->permissions->delete($permission->id);
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Api\Authorization;
use App\Events\Role\PermissionsUpdated;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Role\UpdateRolePermissionsRequest;
use App\Http\Resources\PermissionResource;
use App\Models\Role;
use App\Repositories\Role\RoleRepository;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class RolePermissionsController extends ApiController
{
public function __construct(private RoleRepository $roles)
{
$this->middleware('permission:permissions.manage');
}
public function show(Role $role): AnonymousResourceCollection
{
return PermissionResource::collection($role->cachedPermissions());
}
public function update(Role $role, UpdateRolePermissionsRequest $request): AnonymousResourceCollection
{
$this->roles->updatePermissions(
roleId: $role->id,
permissions: $request->permissions
);
event(new PermissionsUpdated);
return PermissionResource::collection($role->cachedPermissions());
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Api\Authorization;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\Role\CreateRoleRequest;
use App\Http\Requests\Role\RemoveRoleRequest;
use App\Http\Requests\Role\UpdateRoleRequest;
use App\Http\Resources\RoleResource;
use App\Models\Role;
use App\Repositories\Role\RoleRepository;
use App\Repositories\User\UserRepository;
use Cache;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Spatie\QueryBuilder\QueryBuilder;
class RolesController extends ApiController
{
public function __construct(private RoleRepository $roles)
{
$this->middleware('permission:roles.manage');
}
public function index(): AnonymousResourceCollection
{
$roles = QueryBuilder::for(Role::class)
->allowedIncludes(RoleResource::allowedIncludes())
->allowedFilters(['name'])
->allowedSorts(['name', 'created_at'])
->defaultSort('created_at')
->paginate();
return RoleResource::collection($roles);
}
public function store(CreateRoleRequest $request): RoleResource
{
$role = $this->roles->create(
$request->only(['name', 'display_name', 'description'])
);
return new RoleResource($role);
}
public function show($id): RoleResource
{
$role = QueryBuilder::for(Role::where('id', $id))
->allowedIncludes(RoleResource::allowedIncludes())
->first();
return new RoleResource($role);
}
public function update(Role $role, UpdateRoleRequest $request): RoleResource
{
$input = collect($request->all());
$role = $this->roles->update(
$role->id,
$input->only(['name', 'display_name', 'description'])->toArray()
);
return new RoleResource($role);
}
public function destroy(Role $role, UserRepository $users, RemoveRoleRequest $request): JsonResponse
{
$userRole = $this->roles->findByName(Role::DEFAULT_USER_ROLE);
$users->switchRolesForUsers($role->id, $userRole->id);
$this->roles->delete($role->id);
Cache::flush();
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Resources\CountryResource;
use App\Repositories\Country\CountryRepository;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class CountriesController extends ApiController
{
public function __construct(private readonly CountryRepository $countries) {}
public function index(): AnonymousResourceCollection
{
return CountryResource::collection($this->countries->all());
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Api\Profile;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\User\UpdateProfileLoginDetailsRequest;
use App\Http\Resources\UserResource;
use App\Repositories\User\UserRepository;
class AuthDetailsController extends ApiController
{
public function update(UpdateProfileLoginDetailsRequest $request, UserRepository $users): UserResource
{
$user = $request->user();
$data = $request->only(['email', 'username', 'password']);
$user = $users->update($user->id, $data);
return new UserResource($user);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Profile;
use App\Events\User\ChangedAvatar;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\User\UploadAvatarRawRequest;
use App\Http\Resources\UserResource;
use App\Repositories\User\UserRepository;
use App\Services\Upload\UserAvatarManager;
use Illuminate\Http\Request;
class AvatarController extends ApiController
{
public function __construct(
private readonly UserRepository $users,
private readonly UserAvatarManager $avatarManager
) {}
public function update(UploadAvatarRawRequest $request): UserResource
{
$name = $this->avatarManager->uploadAndCropAvatar(
$request->file('file')
);
$user = $this->users->update(
auth()->id(),
['avatar' => $name]
);
event(new ChangedAvatar);
return new UserResource($user);
}
public function updateExternal(Request $request): UserResource
{
$this->validate($request, [
'url' => 'required|url',
]);
$this->avatarManager->deleteAvatarIfUploaded(
auth()->user()
);
$user = $this->users->update(
auth()->id(),
['avatar' => $request->url]
);
event(new ChangedAvatar);
return new UserResource($user);
}
public function destroy(): UserResource
{
$user = auth()->user();
$this->avatarManager->deleteAvatarIfUploaded($user);
$user = $this->users->update(
$user->id,
['avatar' => null]
);
event(new ChangedAvatar);
return new UserResource($user);
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\Profile;
use App\Events\User\UpdatedProfileDetails;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\User\UpdateProfileDetailsRequest;
use App\Http\Resources\UserResource;
use App\Repositories\User\UserRepository;
class DetailsController extends ApiController
{
public function index(): UserResource
{
return new UserResource(auth()->user());
}
public function update(UpdateProfileDetailsRequest $request, UserRepository $users): UserResource
{
$user = $request->user();
$data = collect($request->all());
$data = $data->only([
'first_name', 'last_name', 'birthday',
'phone', 'address', 'country_id',
])->toArray();
if (! isset($data['country_id'])) {
$data['country_id'] = $user->country_id;
}
$user = $users->update($user->id, $data);
event(new UpdatedProfileDetails);
return new UserResource($user);
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Api\Profile;
use App\Http\Controllers\Api\ApiController;
use App\Http\Resources\SessionResource;
use App\Repositories\Session\SessionRepository;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class SessionsController extends ApiController
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('session.database');
}
public function index(SessionRepository $sessions): AnonymousResourceCollection
{
$sessions = $sessions->getUserSessions(auth()->id());
return SessionResource::collection($sessions);
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\Api\Profile;
use App\Events\User\TwoFactorDisabled;
use App\Events\User\TwoFactorEnabled;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\TwoFactor\VerifyTwoFactorTokenRequest;
use App\Http\Resources\UserResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
class TwoFactorController extends ApiController
{
public function update(EnableTwoFactorAuthentication $enable)
{
$user = auth()->user();
if ($user->twoFactorEnabled()) {
return $this->setStatusCode(422)
->respondWithError(trans('auth.2fa.already_enabled'));
}
$enable($user);
return $this->respondWithArray([
'message' => trans('auth.2fa.token_sent'),
'qrcode' => $user->twoFactorQrCodeSvg(),
]);
}
/**
* Verify provided 2FA token.
*/
public function verify(VerifyTwoFactorTokenRequest $request, ConfirmTwoFactorAuthentication $confirm): UserResource|JsonResponse
{
$user = auth()->user();
try {
$confirm($user, $request->input('code'));
} catch (ValidationException $e) {
return $this->setStatusCode(422)
->respondWithError(trans('auth.2fa.invalid_token'));
}
event(new TwoFactorEnabled);
return new UserResource($user);
}
/**
* Disable 2FA for currently authenticated user.
*/
public function destroy(): UserResource|JsonResponse
{
$user = auth()->user();
if (! $user->twoFactorEnabled()) {
return $this->setStatusCode(422)
->respondWithError(trans('auth.2fa.not_enabled'));
}
$user->forceFill([
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
])->save();
event(new TwoFactorDisabled);
return new UserResource($user);
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Resources\SessionResource;
use App\Repositories\Session\SessionRepository;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
class SessionsController extends ApiController
{
public function __construct(private readonly SessionRepository $sessions)
{
$this->middleware('session.database');
}
/**
* @throws AuthorizationException
*/
public function show($session): SessionResource
{
$this->authorize('manage-session', $session);
return new SessionResource($session);
}
/**
* @throws AuthorizationException
*/
public function destroy($session): JsonResponse
{
$this->authorize('manage-session', $session);
$this->sessions->invalidateSession($session->id);
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use Setting;
class SettingsController extends ApiController
{
public function __construct()
{
$this->middleware('permission:settings.general');
}
public function index(): JsonResponse
{
return response()->json(Setting::all());
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Resources\UserResource;
use App\Repositories\User\UserRepository;
use App\Support\Enum\UserStatus;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
class StatsController extends ApiController
{
public function __construct(private UserRepository $users)
{
$this->middleware('role:Admin');
}
public function index(): JsonResponse
{
$usersPerMonth = $this->users->countOfNewUsersPerMonthPerRole(
Carbon::now()->subYear()->startOfMonth(),
Carbon::now()->endOfMonth()
);
$usersPerStatus = [
'total' => $this->users->count(),
'new' => $this->users->newUsersCount(),
'banned' => $this->users->countByStatus(UserStatus::BANNED),
'unconfirmed' => $this->users->countByStatus(UserStatus::UNCONFIRMED),
];
$users = UserResource::collection($this->users->latest(7));
return $this->respondWithArray([
'users_per_month' => $usersPerMonth,
'users_per_status' => $usersPerStatus,
'latest_registrations' => $users->resolve(),
]);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Events\User\UpdatedByAdmin;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\User\UploadAvatarRawRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Repositories\User\UserRepository;
use App\Services\Upload\UserAvatarManager;
use Illuminate\Http\Request;
class AvatarController extends ApiController
{
public function __construct(
private readonly UserRepository $users,
private readonly UserAvatarManager $avatarManager
) {
$this->middleware('permission:users.manage');
}
public function update(User $user, UploadAvatarRawRequest $request): UserResource
{
$name = $this->avatarManager->uploadAndCropAvatar($request->file('file'));
$user = $this->users->update($user->id, ['avatar' => $name]);
event(new UpdatedByAdmin($user));
return new UserResource($user);
}
public function updateExternal(User $user, Request $request): UserResource
{
$this->validate($request, ['url' => 'required|url']);
$this->avatarManager->deleteAvatarIfUploaded($user);
$user = $this->users->update($user->id, ['avatar' => $request->url]);
event(new UpdatedByAdmin($user));
return new UserResource($user);
}
/**
* Remove user's avatar and set it to null.
*/
public function destroy(User $user): UserResource
{
$this->avatarManager->deleteAvatarIfUploaded($user);
$user = $this->users->update($user->id, ['avatar' => null]);
event(new UpdatedByAdmin($user));
return new UserResource($user);
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Http\Controllers\Api\ApiController;
use App\Http\Resources\SessionResource;
use App\Models\User;
use App\Repositories\Session\SessionRepository;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class SessionsController extends ApiController
{
public function __construct()
{
$this->middleware('permission:users.manage');
$this->middleware('session.database');
}
public function index(User $user, SessionRepository $sessions): AnonymousResourceCollection
{
return SessionResource::collection(
$sessions->getUserSessions($user->id)
);
}
}
@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Events\User\TwoFactorDisabledByAdmin;
use App\Events\User\TwoFactorEnabledByAdmin;
use App\Http\Controllers\Api\ApiController;
use App\Http\Requests\TwoFactor\VerifyTwoFactorTokenRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
class TwoFactorController extends ApiController
{
public function __construct()
{
$this->middleware('permission:users.manage');
}
/**
* Enable 2FA for the specified user.
*/
public function update(User $user, EnableTwoFactorAuthentication $enable): JsonResponse
{
if ($user->twoFactorEnabled()) {
return $this->setStatusCode(422)
->respondWithError(trans('auth.2fa.already_enabled'));
}
$enable($user, false);
return $this->respondWithArray([
'message' => trans('auth.2fa.token_sent'),
'qrcode' => $user->twoFactorQrCodeSvg(),
]);
}
/**
* Verify provided 2FA token.
*/
public function verify(VerifyTwoFactorTokenRequest $request, User $user, ConfirmTwoFactorAuthentication $confirm): UserResource|JsonResponse
{
try {
$confirm($user, $request->input('code'));
} catch (ValidationException $e) {
return $this->setStatusCode(422)
->respondWithError(trans('auth.2fa.invalid_token'));
}
event(new TwoFactorEnabledByAdmin($user));
return new UserResource($user);
}
/**
* Disable 2FA for specified user.
*/
public function destroy(User $user): UserResource|JsonResponse
{
if (! $user->twoFactorEnabled()) {
return $this->setStatusCode(422)
->respondWithError(trans('auth.2fa.not_enabled'));
}
$user->forceFill([
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
])->save();
event(new TwoFactorDisabledByAdmin($user));
return new UserResource($user);
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Events\User\Banned;
use App\Events\User\Deleted;
use App\Events\User\UpdatedByAdmin;
use App\Http\Controllers\Api\ApiController;
use App\Http\Filters\UserKeywordSearch;
use App\Http\Requests\User\CreateUserRequest;
use App\Http\Requests\User\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Repositories\User\UserRepository;
use App\Support\Enum\UserStatus;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
class UsersController extends ApiController
{
public function __construct(private UserRepository $users)
{
$this->middleware('permission:users.manage');
}
/**
* Paginate all users.
*/
public function index(Request $request): AnonymousResourceCollection
{
$users = QueryBuilder::for(User::class)
->allowedIncludes(UserResource::allowedIncludes())
->allowedFilters([
AllowedFilter::custom('search', new UserKeywordSearch),
AllowedFilter::exact('status'),
])
->allowedSorts(['id', 'first_name', 'last_name', 'email', 'created_at', 'updated_at'])
->defaultSort('id')
->paginate($request->per_page ?: 20);
return UserResource::collection($users);
}
public function store(CreateUserRequest $request): UserResource
{
$data = $request->only([
'email', 'password', 'username', 'first_name', 'last_name',
'phone', 'address', 'country_id', 'birthday', 'role_id',
]);
$data += [
'status' => setting('approval.enabled') ? UserStatus::WAITING_APPROVAL : UserStatus::ACTIVE,
'email_verified_at' => $request->verified ? now() : null,
];
$user = $this->users->create($data);
return new UserResource($user);
}
public function show($id): UserResource
{
$user = QueryBuilder::for(User::where('id', $id))
->allowedIncludes(UserResource::allowedIncludes())
->firstOrFail();
return new UserResource($user);
}
public function update(User $user, UpdateUserRequest $request): UserResource
{
$data = $request->only([
'email', 'password', 'username', 'first_name', 'last_name',
'phone', 'address', 'country_id', 'birthday', 'status', 'role_id',
]);
$user = $this->users->update($user->id, $data);
event(new UpdatedByAdmin($user));
// If user status was updated to "Banned",
// fire the appropriate event.
if ($this->userIsBanned($user, $request)) {
event(new Banned($user));
}
return new UserResource($user);
}
/**
* Check if user is banned during last update.
*/
private function userIsBanned(User $user, Request $request): bool
{
return $user->status != $request->status && $request->status == UserStatus::BANNED;
}
public function destroy(User $user): JsonResponse
{
if ($user->id == auth()->id()) {
return $this->errorForbidden(__('You cannot delete yourself.'));
}
event(new Deleted($user));
$this->users->delete($user->id);
return $this->respondWithSuccess();
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\PasswordBroker;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
/**
* Handles requesting a password reset link.
*
* This behaviour came from laravel/ui's SendsPasswordResetEmails trait, inlined
* here so the package is no longer a dependency.
*/
class ForgotPasswordController extends Controller
{
public function __construct()
{
$this->middleware('guest');
}
/**
* Display the form to request a password reset link.
*/
public function showLinkRequestForm(): InertiaResponse
{
return Inertia::render('auth/ForgotPassword');
}
/**
* Send a reset link to the given user.
*/
public function sendResetLinkEmail(Request $request): RedirectResponse|JsonResponse
{
$request->validate(['email' => 'required|email']);
$response = $this->broker()->sendResetLink($request->only('email'));
return $response === Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);
}
/**
* Get the response for a successful password reset link.
*/
protected function sendResetLinkResponse(Request $request, string $response): RedirectResponse
{
return back()->with('success', trans($response));
}
/**
* Get the response for a failed password reset link.
*
* An unknown address is reported as a success, so the form cannot be used
* to discover which addresses are registered. That message is flashed as a
* plain string because it is rendered as a toast, not against a field: an
* array there reaches the toast as an object and prints "[object Object]".
*
* @throws ValidationException
*/
protected function sendResetLinkFailedResponse(Request $request, string $response): RedirectResponse
{
if ($request->wantsJson()) {
throw ValidationException::withMessages(['email' => [trans($response)]]);
}
$httpResponse = back()->withInput($request->only('email'));
return $response === PasswordBroker::INVALID_USER
? $httpResponse->with('success', trans($response))
: $httpResponse->withErrors(['email' => trans($response)]);
}
/**
* Get the broker to be used during password reset.
*/
public function broker(): PasswordBroker
{
return Password::broker();
}
}
@@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Events\User\LoggedIn;
use App\Events\User\LoggedOut;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Repositories\Session\SessionRepository;
use App\Repositories\User\UserRepository;
use App\Services\Auth\ThrottlesLogins;
use App\Support\Authorization\PasswordChangeManager;
use Auth;
use Illuminate\Contracts\Auth\Authenticatable as BaseAuthenticatable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
class LoginController extends Controller
{
use ThrottlesLogins;
public function __construct(private readonly UserRepository $users)
{
$this->middleware('guest')->except('logout');
$this->middleware('auth')->only('logout');
}
/**
* Show the application login form.
*/
public function show(Request $request): InertiaResponse
{
return Inertia::render('auth/Login', [
'socialProviders' => config('auth.social.providers'),
'to' => $request->get('to'),
]);
}
public function login(
LoginRequest $request,
SessionRepository $sessions,
PasswordChangeManager $passwordChangeManager
): Response|RedirectResponse {
// In case that request throttling is enabled, we have to check if user can perform this request.
// We'll key this by the username and the IP address of the client making these requests into this application.
$throttles = (bool) setting('throttle_enabled');
// Redirect URL that can be passed as hidden field.
$to = $request->has('to') ? '?to='.$request->get('to') : '';
if ($throttles && $this->hasTooManyLoginAttempts($request)) {
return $this->sendLockoutResponse($request);
}
$credentials = $request->getCredentials();
if (! Auth::validate($credentials)) {
// If the login attempt was unsuccessful we will increment the number of attempts
// to log in and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
if ($throttles) {
$this->incrementLoginAttempts($request);
}
return redirect()->to('login'.$to)
->withErrors(trans('auth.failed'));
}
$user = Auth::getProvider()->retrieveByCredentials($credentials);
if ($user->isBanned()) {
return redirect()->to('login'.$to)
->withErrors(trans('auth.banned'));
}
if ($user->isWaitingApproval()) {
return redirect()->to('login'.$to)
->withErrors(trans('auth.approval'));
}
$maxSessions = setting('max_active_sessions');
if ($maxSessions && $sessions->getActiveSessionsCount($user->id) >= $maxSessions) {
return redirect()->to('login'.$to)
->withErrors(trans('auth.max_sessions_reached'));
}
Auth::login($user, setting('remember_me') && $request->get('remember'));
$passwordChangeManager->forcePasswordChange($user);
return $this->authenticated($request, $throttles, $user);
}
/**
* Send the response after the user was authenticated.
*/
protected function authenticated(
Request $request,
bool $throttles,
BaseAuthenticatable $user,
): Response|RedirectResponse {
if ($throttles) {
$this->clearLoginAttempts($request);
}
$redirectPage = $request->get('to');
if (setting('2fa.enabled') && $user->twoFactorEnabled()) {
return $this->logoutAndRedirectToTokenPage($request, $user, $redirectPage);
}
event(new LoggedIn);
if ($redirectPage) {
return redirect()->to($redirectPage);
}
return redirect()->intended();
}
protected function logoutAndRedirectToTokenPage(Request $request, $user, ?string $redirectPage): RedirectResponse
{
Auth::logout();
$request->session()->put('auth.2fa.id', $user->id);
if ($redirectPage) {
$request->session()->put('auth.redirect_to', $redirectPage);
}
return redirect()->route('auth.token');
}
/**
* Log the user out of the application.
*/
public function logout(): RedirectResponse
{
event(new LoggedOut);
Auth::logout();
return redirect('login');
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\RegisterRequest;
use App\Models\Role;
use App\Repositories\Role\RoleRepository;
use App\Repositories\User\UserRepository;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
class RegisterController extends Controller
{
public function __construct(private readonly UserRepository $users)
{
$this->middleware('registration')->only('show', 'register');
}
public function show(): InertiaResponse
{
return Inertia::render('auth/Register', [
'socialProviders' => config('auth.social.providers'),
'captchaSitekey' => setting('registration.captcha.enabled')
? config('captcha.sitekey')
: null,
]);
}
public function register(RegisterRequest $request, RoleRepository $roles): RedirectResponse
{
$user = $this->users->create(
array_merge(
$request->validFormData(),
['role_id' => $roles->findByName(Role::DEFAULT_USER_ROLE)->id],
)
);
event(new Registered($user));
$message = setting('reg_email_confirmation')
? __('Your account is created successfully! Please confirm your email.')
: __('Your account is created successfully!');
if (setting('approval.enabled')) {
return redirect('/login')->with('success', $message);
} else {
\Auth::login($user);
return redirect('/')->with('success', $message);
}
}
}
@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\Auth\CanResetPassword;
use Illuminate\Contracts\Auth\PasswordBroker;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
/**
* Handles resetting a password from an emailed token.
*
* This behaviour came from laravel/ui's ResetsPasswords trait, inlined here so
* the package is no longer a dependency.
*/
class ResetPasswordController extends Controller
{
/**
* Where to redirect users after resetting their password.
*/
protected string $redirectTo = '/';
public function __construct()
{
$this->middleware('guest');
}
/**
* Display the password reset form for the given token.
*/
public function showResetForm(Request $request, ?string $token = null): InertiaResponse
{
return Inertia::render('auth/ResetPassword', [
'token' => $token,
'email' => $request->email,
]);
}
/**
* Reset the given user's password.
*/
public function reset(Request $request): RedirectResponse|JsonResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$response = $this->broker()->reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
fn (CanResetPassword $user, string $password) => $this->resetPassword($user, $password)
);
return $response === Password::PASSWORD_RESET
? $this->sendResetResponse($request, $response)
: $this->sendResetFailedResponse($request, $response);
}
/**
* Reset the given user's password.
*
* The password is assigned unhashed because the User model hashes it.
*/
protected function resetPassword(CanResetPassword $user, string $password): void
{
$user->password = $password;
$user->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
$this->guard()->login($user);
}
/**
* Get the response for a successful password reset.
*/
protected function sendResetResponse(Request $request, string $response): RedirectResponse|JsonResponse
{
if ($request->wantsJson()) {
return new JsonResponse(['message' => trans($response)], 200);
}
return redirect($this->redirectTo)->with('status', trans($response));
}
/**
* Get the response for a failed password reset.
*
* @throws ValidationException
*/
protected function sendResetFailedResponse(Request $request, string $response): RedirectResponse
{
if ($request->wantsJson()) {
throw ValidationException::withMessages(['email' => [trans($response)]]);
}
return back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
/**
* Get the broker to be used during password reset.
*/
public function broker(): PasswordBroker
{
return Password::broker();
}
/**
* Get the guard to be used during password reset.
*/
protected function guard(): StatefulGuard
{
return Auth::guard();
}
}
@@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Events\User\LoggedIn;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Repositories\User\UserRepository;
use App\Services\Auth\Social\SocialManager;
use Auth;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Contracts\User as SocialUser;
use Socialite;
class SocialAuthController extends Controller
{
public function __construct(private readonly UserRepository $users, private readonly SocialManager $socialManager)
{
$this->middleware('guest');
}
/**
* Redirect user to specified provider in order to complete the authentication process.
*/
public function redirectToProvider(string $provider): RedirectResponse
{
session(['to' => request()->get('to')]);
if (strtolower($provider) == 'facebook') {
return Socialite::driver('facebook')->with(['auth_type' => 'rerequest'])->redirect();
}
return Socialite::driver($provider)->redirect();
}
/**
* Handle response authentication provider.
*/
public function handleProviderCallback(string $provider): RedirectResponse
{
if (request()->get('error')) {
return redirect('login')
->withErrors(__('Something went wrong during the authentication process. Please try again.'));
}
$socialUser = $this->getUserFromProvider($provider);
$user = $this->users->findBySocialId($provider, $socialUser->getId());
if (! $user) {
if (! setting('reg_enabled')) {
return redirect('login')
->withErrors(__('Only users who already created an account can log in.'));
}
if (! $socialUser->getEmail()) {
return redirect('login')
->withErrors(__('You have to provide your email address.'));
}
$user = $this->socialManager->associate($socialUser, $provider);
event(new Registered($user));
}
return $this->loginAndRedirect($user);
}
/**
* Get user from authentication provider.
*/
private function getUserFromProvider(string $provider): SocialUser
{
return Socialite::driver($provider)->user();
}
/**
* Log provided user in and redirect him to intended page.
*/
private function loginAndRedirect(User $user): RedirectResponse
{
$redirectPage = session('to');
$to = $redirectPage ? '?to='.$redirectPage : '';
if ($user->isBanned()) {
return redirect()->to('login'.$to)
->withErrors(__('Your account is banned by administrator.'));
}
if ($user->isWaitingApproval()) {
return redirect()->to('login'.$to)
->withErrors(__('Your account is waiting approval from administrators.'));
}
if (setting('2fa.enabled') && $user->twoFactorEnabled()) {
session()->put('auth.2fa.id', $user->id);
if ($redirectPage) {
session()->put('auth.redirect_to', $redirectPage);
}
return redirect()->route('auth.token');
}
Auth::login($user);
event(new LoggedIn);
if ($redirectPage) {
return redirect()->to($redirectPage);
}
return redirect()->intended('/');
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Events\User\LoggedIn;
use App\Http\Controllers\Controller;
use App\Http\Requests\TwoFactor\TwoFactorLoginRequest;
use App\Repositories\User\UserRepository;
use App\Services\Auth\ThrottlesLogins;
use Auth;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class TwoFactorTokenController extends Controller
{
use ThrottlesLogins;
public function __construct(private readonly UserRepository $users) {}
/**
* Show Two-Factor Token form.
*/
public function show(): InertiaResponse|RedirectResponse
{
return session('auth.2fa.id')
? Inertia::render('auth/TwoFactorToken')
: redirect('login');
}
/**
* Handle Two-Factor token form submission.
*/
public function update(TwoFactorLoginRequest $request): RedirectResponse
{
$this->validate($request, ['code' => 'required']);
if (! session('auth.2fa.id')) {
return redirect('login');
}
$user = $this->users->find(
$request->session()->pull('auth.2fa.id')
);
if (! $user) {
throw new NotFoundHttpException;
}
$customRedirect = $request->session()->pull('auth.redirect_to') ?: '';
if (! $request->hasValidCode($user)) {
return redirect()->to('login'.($customRedirect ? "?to={$customRedirect}" : ''))
->withErrors(trans('auth.2fa.invalid_token'));
}
Auth::login($user);
event(new LoggedIn);
return redirect()->intended($customRedirect ?: '/');
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Events\Verified;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
/**
* Handles email verification.
*
* This behaviour came from laravel/ui's VerifiesEmails trait, inlined here so
* the package is no longer a dependency.
*/
class VerificationController extends Controller
{
/**
* Where to redirect users after verification.
*/
protected string $redirectTo = '/';
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
/**
* Display the email verification notice.
*/
public function show(Request $request): InertiaResponse|RedirectResponse
{
return $request->user()->hasVerifiedEmail()
? redirect($this->redirectTo)
: Inertia::render('auth/VerifyEmail', [
'resent' => (bool) $request->session()->get('resent'),
]);
}
/**
* Mark the authenticated user's email address as verified.
*
* @throws AuthorizationException
*/
public function verify(Request $request): RedirectResponse|JsonResponse
{
if (! hash_equals((string) $request->route('id'), (string) $request->user()->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $request->route('hash'), sha1($request->user()->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($request->user()->hasVerifiedEmail()) {
return $request->wantsJson()
? new JsonResponse([], 204)
: redirect($this->redirectTo);
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return $request->wantsJson()
? new JsonResponse([], 204)
: redirect($this->redirectTo)->with('verified', true);
}
/**
* Resend the email verification notification.
*/
public function resend(Request $request): RedirectResponse|JsonResponse
{
if ($request->user()->hasVerifiedEmail()) {
return $request->wantsJson()
? new JsonResponse([], 204)
: redirect($this->redirectTo);
}
$request->user()->sendEmailVerificationNotification();
return $request->wantsJson()
? new JsonResponse([], 202)
: back()->with('resent', true);
}
}
@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Web\Authorization;
use App\Http\Controllers\Controller;
use App\Http\Requests\Permission\CreatePermissionRequest;
use App\Http\Requests\Permission\UpdatePermissionRequest;
use App\Models\Permission;
use App\Models\Role;
use App\Repositories\Permission\PermissionRepository;
use App\Repositories\Role\RoleRepository;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class PermissionsController extends Controller
{
public function __construct(
private readonly RoleRepository $roles,
private readonly PermissionRepository $permissions
) {}
public function index(): InertiaResponse
{
$roles = $this->roles->all();
$permissions = $this->permissions->all();
return Inertia::render('permissions/Index', [
'roles' => $roles->map(fn (Role $role) => [
'id' => $role->id,
'display_name' => $role->display_name,
])->values()->all(),
'permissions' => $permissions->map(fn (Permission $permission) => [
'id' => $permission->id,
'name' => $permission->name,
'display_name' => $permission->display_name,
'removable' => (bool) $permission->removable,
'editUrl' => route('permissions.edit', $permission),
'deleteUrl' => route('permissions.destroy', $permission),
])->values()->all(),
// Replaces the per-checkbox $role->hasPermission() calls the Blade
// matrix made, so the client can render the grid from plain data.
'granted' => $roles->mapWithKeys(fn (Role $role) => [
$role->id => $permissions
->filter(fn (Permission $permission) => $role->hasPermission($permission->name))
->pluck('id')
->values()
->all(),
])->all(),
]);
}
public function create(): InertiaResponse
{
return Inertia::render('permissions/Form', ['edit' => false, 'permission' => null]);
}
public function store(CreatePermissionRequest $request): RedirectResponse
{
$this->permissions->create($request->all());
return redirect()->route('permissions.index')
->withSuccess(__('Permission created successfully.'));
}
public function edit(Permission $permission): InertiaResponse
{
return Inertia::render('permissions/Form', [
'edit' => true,
'permission' => [
'id' => $permission->id,
'name' => $permission->name,
'display_name' => $permission->display_name,
'description' => $permission->description,
],
]);
}
public function update(Permission $permission, UpdatePermissionRequest $request): RedirectResponse
{
$this->permissions->update($permission->id, $request->all());
return redirect()->route('permissions.index')
->withSuccess(__('Permission updated successfully.'));
}
/**
* @throws NotFoundHttpException
*/
public function destroy(Permission $permission)
{
if (! $permission->removable) {
throw new NotFoundHttpException;
}
$this->permissions->delete($permission->id);
return redirect()->route('permissions.index')
->withSuccess(__('Permission deleted successfully.'));
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Web\Authorization;
use App\Events\Role\PermissionsUpdated;
use App\Http\Controllers\Controller;
use App\Repositories\Role\RoleRepository;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class RolePermissionsController extends Controller
{
public function __construct(private readonly RoleRepository $roles) {}
public function update(Request $request): RedirectResponse
{
$roles = $request->get('roles');
$allRoles = $this->roles->lists('id');
foreach ($allRoles as $roleId) {
$permissions = Arr::get($roles, $roleId, []);
$this->roles->updatePermissions($roleId, $permissions);
}
event(new PermissionsUpdated);
return redirect()->route('permissions.index')
->withSuccess(__('Permissions saved successfully.'));
}
}
@@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Web\Authorization;
use App\Http\Controllers\Controller;
use App\Http\Requests\Role\CreateRoleRequest;
use App\Http\Requests\Role\UpdateRoleRequest;
use App\Models\Role;
use App\Repositories\Role\RoleRepository;
use App\Repositories\User\UserRepository;
use Cache;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RolesController extends Controller
{
public function __construct(private readonly RoleRepository $roles) {}
public function index(): InertiaResponse
{
return Inertia::render('roles/Index', [
'roles' => $this->roles->getAllWithUsersCount()->map(fn (Role $role) => [
'id' => $role->id,
'name' => $role->name,
'display_name' => $role->display_name,
'users_count' => $role->users_count,
'removable' => (bool) $role->removable,
'editUrl' => route('roles.edit', $role),
'deleteUrl' => route('roles.destroy', $role),
])->values()->all(),
]);
}
public function create(): InertiaResponse
{
return Inertia::render('roles/Form', ['edit' => false, 'role' => null]);
}
public function store(CreateRoleRequest $request): RedirectResponse
{
$this->roles->create($request->all());
return redirect()->route('roles.index')
->withSuccess(__('Role created successfully.'));
}
public function edit(Role $role): InertiaResponse
{
return Inertia::render('roles/Form', [
'edit' => true,
'role' => [
'id' => $role->id,
'name' => $role->name,
'display_name' => $role->display_name,
'description' => $role->description,
],
]);
}
public function update(Role $role, UpdateRoleRequest $request): RedirectResponse
{
$this->roles->update($role->id, $request->all());
return redirect()->route('roles.index')
->withSuccess(__('Role updated successfully.'));
}
public function destroy(Role $role, UserRepository $userRepository): RedirectResponse
{
if (! $role->removable) {
throw new NotFoundHttpException;
}
$userRole = $this->roles->findByName(Role::DEFAULT_USER_ROLE);
$userRepository->switchRolesForUsers($role->id, $userRole->id);
$this->roles->delete($role->id);
Cache::flush();
return redirect()->route('roles.index')
->withSuccess(__('Role deleted successfully.'));
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Vanguard\Plugins\Vanguard;
use Vanguard\Plugins\Widget;
class DashboardController extends Controller
{
public function index(): InertiaResponse
{
if (session()->has('verified')) {
session()->flash('success', __('E-Mail verified successfully.'));
}
return Inertia::render('Dashboard', [
'widgets' => fn () => Vanguard::availableWidgets(auth()->user())
->map(fn (Widget $widget) => $widget->toArray())
->all(),
]);
}
}
@@ -0,0 +1,307 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Artisan;
use DB;
use Illuminate\Contracts\View\View;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Session;
use Symfony\Component\HttpFoundation\RedirectResponse;
class InstallController extends Controller
{
public const TMP_APP_KEY = 'lsXxrP1d6nUGpfGO6vQ1ezNy1KCuDD3o';
public function index(): View
{
return view('install.start');
}
public function requirements(): View
{
$requirements = $this->getRequirements();
$allLoaded = $this->allRequirementsLoaded();
return view('install.requirements', compact('requirements', 'allLoaded'));
}
public function permissions(): View|RedirectResponse
{
if (! $this->allRequirementsLoaded()) {
return redirect()->route('install.requirements');
}
$folders = $this->getPermissions();
$allGranted = $this->allPermissionsGranted();
return view('install.permissions', compact('folders', 'allGranted'));
}
public function databaseInfo(): View|RedirectResponse
{
if (! $this->allRequirementsLoaded()) {
return redirect()->route('install.requirements');
}
if (! $this->allPermissionsGranted()) {
return redirect()->route('install.permissions');
}
return view('install.database');
}
public function installation(Request $request): View|RedirectResponse
{
if (! $this->allRequirementsLoaded()) {
return redirect()->route('install.requirements');
}
if (! $this->allPermissionsGranted()) {
return redirect()->route('install.permissions');
}
$dbCredentials = $request->only('host', 'username', 'password', 'database', 'prefix');
if (! $this->dbCredentialsAreValid($dbCredentials)) {
return redirect()->route('install.database')
->withInput(Arr::except($dbCredentials, 'password'))
->withErrors('Connection to your database cannot be established.
Please provide correct database credentials.');
}
if (! $this->foreignKeysAreEnabled()) {
return redirect()->route('install.database')
->withInput(Arr::except($dbCredentials, 'password'))
->withErrors('Database connection established but foreign keys are not enabled.
Please enable foreign key support to proceed.');
}
Session::put('install.db_credentials', $dbCredentials);
return view('install.installation');
}
public function install(): RedirectResponse
{
try {
$db = Session::pull('install.db_credentials');
$appKey = $this->generateRandomKey();
$content = <<<PHPENV
APP_ENV=production
APP_DEBUG=false
APP_KEY={$appKey}
APP_URL=http://vanguard.test
# Public demo mode. Refuses the writes listed in config/demo.php and
# protects the seeded administrator account.
DEMO_MODE=false
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST="{$db['host']}"
DB_DATABASE="{$db['database']}"
DB_USERNAME="{$db['username']}"
DB_PASSWORD="{$db['password']}"
DB_PREFIX="{$db['prefix']}"
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
CACHE_STORE=file
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="vanguard@test.dev"
MAIL_FROM_NAME="\${APP_NAME}"
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="\${APP_NAME}"
PHPENV;
file_put_contents(base_path('.env'), $content);
$this->setDatabaseCredentials($db);
config(['app.debug' => true]);
\Setting::set('app_name', \request('app_name'));
\Setting::save();
Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]);
Artisan::call('db:seed', [
'--force' => true,
'--quiet' => true,
'--class' => '\\Vanguard\\Announcements\\Database\\Seeders\\AnnouncementsDatabaseSeeder',
]);
return redirect()->route('install.complete');
} catch (\Exception $e) {
@unlink(base_path('.env'));
\Log::error($e->getMessage());
\Log::error($e->getTraceAsString());
return redirect()->route('install.error');
}
}
public function complete(): View
{
return view('install.complete');
}
public function error(): View
{
return view('install.error');
}
private function getRequirements(): array
{
$requirements = [
'PHP Version (>= 8.3.0)' => version_compare(phpversion(), '8.3.0', '>='),
'BCMath Extension' => extension_loaded('bcmath'),
'OpenSSL Extension' => extension_loaded('openssl'),
'PDO Extension' => extension_loaded('PDO'),
'PDO MySQL Extension' => extension_loaded('pdo_mysql'),
'Mbstring Extension' => extension_loaded('mbstring'),
'Tokenizer Extension' => extension_loaded('tokenizer'),
'XML Extension' => extension_loaded('xml'),
'Ctype PHP Extension' => extension_loaded('ctype'),
'JSON PHP Extension' => extension_loaded('json'),
'GD Extension' => extension_loaded('gd'),
'Fileinfo Extension' => extension_loaded('fileinfo'),
];
if (extension_loaded('xdebug')) {
$requirements['Xdebug Max Nesting Level (>= 500)'] = (int) ini_get('xdebug.max_nesting_level') >= 500;
}
return $requirements;
}
private function allRequirementsLoaded(): bool
{
foreach ($this->getRequirements() as $loaded) {
if (! $loaded) {
return false;
}
}
return true;
}
private function getPermissions(): array
{
return [
'public/upload/users' => is_writable(public_path('upload/users')),
'storage/app' => is_writable(storage_path('app')),
'storage/framework/cache' => is_writable(storage_path('framework/cache')),
'storage/framework/sessions' => is_writable(storage_path('framework/sessions')),
'storage/framework/views' => is_writable(storage_path('framework/views')),
'storage/logs' => is_writable(storage_path('logs')),
'bootstrap/cache' => is_writable(base_path('bootstrap/cache')),
'Base Directory' => is_writable(base_path('')),
];
}
private function allPermissionsGranted(): bool
{
foreach ($this->getPermissions() as $permission => $granted) {
if (! $granted) {
return false;
}
}
return true;
}
private function dbCredentialsAreValid($credentials): bool
{
$this->setDatabaseCredentials($credentials);
try {
DB::statement('SHOW FULL TABLES');
} catch (\Exception $e) {
\Log::info($e->getMessage());
return false;
}
return true;
}
private function foreignKeysAreEnabled(): bool
{
try {
$result = DB::select('SELECT @@foreign_key_checks;');
return data_get($result, '0.@@foreign_key_checks') === 1;
} catch (\Exception $e) {
\Log::info($e->getMessage());
return false;
}
}
private function setDatabaseCredentials($credentials): void
{
$default = config('database.default');
config([
"database.connections.{$default}.host" => $credentials['host'],
"database.connections.{$default}.database" => $credentials['database'],
"database.connections.{$default}.username" => $credentials['username'],
"database.connections.{$default}.password" => $credentials['password'],
"database.connections.{$default}.prefix" => $credentials['prefix'],
]);
}
protected function generateRandomKey(): string
{
return 'base64:'.base64_encode(Encrypter::generateKey(config('app.cipher')));
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
/**
* Smistamento post-login verso le pagine legacy in base al ruolo.
*
* Sostituisce la rotta "/" (name: dashboard) di Vanguard.
* - Admin -> vede la dashboard nativa di Vanguard (delega al DashboardController originale)
* - Altri -> redirect verso la pagina legacy corrispondente al ruolo
*
* Questo file e' TUO: gli update di Vanguard non lo toccano.
*/
class PostLoginDispatchController extends Controller
{
public function index(Request $request, DashboardController $dashboard)
{
$user = $request->user();
// Admin: resta nella dashboard di gestione utenti di Vanguard.
if ($user->hasRole('Admin')) {
return $dashboard->index($request);
}
// Altri ruoli: redirect alla pagina legacy dedicata.
return $this->redirectByRole($user);
}
private function redirectByRole($user): RedirectResponse
{
if ($user->hasRole('Owner')) {
return redirect()->to('ownerdashboard.php');
}
if ($user->hasRole('User')) {
return redirect()->to('dashboard.php');
}
if ($user->hasRole('Superuser')) {
return redirect()->to('dashboard.php');
}
if ($user->hasRole('Certification')) {
return redirect()->to('cstd.php');
}
if ($user->hasRole('CustomerService')) {
return redirect()->to('cstrf.php');
}
// Fallback di sicurezza: se il ruolo non e' tra quelli previsti,
// manda comunque alla dashboard legacy generica.
return redirect()->to('dashboard.php');
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Web\Profile;
use App\Events\User\ChangedAvatar;
use App\Http\Controllers\Controller;
use App\Repositories\User\UserRepository;
use App\Services\Upload\UserAvatarManager;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class AvatarController extends Controller
{
public function __construct(private readonly UserRepository $users) {}
public function update(Request $request, UserAvatarManager $avatarManager): RedirectResponse
{
$request->validate(['avatar' => 'image']);
$name = $avatarManager->uploadAndCropAvatar(
$request->file('avatar'),
$request->get('points')
);
if ($name) {
return $this->handleAvatarUpdate($name);
}
return redirect()->route('profile')
->withErrors(__('Avatar image cannot be updated. Please try again.'));
}
/**
* Update avatar for currently logged-in user and fire appropriate event.
*/
private function handleAvatarUpdate(?string $avatar): RedirectResponse
{
$this->users->update(auth()->id(), ['avatar' => $avatar]);
event(new ChangedAvatar);
return redirect()->route('profile')
->withSuccess(__('Avatar changed successfully.'));
}
/**
* Update user's avatar from external location/url.
*/
public function updateExternal(Request $request, UserAvatarManager $avatarManager): RedirectResponse
{
$avatarManager->deleteAvatarIfUploaded(auth()->user());
return $this->handleAvatarUpdate($request->get('url'));
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Web\Profile;
use App\Events\User\UpdatedProfileDetails;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UpdateProfileDetailsRequest;
use App\Repositories\User\UserRepository;
use Illuminate\Http\RedirectResponse;
class DetailsController extends Controller
{
public function __construct(private readonly UserRepository $users) {}
public function update(UpdateProfileDetailsRequest $request): RedirectResponse
{
$this->users->update(auth()->id(), $request->except('role_id', 'status'));
event(new UpdatedProfileDetails);
return redirect()->back()
->withSuccess(__('Profile updated successfully.'));
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Web\Profile;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UpdateProfileLoginDetailsRequest;
use App\Repositories\User\UserRepository;
use App\Support\Authorization\PasswordChangeManager;
use Illuminate\Http\RedirectResponse;
class LoginDetailsController extends Controller
{
public function __construct(private readonly UserRepository $users) {}
public function update(UpdateProfileLoginDetailsRequest $request, PasswordChangeManager $passwordChangeManager): RedirectResponse
{
$data = $request->except('role', 'status');
// If password is not provided, then we will
// just remove it from $data array and do not change it
if (! data_get($data, 'password')) {
unset($data['password']);
unset($data['password_confirmation']);
} else {
$data['force_password_change'] = false;
$passwordChangeManager->liftPasswordChangeRequest(auth()->user());
}
$this->users->update(auth()->id(), $data);
return redirect()->route('profile')
->withSuccess(__('Login details updated successfully.'));
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Web\Profile;
use App\Http\Controllers\Controller;
use App\Repositories\Country\CountryRepository;
use App\Repositories\Role\RoleRepository;
use App\Repositories\User\UserRepository;
use App\Support\Authorization\PasswordChangeManager;
use App\Support\Enum\UserStatus;
use App\Support\Options;
use App\Support\UserPayload;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
class ProfileController extends Controller
{
public function __construct(
private readonly UserRepository $users,
private readonly RoleRepository $roles,
private readonly CountryRepository $countries
) {}
public function show(PasswordChangeManager $passwordChangeManager): InertiaResponse
{
$user = auth()->user();
// The role and status selects are disabled on the profile, so only the
// user's own role is offered rather than the full list.
$roles = $this->roles->all()
->filter(fn ($role) => $role->id == $user->role_id)
->pluck('display_name', 'id');
$forcePasswordChange = $passwordChangeManager->isPasswordChangeForcedForUser($user);
return Inertia::render('Profile', [
'user' => UserPayload::form($user),
'roles' => Options::fromList($roles),
'countries' => Options::fromList(
[0 => __('Select a Country')] + $this->countries->lists()->toArray()
),
'statuses' => Options::fromList(UserStatus::lists()),
'avatarSources' => UserPayload::avatarSources(
$user,
$this->users->getUserSocialLogins($user->id)
),
'twoFactor' => UserPayload::twoFactor($user),
'forcePasswordChange' => $forcePasswordChange,
'activeTab' => $forcePasswordChange ? 'login' : (session('tab') ?? 'details'),
]);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Web\Profile;
use App\Http\Controllers\Controller;
use App\Repositories\Session\SessionRepository;
use App\Support\SessionPayload;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
class SessionsController extends Controller
{
public function __construct(private readonly SessionRepository $sessions) {}
public function index(): InertiaResponse
{
$user = auth()->user();
return Inertia::render('users/Sessions', [
'adminView' => false,
'user' => [
'id' => $user->id,
'nameOrEmail' => $user->present()->nameOrEmail,
],
'sessions' => SessionPayload::collection(
$this->sessions->getUserSessions($user->id),
fn (string $sessionId) => route('profile.sessions.invalidate', $sessionId)
),
]);
}
public function destroy($session): RedirectResponse
{
$this->sessions->invalidateSession($session->id);
return redirect()->route('profile.sessions')
->withSuccess(__('Session invalidated successfully.'));
}
}
@@ -0,0 +1,160 @@
<?php
namespace App\Http\Controllers\Web;
use App\Events\Settings\Updated as SettingsUpdated;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Setting;
class SettingsController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Display general settings page.
*/
public function general(): InertiaResponse
{
return Inertia::render('settings/General', [
'values' => ['app_name' => setting('app_name')],
]);
}
/**
* Display Authentication & Registration settings page.
*/
public function auth(): InertiaResponse
{
return Inertia::render('settings/Auth', [
'values' => $this->settingValues([
'remember_me',
'forgot_password',
'login_reset_token_lifetime',
'max_active_sessions',
'throttle_enabled',
'throttle_attempts',
'throttle_lockout_time',
'reg_enabled',
'tos',
'reg_email_confirmation',
'2fa.enabled',
'approval.enabled',
'password-change.enabled',
'registration.captcha.enabled',
]),
'captchaConfigured' => (bool) (config('captcha.secret') && config('captcha.sitekey')),
]);
}
/**
* @param array<int, string> $keys
* @return array<string, mixed>
*/
private function settingValues(array $keys): array
{
$defaults = [
'login_reset_token_lifetime' => 30,
'max_active_sessions' => 0,
'throttle_attempts' => 10,
'throttle_lockout_time' => 1,
];
$values = [];
foreach ($keys as $key) {
$values[$key] = setting($key, $defaults[$key] ?? null);
}
return $values;
}
public function update(Request $request): RedirectResponse
{
$this->updateSetting($request->except('_token'));
return back()->withSuccess(__('Settings updated successfully.'));
}
private function updateSetting(array $input): void
{
foreach ($input as $key => $value) {
Setting::set($key, $value);
}
Setting::save();
event(new SettingsUpdated);
}
public function enableTwoFactor(): RedirectResponse
{
$this->updateSetting(['2fa.enabled' => true]);
return back()->withSuccess(__('Two-Factor Authentication enabled successfully.'));
}
public function disableTwoFactor(): RedirectResponse
{
$this->updateSetting(['2fa.enabled' => false]);
return back()->withSuccess(__('Two-Factor Authentication disabled successfully.'));
}
public function enableApproval(): RedirectResponse
{
$this->updateSetting(['approval.enabled' => true]);
return back()->withSuccess(__('User Confirmation Flow enabled successfully.'));
}
public function disableApproval(): RedirectResponse
{
$this->updateSetting(['approval.enabled' => false]);
return back()->withSuccess(__('User Confirmation Flow disabled successfully.'));
}
public function enablePasswordChange(): RedirectResponse
{
$this->updateSetting(['password-change.enabled' => true]);
return back()->withSuccess(__('Force Password Change for users enabled successfully.'));
}
public function disablePasswordChange(): RedirectResponse
{
$this->updateSetting(['password-change.enabled' => false]);
return back()->withSuccess(__('Force Password Change for users disabled successfully.'));
}
public function enableCaptcha(): RedirectResponse
{
$this->updateSetting(['registration.captcha.enabled' => true]);
return back()->withSuccess(__('reCAPTCHA enabled successfully.'));
}
public function disableCaptcha(): RedirectResponse
{
$this->updateSetting(['registration.captcha.enabled' => false]);
return back()->withSuccess(__('reCAPTCHA disabled successfully.'));
}
public function notifications(): InertiaResponse
{
return Inertia::render('settings/Notifications', [
'values' => [
'notifications_signup_email' => setting('notifications_signup_email'),
],
]);
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers\Web;
use App\Events\User\TwoFactorDisabled;
use App\Events\User\TwoFactorEnabled;
use App\Events\User\TwoFactorEnabledByAdmin;
use App\Http\Controllers\Controller;
use App\Http\Requests\TwoFactor\DisableTwoFactorRequest;
use App\Http\Requests\TwoFactor\EnableTwoFactorRequest;
use App\Http\Requests\TwoFactor\VerifyTwoFactorTokenRequest;
use App\Repositories\User\UserRepository;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
class TwoFactorController extends Controller
{
public function __construct(protected UserRepository $users, protected TwoFactorAuthenticationProvider $twoFactorProvider)
{
$this->middleware('auth');
$this->middleware(function ($request, $next) use ($users) {
$user = $request->get('user')
? $users->find($request->get('user'))
: auth()->user();
return $user->twoFactorEnabled() ? abort(404) : $next($request);
})->only('enable', 'verification', 'resend', 'verify');
}
/**
* Enable 2FA for currently logged user.
*/
public function enable(EnableTwoFactorRequest $request, EnableTwoFactorAuthentication $enable)
{
$user = $request->theUser();
$enable($user, $request->boolean('force', false));
session()->flash('tab', '2fa');
return redirect()->back()
->withSuccess(trans('auth.2fa.enabled_successfully'));
}
/**
* Verify 2FA token and enable 2FA if token is valid.
*/
public function verify(VerifyTwoFactorTokenRequest $request, ConfirmTwoFactorAuthentication $confirm): RedirectResponse
{
$user = $request->theUser();
try {
$confirm($user, $request->input('code'));
} catch (ValidationException $e) {
session()->flash('tab', '2fa');
return redirect()->back()
->withErrors(trans('auth.2fa.invalid_token'));
}
$message = trans('auth.2fa.enabled_successfully');
if ($user->is(auth()->user())) {
event(new TwoFactorEnabled);
return redirect()->route('profile')->withSuccess($message);
}
event(new TwoFactorEnabledByAdmin($user));
return redirect()->route('users.edit', $user)->withSuccess($message);
}
/**
* Disable 2FA for currently logged user.
*/
public function disable(DisableTwoFactorRequest $request): RedirectResponse
{
$user = $request->theUser();
if (! $user->twoFactorEnabled()) {
abort(404);
}
$user->forceFill([
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
])->save();
event(new TwoFactorDisabled);
return redirect()->back()
->withSuccess(trans('auth.2fa.disabled_successfully'));
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Web\Users;
use App\Events\User\UpdatedByAdmin;
use App\Http\Controllers\Api\ApiController;
use App\Models\User;
use App\Repositories\User\UserRepository;
use App\Services\Upload\UserAvatarManager;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class AvatarController extends ApiController
{
public function __construct(
private readonly UserRepository $users,
private readonly UserAvatarManager $avatarManager
) {}
/**
* @throws ValidationException
*/
public function update(User $user, Request $request): RedirectResponse
{
$this->validate($request, ['avatar' => 'image']);
$name = $this->avatarManager->uploadAndCropAvatar(
$request->file('avatar'),
$request->get('points')
);
if ($name) {
$this->users->update($user->id, ['avatar' => $name]);
event(new UpdatedByAdmin($user));
return redirect()->route('users.edit', $user)
->withSuccess(__('Avatar changed successfully.'));
}
return redirect()->route('users.edit', $user)
->withErrors(__('Avatar image cannot be updated. Please try again.'));
}
/**
* Update user's avatar from some external source (Gravatar, Facebook, Twitter...)
*/
public function updateExternal(User $user, Request $request): RedirectResponse
{
$this->avatarManager->deleteAvatarIfUploaded($user);
$this->users->update($user->id, ['avatar' => $request->get('url')]);
event(new UpdatedByAdmin($user));
return redirect()->route('users.edit', $user)
->withSuccess(__('Avatar changed successfully.'));
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Web\Users;
use App\Events\User\Approved;
use App\Events\User\Banned;
use App\Events\User\UpdatedByAdmin;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UpdateDetailsRequest;
use App\Models\User;
use App\Repositories\User\UserRepository;
use App\Support\Enum\UserStatus;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class DetailsController extends Controller
{
public function __construct(private readonly UserRepository $users) {}
public function update(User $user, UpdateDetailsRequest $request): RedirectResponse
{
$data = $request->all();
if (! data_get($data, 'country_id')) {
$data['country_id'] = null;
}
$this->users->update($user->id, $data);
$this->users->setRole($user->id, $request->role_id);
event(new UpdatedByAdmin($user));
// If user status was updated to "Banned",
// fire the appropriate event.
if ($this->userWasBanned($user, $request)) {
event(new Banned($user));
}
if ($this->userWasApproved($user, $request)) {
event(new Approved($user));
}
return redirect()->back()
->withSuccess(__('User updated successfully.'));
}
public function approve(User $user): RedirectResponse
{
$this->users->update($user->id, [
'status' => UserStatus::ACTIVE,
]);
event(new UpdatedByAdmin($user));
event(new Approved($user));
return redirect()->back()
->withSuccess(__('User updated successfully.'));
}
/**
* Check if user is banned during last update.
*/
private function userWasBanned(User $user, Request $request): bool
{
return $user->status != $request->status
&& $request->status == UserStatus::BANNED->value;
}
/**
* Check if user is approved during last update.
*/
private function userWasApproved(User $user, Request $request): bool
{
return $user->status == UserStatus::WAITING_APPROVAL->value
&& $request->status == UserStatus::ACTIVE->value;
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Web\Users;
use App\Events\User\UpdatedByAdmin;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UpdateLoginDetailsRequest;
use App\Models\User;
use App\Repositories\User\UserRepository;
use Illuminate\Http\RedirectResponse;
class LoginDetailsController extends Controller
{
public function __construct(private readonly UserRepository $users) {}
public function update(User $user, UpdateLoginDetailsRequest $request): RedirectResponse
{
$data = $request->all();
$data['force_password_change'] = $request->filled('force_password_change');
if (! $data['password']) {
unset($data['password']);
unset($data['password_confirmation']);
}
$this->users->update($user->id, $data);
event(new UpdatedByAdmin($user));
return redirect()->route('users.edit', $user->id)
->withSuccess(__('Login details updated successfully.'));
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Web\Users;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Repositories\Session\SessionRepository;
use App\Support\SessionPayload;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
class SessionsController extends Controller
{
public function __construct(private readonly SessionRepository $sessions)
{
$this->middleware('permission:users.manage');
}
public function index(User $user): InertiaResponse
{
return Inertia::render('users/Sessions', [
'adminView' => true,
'user' => [
'id' => $user->id,
'nameOrEmail' => $user->present()->nameOrEmail,
],
'sessions' => SessionPayload::collection(
$this->sessions->getUserSessions($user->id),
fn (string $sessionId) => route('user.sessions.invalidate', [$user->id, $sessionId])
),
]);
}
public function destroy(User $user, $session): RedirectResponse
{
$this->sessions->invalidateSession($session->id);
return redirect()->route('user.sessions', $user->id)
->withSuccess(__('Session invalidated successfully.'));
}
}
@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\Web\Users;
use App\Events\User\Deleted;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\CreateUserRequest;
use App\Models\Role;
use App\Models\User;
use App\Repositories\Country\CountryRepository;
use App\Repositories\Role\RoleRepository;
use App\Repositories\User\UserRepository;
use App\Support\Enum\UserStatus;
use App\Support\Options;
use App\Support\UserPayload;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Vanguard\Plugins\Vanguard;
class UsersController extends Controller
{
public function __construct(private readonly UserRepository $users) {}
public function index(Request $request): InertiaResponse
{
$users = $this->users->paginate(20, $request->search, $request->status);
return Inertia::render('users/Index', [
'users' => $users->through(fn (User $user) => UserPayload::row($user)),
'statuses' => Options::fromList(['' => __('All')] + UserStatus::lists()),
'filters' => [
'search' => (string) $request->search,
'status' => (string) $request->status,
],
'databaseSessions' => config('session.driver') === 'database',
]);
}
public function show(User $user): InertiaResponse
{
return Inertia::render('users/Show', [
'user' => UserPayload::details($user),
'canImpersonate' => UserPayload::canImpersonate($user),
// Replaces the View::composer that let the activity log plugin push
// a panel onto this page.
'slots' => Vanguard::resolveSlot('user:show', ['user' => $user]),
]);
}
public function create(CountryRepository $countryRepository, RoleRepository $roleRepository): InertiaResponse
{
return Inertia::render('users/Create', [
'countries' => $this->parseCountries($countryRepository),
'roles' => Options::fromList($roleRepository->lists()),
'statuses' => Options::fromList(UserStatus::lists()),
'defaultRoleId' => Role::where('name', Role::DEFAULT_USER_ROLE)->value('id'),
]);
}
/**
* Countries with a blank first entry, so a user may be left without one.
*
* @return array<int, array{value: string|int, label: string}>
*/
private function parseCountries(CountryRepository $countryRepository): array
{
return Options::fromList([0 => __('Select a Country')] + $countryRepository->lists()->toArray());
}
public function store(CreateUserRequest $request): RedirectResponse
{
// When user is created by administrator, we will set his
// status to Active by default.
$data = $request->all() + [
'status' => UserStatus::ACTIVE,
'email_verified_at' => now(),
];
$data['force_password_change'] = (bool) setting('password-change.enabled');
if (! data_get($data, 'country_id')) {
$data['country_id'] = null;
}
// Username should be updated only if it is provided.
if (! data_get($data, 'username')) {
$data['username'] = null;
}
$this->users->create($data);
return redirect()->route('users.index')
->withSuccess(__('User created successfully.'));
}
public function edit(User $user, CountryRepository $countryRepository, RoleRepository $roleRepository): InertiaResponse
{
return Inertia::render('users/Edit', [
'user' => UserPayload::form($user),
'countries' => $this->parseCountries($countryRepository),
'roles' => Options::fromList($roleRepository->lists()),
'statuses' => Options::fromList(UserStatus::lists()),
'avatarSources' => UserPayload::avatarSources(
$user,
$this->users->getUserSocialLogins($user->id)
),
'twoFactor' => UserPayload::twoFactor($user),
'showForcePasswordChange' => ! $user->is(auth()->user())
&& (bool) setting('password-change.enabled'),
// The two-factor controller flashes this to send the user back to
// the tab they were on.
'activeTab' => session('tab') === '2fa' ? '2fa' : 'details',
]);
}
public function destroy(User $user): RedirectResponse
{
if ($user->is(auth()->user())) {
return redirect()->route('users.index')
->withErrors(__('You cannot delete yourself.'));
}
$this->users->delete($user->id);
event(new Deleted($user));
return redirect()->route('users.index')
->withSuccess(__('User deleted successfully.'));
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Filters;
use Illuminate\Database\Eloquent\Builder;
use Spatie\QueryBuilder\Filters\Filter;
class UserKeywordSearch implements Filter
{
public function __invoke(Builder $query, $search, string $property = ''): void
{
$query->where(function ($q) use ($search) {
$q->where('username', 'like', "%{$search}%");
$q->orWhere('email', 'like', "%{$search}%");
$q->orWhere('first_name', 'like', "%{$search}%");
$q->orWhere('last_name', 'like', "%{$search}%");
});
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
class Authenticate
{
public function __construct(private readonly Guard $auth) {}
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
return $request->expectsJson()
? response('Unauthorized.', 401)
: redirect()->guest('login');
}
return $next($request);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckIfBanned
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->user() && $request->user()->isBanned()) {
abort(403, __('Your account is banned by administrator.'));
}
return $next($request);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
class CheckPermissions
{
public function __construct(protected Guard $auth) {}
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next, $permissions)
{
if (! is_array($permissions)) {
$permissions = explode('|', $permissions);
}
if ($this->auth->guest() || ! $request->user()->hasPermission($permissions)) {
abort(403, 'Forbidden.');
}
return $next($request);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
class CheckRole
{
public function __construct(protected Guard $auth) {}
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next, $role)
{
if ($this->auth->guest() || ! $request->user()->hasRole($role)) {
abort(403);
}
return $next($request);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class DatabaseSession
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
// If we are not using database session driver,
// just display 404 page
if (config('session.driver') != 'database') {
throw new NotFoundHttpException('The entity you are looking for does not exist.');
}
return $next($request);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Http\Middleware;
use App\Support\Demo;
use Closure;
use Illuminate\Http\Request;
class DemoMode
{
/**
* Refuse the writes config/demo.php names, while demo mode is on.
*
* Registered in the web and api groups, which run SubstituteBindings
* first, so the rules can read the route bound models. Inertia pushes its
* own global middleware to turn the 302 below into a 303 for PUT, PATCH
* and DELETE, which is what stops the browser repeating the verb.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
$reason = Demo::blocks($request);
if ($reason === null) {
return $next($request);
}
// The API answers with a status the client can branch on. The web UI
// gets a flash message, which HandleInertiaRequests turns into the red
// toast both layouts already watch for. An abort() there would render
// the stock error page, which an Inertia visit cannot parse.
return $request->expectsJson()
? abort(403, $reason)
: back()->with('error', $reason);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
class EnsureUserIsApproved
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->user() && $request->user()->isWaitingApproval()) {
return $request->expectsJson()
? abort(403, __('Your account is waiting approval from administrators.'))
: Redirect::guest(URL::route('approval.notice'));
}
return $next($request);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use App\Support\Authorization\PasswordChangeManager;
use Closure;
use Illuminate\Http\Request;
class ForcePasswordChange
{
public function __construct(private readonly PasswordChangeManager $forcePasswordChange) {}
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! $this->forcePasswordChange->isPasswordChangeForcedForUser($request->user())) {
return $next($request);
}
if (in_array($request->route()->getName(), ['profile', 'profile.update.login-details'])) {
return $next($request);
}
return $request->expectsJson() ?
abort(403, __('Before continuing please update your password and set a new one.')) :
redirect(route('profile'));
}
}
@@ -0,0 +1,142 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use App\Support\Locale;
use App\Support\Sidebar\Item;
use App\Support\Translations;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Vanguard\Plugins\Plugin;
use Vanguard\Plugins\Vanguard;
class HandleInertiaRequests extends Middleware
{
/**
* The root template loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Application settings that the whole UI needs.
*
* Page specific settings (throttling, notification toggles, ...) are passed
* as page props by the controller that owns them rather than shared here.
*/
private const SHARED_SETTINGS = [
'app_name',
'reg_enabled',
'forgot_password',
'remember_me',
'tos',
'registration.captcha.enabled',
'2fa.enabled',
'password-change.enabled',
];
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
/** @var User|null $user */
$user = $request->user();
return array_merge(parent::share($request), [
'auth' => [
'user' => $user ? $this->serializeUser($user) : null,
'permissions' => fn () => $user
? $user->role->cachedPermissions()->pluck('name')->all()
: [],
'impersonating' => app('impersonate')->isImpersonating(),
],
'settings' => $this->settings(),
'config' => [
// Drives the "Active Sessions" UI, which only works on the
// database session driver.
'databaseSessions' => config('session.driver') === 'database',
'dateFormat' => config('app.date_format'),
'dateTimeFormat' => config('app.date_time_format'),
],
'flash' => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
],
'locale' => app()->getLocale(),
'locales' => collect(Locale::AVAILABLE_LOCALES)
->map(fn (string $locale) => [
'code' => $locale,
'flag' => Locale::flagUrl($locale),
])->all(),
'translations' => fn () => Translations::catalog(),
'vanguard' => [
'sidebar' => fn () => $user ? $this->sidebar($user) : [],
'slots' => fn () => [
'navbar:items' => Vanguard::resolveSlot('navbar:items'),
'navbar:dropdown' => Vanguard::resolveSlot('navbar:dropdown'),
],
],
]);
}
/**
* The authenticated user, including the presenter derived fields that the
* Blade layer used to read through $user->present().
*
* @return array<string, mixed>
*/
private function serializeUser(User $user): array
{
return [
'id' => $user->id,
'email' => $user->email,
'username' => $user->username,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'status' => $user->status,
'name' => $user->present()->name,
'nameOrEmail' => $user->present()->nameOrEmail,
'avatar' => $user->present()->avatar,
'lastLogin' => $user->present()->lastLogin,
];
}
/**
* @return array<string, mixed>
*/
private function settings(): array
{
$settings = [];
foreach (self::SHARED_SETTINGS as $key) {
$settings[$key] = setting($key);
}
return $settings;
}
/**
* The authorised sidebar tree contributed by every registered plugin.
*
* @return array<int, mixed>
*/
private function sidebar(User $user): array
{
return collect(Vanguard::availablePlugins())
->map(fn (Plugin $plugin) => $plugin->sidebar())
->filter(fn (?Item $item) => $item !== null && $item->authorize($user))
->map(fn (Item $item) => $item->toArray($user))
->values()
->all();
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class PasswordResetEnabled
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! setting('forgot_password')) {
throw new NotFoundHttpException;
}
return $next($request);
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response|RedirectResponse) $next
* @param string|null ...$guards
* @return Response|RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return request()->has('to')
? redirect(request()->get('to'))
: redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RegistrationEnabled
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! setting('reg_enabled')) {
throw new NotFoundHttpException;
}
return $next($request);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Middleware;
use App\Support\Locale;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class SetLocale
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): mixed
{
$locale = $this->getLocale($request);
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getLocale(Request $request): string
{
$newLocale = $request->get('lang');
if (! $newLocale || ! Locale::validateLocale($newLocale)) {
return $this->getSelectedLocale();
}
session()->put('locale', $newLocale);
return $newLocale;
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getSelectedLocale(): string
{
if (session()->has('locale')) {
return session()->get('locale');
}
$locale = \Request::getPreferredLanguage(Locale::AVAILABLE_LOCALES) ?? config('app.locale');
session()->put('locale', $locale);
return $locale;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SocialLogin
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
$provider = $request->route()->parameter('provider');
if (! in_array($provider, config('auth.social.providers'))) {
throw new NotFoundHttpException;
}
return $next($request);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class TwoFactorEnabled
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! setting('2fa.enabled')) {
throw new NotFoundHttpException;
}
return $next($request);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Http\Request;
class UseApiGuard
{
public function __construct(protected Factory $auth) {}
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
$this->auth->shouldUse('sanctum');
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Middleware;
use App\Http\Controllers\Web\InstallController;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class VerifyInstallation
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*
* @throws HttpException
*/
public function handle($request, Closure $next)
{
if (app()->environment('testing')) {
return $next($request);
}
if (! file_exists(base_path('.env')) && ! $request->is('install*')) {
return redirect()->to('install');
}
if (file_exists(base_path('.env')) && $request->is('install*') && ! $request->is('install/complete')) {
throw new NotFoundHttpException;
}
if ($request->is('install*') && ! config()->get('app.key')) {
config()->set('app.key', InstallController::TMP_APP_KEY);
}
return $next($request);
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Middleware;
use App\Repositories\User\UserRepository;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
class VerifyTwoFactorCode
{
/**
* Handle an incoming request.
*
* @param Request $request
* @return mixed
*
* @throws HttpException
*/
public function handle($request, Closure $next)
{
$user = $this->getUser($request);
if ($user->two_factor_secret) {
return $next($request);
}
abort(404);
}
/**
* @return mixed
*/
private function getUser($request)
{
if ($userId = $request->get('user')) {
return app(UserRepository::class)->find($userId);
}
return $request->user();
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Auth;
class ApiLoginRequest extends LoginRequest
{
public function rules(): array
{
return array_merge(parent::rules(), [
'device_name' => 'required',
]);
}
public function getCredentials(): array
{
$credentials = parent::getCredentials();
unset($credentials['password']);
return $credentials;
}
}

Some files were not shown because too many files have changed in this diff Show More