| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- <?php
- namespace App\Services;
- use App\Enums\LanguageEnum;
- 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';
- 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.
- */
- public function check(string $registration): array
- {
- $user = $this->findByRegistration($registration);
- if ($user?->first_access_completed_at) {
- return ['status' => self::STATUS_DONE];
- }
- return [
- 'status' => $user ? self::STATUS_PENDING : self::STATUS_NOT_FOUND,
- 'token' => $this->issueToken($registration),
- 'registration' => $registration,
- 'user' => $user ? $this->prefill($user) : null,
- 'missing_fields' => $user ? $this->missingFields($user) : self::PROFILE_FIELDS,
- ];
- }
- /**
- * Conclui o primeiro acesso: cria o associado (ou completa o existente), define a senha e a foto.
- */
- public function register(array $data, UploadedFile $photo): User
- {
- $registration = $data['registration'];
- $this->assertValidToken($data['token'] ?? null, $registration);
- $user = $this->findByRegistration($registration);
- if ($user?->first_access_completed_at) {
- throw ValidationException::withMessages([
- 'registration' => __('messages.first_access.already_done'),
- ]);
- }
- return DB::transaction(function () use ($data, $photo, $registration, $user): User {
- if ($user) {
- // Associado já existente (importação ou landing page): completa apenas o que falta,
- // sem sobrescrever o que a administração já cadastrou, e mantém o status atual.
- foreach (self::PROFILE_FIELDS as $field) {
- if ($this->isEmpty($user->{$field})) {
- $user->{$field} = $data[$field];
- }
- }
- } else {
- $user = new User([
- 'registration' => $registration,
- 'type' => UserTypeEnum::ASSOCIADO,
- 'status' => UserStatusEnum::PENDING,
- 'language' => LanguageEnum::PORTUGUESE,
- ]);
- foreach (self::PROFILE_FIELDS as $field) {
- $user->{$field} = $data[$field];
- }
- }
- $user->password = $data['password'];
- $user->first_access_completed_at = now();
- $user->save();
- $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
- {
- return [
- 'name' => $user->name,
- '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 (!$user->photo_path) {
- $missing[] = 'photo';
- }
- return $missing;
- }
- 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;
- }
- }
- }
|