| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- <?php
- namespace App\Services;
- use App\Enums\UserStatusEnum;
- use App\Enums\UserTypeEnum;
- use App\Models\User;
- use Illuminate\Contracts\Encryption\DecryptException;
- use Illuminate\Http\UploadedFile;
- use Illuminate\Support\Facades\Crypt;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Validation\ValidationException;
- class FirstAccessService
- {
- public const STATUS_NOT_FOUND = 'not_found';
- public const STATUS_PENDING = 'pending_first_access';
- public const STATUS_DONE = 'already_done';
- public const STATUS_NOT_APPROVED = 'not_approved';
- public const STATUS_REFUSED = 'refused';
- /** Situações em que o crachá não libera o formulário de primeiro acesso. */
- public const BLOCKING_STATUSES = [
- self::STATUS_NOT_FOUND,
- self::STATUS_DONE,
- self::STATUS_NOT_APPROVED,
- self::STATUS_REFUSED,
- ];
- private const TOKEN_TTL_MINUTES = 15;
- private const PROFILE_FIELDS = ['name', 'cpf', 'email', 'phone', 'position_id', 'sector_id'];
- public function __construct(protected MediaService $mediaService) {}
- /**
- * Consulta um crachá e diz se o associado pode seguir para o formulário de primeiro acesso.
- * Só crachá já liberado pela administração passa: cadastro inexistente, pendente de aprovação
- * ou recusado é bloqueado aqui, antes de o formulário abrir.
- */
- public function check(string $registration): array
- {
- $user = $this->findByRegistration($registration);
- if ($blockedStatus = $this->blockingStatus($user)) {
- return ['status' => $blockedStatus];
- }
- return [
- 'status' => self::STATUS_PENDING,
- 'token' => $this->issueToken($registration),
- 'registration' => $registration,
- 'user' => $this->prefill($user),
- 'missing_fields' => $this->missingFields($user),
- ];
- }
- private function blockingStatus(?User $user): ?string
- {
- return match (true) {
- !$user => self::STATUS_NOT_FOUND,
- (bool) $user->first_access_completed_at => self::STATUS_DONE,
- $user->status === UserStatusEnum::REFUSED => self::STATUS_REFUSED,
- $user->status === UserStatusEnum::PENDING => self::STATUS_NOT_APPROVED,
- default => null,
- };
- }
- public function blockedMessage(string $status): string
- {
- return match ($status) {
- self::STATUS_DONE => __('messages.first_access.already_done'),
- self::STATUS_NOT_APPROVED => __('messages.first_access.not_approved'),
- self::STATUS_REFUSED => __('messages.first_access.refused'),
- default => __('messages.first_access.not_found'),
- };
- }
- public function register(array $data, ?UploadedFile $photo = null): User
- {
- $registration = $data['registration'];
- $this->assertValidToken($data['token'] ?? null, $registration);
- $user = $this->findByRegistration($registration);
- if ($blockedStatus = $this->blockingStatus($user)) {
- throw ValidationException::withMessages([
- 'registration' => $this->blockedMessage($blockedStatus),
- ]);
- }
- $data['name'] = $this->fullName($data['name'], $data['last_name'] ?? null);
- return DB::transaction(function () use ($data, $photo, $user): User {
- foreach (self::PROFILE_FIELDS as $field) {
- if ($this->isEmpty($user->{$field})) {
- $user->{$field} = $data[$field];
- }
- }
- $user->name = $data['name'];
- $user->password = $data['password'];
- $user->first_access_completed_at = now();
- $user->save();
- // Sem arquivo novo, mantém a foto que a administração já havia cadastrado.
- if ($photo) {
- $this->mediaService->uploadUserAvatar($photo, $user);
- }
- return $user->fresh();
- });
- }
- private function findByRegistration(string $registration): ?User
- {
- return User::where('type', UserTypeEnum::ASSOCIADO)
- ->where('registration', $registration)
- ->first();
- }
- private function prefill(User $user): array
- {
- [$name, $lastName] = $this->splitName($user->name);
- return [
- 'name' => $name,
- 'last_name' => $lastName,
- 'cpf' => $user->cpf,
- 'email' => $user->email,
- 'phone' => $user->phone,
- 'position_id' => $user->position_id,
- 'sector_id' => $user->sector_id,
- 'photo_url' => $user->photo_path
- ? Storage::disk('s3')->temporaryUrl($user->photo_path, now()->addHours(24))
- : null,
- ];
- }
- private function missingFields(User $user): array
- {
- $missing = array_values(array_filter(
- self::PROFILE_FIELDS,
- fn(string $field): bool => $this->isEmpty($user->{$field}),
- ));
- if ($this->isEmpty($this->splitName($user->name)[1])) {
- $missing[] = 'last_name';
- }
- if (!$user->photo_path) {
- $missing[] = 'photo';
- }
- return $missing;
- }
- /**
- * Quebra o nome gravado em primeiro nome + sobrenome para preencher o formulário.
- *
- * @return array{0: string, 1: string}
- */
- private function splitName(?string $name): array
- {
- $parts = preg_split('/\s+/', trim((string) $name), 2, PREG_SPLIT_NO_EMPTY) ?: [];
- return [$parts[0] ?? '', $parts[1] ?? ''];
- }
- private function fullName(string $name, ?string $lastName): string
- {
- return trim(preg_replace('/\s+/', ' ', $name . ' ' . $lastName));
- }
- private function isEmpty(mixed $value): bool
- {
- return $value === null || $value === '';
- }
- private function issueToken(string $registration): string
- {
- return Crypt::encryptString(json_encode([
- 'registration' => $registration,
- 'expires_at' => now()->addMinutes(self::TOKEN_TTL_MINUTES)->timestamp,
- ]));
- }
- /**
- * Garante que o formulário enviado corresponde ao crachá consultado no passo anterior.
- */
- private function assertValidToken(?string $token, string $registration): void
- {
- $invalid = ValidationException::withMessages([
- 'token' => __('messages.first_access.invalid_token'),
- ]);
- if (!$token) {
- throw $invalid;
- }
- try {
- $payload = json_decode(Crypt::decryptString($token), true);
- } catch (DecryptException) {
- throw $invalid;
- }
- if (
- !is_array($payload) ||
- ($payload['registration'] ?? null) !== $registration ||
- ($payload['expires_at'] ?? 0) < now()->timestamp
- ) {
- throw $invalid;
- }
- }
- }
|