EscolaLMS/Auth

View on GitHub
src/Services/UserService.php

Summary

Maintainability
B
6 hrs
Test Coverage
B
88%
<?php

namespace EscolaLms\Auth\Services;

use EscolaLms\Auth\Dtos\Admin\UserAssignableDto;
use EscolaLms\Auth\Dtos\Admin\UserUpdateDto as AdminUserUpdateDto;
use EscolaLms\Auth\Dtos\Admin\UserUpdateKeysDto as AdminUserUpdateKeysDto;
use EscolaLms\Auth\Dtos\UserSaveDto;
use EscolaLms\Auth\Dtos\UserUpdateDto;
use EscolaLms\Auth\Dtos\UserUpdateKeysDto;
use EscolaLms\Auth\Dtos\UserUpdateSettingsDto;
use EscolaLms\Auth\Events\AccountConfirmed;
use EscolaLms\Auth\Events\AccountDeletionRequested;
use EscolaLms\Auth\Events\Impersonate;
use EscolaLms\Auth\Events\Login;
use EscolaLms\Auth\Exceptions\DeletionTokenExpiredException;
use EscolaLms\Auth\Models\User as AuthUser;
use EscolaLms\Auth\Repositories\Contracts\UserRepositoryContract;
use EscolaLms\Auth\Services\Contracts\UserServiceContract;
use EscolaLms\Core\Dtos\CriteriaDto;
use EscolaLms\Core\Dtos\OrderDto;
use EscolaLms\Core\Dtos\PaginationDto;
use EscolaLms\Files\Helpers\FileHelper;
use EscolaLms\ModelFields\Facades\ModelFields;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable as User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class UserService implements UserServiceContract
{
    private UserRepositoryContract $userRepository;

    /**
     * UserService constructor.
     * @param UserRepositoryContract $userRepository
     */
    public function __construct(UserRepositoryContract $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function create(UserSaveDto $userSaveDto): User
    {
        $attributes['remember_token'] = Str::random(10);
        /** @var \EscolaLms\Auth\Models\User $user */
        $user = $this->userRepository->create($userSaveDto->getUserAttributes());
        if ($user instanceof MustVerifyEmail && $userSaveDto->getVerified()) {
            $user->markEmailAsVerified();
            event(new AccountConfirmed($user));
        }
        assert($user instanceof User);
        $this->syncRoles($user, $userSaveDto->getRoles());
        return $user;
    }

    public function createWithSettings(UserSaveDto $userSaveDto, UserUpdateSettingsDto $userSettingsDto): User
    {
        $user = $this->create($userSaveDto);
        $this->userRepository->putSettingsUsingDto($user, $userSettingsDto);
        return $user;
    }

    public function update(User $user, UserSaveDto $userSaveDto): User
    {
        $this->userRepository->update($userSaveDto->getUserAttributes(), $user->id);
        $this->syncRoles($user, $userSaveDto->getRoles());
        return $user;
    }

    public function putUsingDto(UserUpdateDto $dto, int $id): User
    {
        $data = $dto->toArray();

        $user = $this->userRepository->update($data, $id);

        assert($user instanceof AuthUser);
        if ($dto instanceof AdminUserUpdateDto) {
            if (!is_null($dto->getEmailVerified())) {
                if ($dto->getEmailVerified() && !$user->hasVerifiedEmail()) {
                    $user->markEmailAsVerified();
                    event(new AccountConfirmed($user));
                }
                if (!$dto->getEmailVerified()) {
                    $user->email_verified_at = null;
                    $user->save();
                }
            }
            if ($dto->getRoles() !== null) {
                $this->syncRoles($user, $dto->getRoles());
            }
        }

        return $user;
    }

    public function patchUsingDto(UserUpdateDto $dto, UserUpdateKeysDto $keysDto, int $id): User
    {
        $data = $dto->toArray();

        $user = $this->userRepository->update($data, $id);

        assert($user instanceof AuthUser);
        if ($dto instanceof AdminUserUpdateDto && $keysDto instanceof AdminUserUpdateKeysDto) {
            if (!is_null($dto->getEmailVerified())) {
                if ($dto->getEmailVerified() && !$user->hasVerifiedEmail()) {
                    $user->markEmailAsVerified();
                    event(new AccountConfirmed($user));
                }
                if (!$dto->getEmailVerified()) {
                    $user->email_verified_at = null;
                    $user->save();
                }
            }
            if ($dto->getRoles() !== null && $keysDto->getRoles()) {
                $this->syncRoles($user, $dto->getRoles());
            }
        }
        return $user;
    }

    public function login(string $email, string $password): User
    {

        $user = $this->userRepository->findByEmail($email);

        if (is_null($user) || !Hash::check($password, $user->password)) {
            throw new Exception('Invalid credentials');
        }

        assert($user instanceof AuthUser);

        if (!$user->hasVerifiedEmail() && $this->checkIfSuperadmin($user->getEmailForVerification())) {
            $user->markEmailAsVerified();
            event(new AccountConfirmed($user));
            $user->refresh();
        }

        if (!$user->hasVerifiedEmail()) {
            throw new Exception('Email not validated');
        }

        if (!$user->is_active) {
            throw new Exception("User account has been deactivated");
        }

        event(new Login($user));

        return $user;
    }

    public function impersonate(int $id): User
    {
        $user = $this->userRepository->find($id);

        assert($user instanceof AuthUser);

        event(new Impersonate($user));

        return $user;
    }

    private function checkIfSuperadmin(string $email): bool
    {
        $superadmins = array_filter(config('escola_auth.superadmins', []), fn ($item) => !empty($item));
        return in_array($email, $superadmins);
    }

    public function deleteAvatar(User $user): bool
    {
        assert($user instanceof AuthUser);
        if (!empty($user->path_avatar)) {
            $result = Storage::delete($user->path_avatar);
            $user->update(['path_avatar' => null]);
            return $result;
        }
        return false;
    }

    public function uploadAvatar(User $user, $avatar): ?User
    {
        assert($user instanceof AuthUser);
        $user->path_avatar = FileHelper::getFilePath($avatar, 'avatars/' . $user->id);
        $user->save();
        return $user;
    }

    private function syncRoles(User $user, array $roles): void
    {
        assert($user instanceof AuthUser);
        $user->syncRoles($roles);
    }

    public function search(CriteriaDto $criteriaDto, PaginationDto $paginationDto): Collection
    {
        return $this->userRepository->searchByCriteria($criteriaDto->toArray(), $paginationDto->getSkip(), $paginationDto->getLimit());
    }

    public function searchAndPaginate(
        CriteriaDto $criteriaDto,
        ?array $columns = [],
        ?array $with = [],
        ?array $appends = [],
        ?int $perPage = null,
        ?int $page = null,
        ?OrderDto $orderDto = null
    ): LengthAwarePaginator
    {
        $columns = $this->makeColumns($columns);
        $with = $this->makeRelations($with);

        $query = $this->userRepository
            ->queryWithAppliedCriteria($criteriaDto->toArray())
            ->with($with);

        if (!is_null($orderDto) && !is_null($orderDto->getOrderBy())) {
            $query->orderBy($orderDto->getOrderBy(), $orderDto->getOrder() ?? 'asc');
        }

        return $query
            ->paginate($perPage, $columns, 'page', $page)
            ->appends($appends);
    }

    public function updateAdditionalFieldsFromRequest(User $user, FormRequest $request): void
    {
        $keys = ModelFields::getFieldsMetadata(AuthUser::class)->pluck('name');
        $fields = $request->collect()->only($keys)->toArray();
        $this->userRepository->update($fields, $user->getKey());
    }

    public function anonymiseEmail(AuthUser $user): void
    {
        $this->userRepository->update([
            'email' => $user->email . '+deleted+' . now()->timestamp,
        ], $user->getKey());
    }

    /**
     * @deprecated
     */
    public function assignableUsers(string $assignableBy, ?int $perPage = null, ?int $page = null): LengthAwarePaginator
    {
        $dto = UserAssignableDto::instantiateFromArray(['assignable_by' => $assignableBy]);
        return $this->searchAndPaginate($dto, [], [], [], $perPage, $page);
    }

    public function assignableUsersWithCriteria(CriteriaDto $dto, ?int $perPage = null, ?int $page = null): LengthAwarePaginator
    {
        return $this->searchAndPaginate($dto, [], [], [], $perPage, $page);
    }

    public function initProfileDeletion(User $user, string $returnUrl): void
    {
        $token = Crypt::encrypt(Carbon::now()->addHour());
        $user = $this->userRepository->update(['delete_user_token' => $token], $user->getKey());

        event(new AccountDeletionRequested($user, $returnUrl));
    }

    /**
     * @throws DeletionTokenExpiredException
     */
    public function confirmDeletionProfile(int $userId, string $token): void
    {
        $user = $this->userRepository->find($userId);
        $expiredAt = Crypt::decrypt($token);

        if (!$user || !$user->delete_user_token || $user->delete_user_token !== $token) {
            throw new DeletionTokenExpiredException("Deletion token is invalid");
        }

        if (!$expiredAt instanceof Carbon || $expiredAt <= Carbon::now()) {
            throw new DeletionTokenExpiredException();
        }

        $user->delete();
    }

    private function makeColumns(?array $columns): array
    {
        if (!$columns) {
            return ['*'];
        }

        $fields = ModelFields::getFieldsMetadata($this->userRepository->model())->pluck('name')->toArray();
        $columns[] = 'id';
        return array_diff($columns, $fields);
    }

    private function makeRelations(?array $relations): array
    {
        return ($relations ?? []) + ['roles', 'roles.permissions', 'permissions'];
    }
}