orgUserModel = new OrganizationUserModel(); $this->orgModel = new OrganizationModel(); $this->userModel = new UserModel(); $this->baseUrl = rtrim(config('App')->baseURL, '/'); } /** * Создание приглашения в организацию * * @param int $organizationId ID организации * @param string $email Email приглашаемого * @param string $role Роль (admin, manager, guest) * @param int $invitedBy ID пользователя, отправляющего приглашение * @return array ['success' => bool, 'message' => string, 'invite_link' => string, 'invitation_id' => int] */ public function createInvitation(int $organizationId, string $email, string $role, int $invitedBy): array { // Валидация email if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return [ 'success' => false, 'message' => 'Некорректный email адрес', 'invite_link' => '', 'invitation_id' => 0, ]; } // Получаем организацию $organization = $this->orgModel->find($organizationId); if (!$organization) { return [ 'success' => false, 'message' => 'Организация не найдена', 'invite_link' => '', 'invitation_id' => 0, ]; } // Проверяем, существует ли пользователь $existingUser = $this->userModel->where('email', $email)->first(); $userId = $existingUser['id'] ?? null; // Проверяем, не состоит ли уже пользователь в организации if ($userId) { $existingMembership = $this->orgUserModel ->where('organization_id', $organizationId) ->where('user_id', $userId) ->first(); if ($existingMembership) { return [ 'success' => false, 'message' => 'Пользователь уже состоит в этой организации', 'invite_link' => '', 'invitation_id' => 0, ]; } // Проверяем, есть ли уже приглашение if ($this->orgUserModel->hasPendingInvite($organizationId, $userId)) { return [ 'success' => false, 'message' => 'Приглашение для этого пользователя уже отправлено', 'invite_link' => '', 'invitation_id' => 0, ]; } } // Генерируем токен приглашения $inviteToken = $this->generateToken(); $inviteExpiresAt = date('Y-m-d H:i:s', strtotime('+7 days')); // Создаем запись приглашения $invitationData = [ 'organization_id' => $organizationId, 'user_id' => $userId, // NULL для новых пользователей 'role' => $role, 'status' => OrganizationUserModel::STATUS_PENDING, 'invite_token' => $inviteToken, 'invite_expires_at' => $inviteExpiresAt, 'invited_by' => $invitedBy, ]; $invitationId = $this->orgUserModel->createInvitation($invitationData); if (!$invitationId) { return [ 'success' => false, 'message' => 'Ошибка при создании приглашения', 'invite_link' => '', 'invitation_id' => 0, ]; } // Если пользователь новый - создаем "теневую" запись в users if (!$existingUser) { $this->createShadowUser($email); } // Формируем ссылку приглашения $inviteLink = $this->baseUrl . '/invitation/accept/' . $inviteToken; // Отправляем email $emailSent = $this->sendInvitationEmail($email, $organization['name'], $role, $inviteLink); return [ 'success' => $emailSent, 'message' => $emailSent ? 'Приглашение успешно отправлено' : 'Приглашение создано, но не удалось отправить email', 'invite_link' => $inviteLink, 'invitation_id' => $invitationId, ]; } /** * Принятие приглашения */ public function acceptInvitation(string $token, int $userId): array { $invitation = $this->orgUserModel->findByInviteToken($token); if (!$invitation) { return [ 'success' => false, 'message' => 'Приглашение не найдено или уже обработано', ]; } // Обновляем приглашение $updated = $this->orgUserModel->acceptInvitation($invitation['id'], $userId); if (!$updated) { return [ 'success' => false, 'message' => 'Ошибка при принятии приглашения', ]; } // Если это был теневой пользователь - привязываем к реальному if ($invitation['user_id'] === null) { $this->bindShadowUser($invitation['organization_id'], $userId); } // Получаем организацию для редиректа $organization = $this->orgModel->find($invitation['organization_id']); return [ 'success' => true, 'message' => 'Приглашение принято', 'organization_id' => $invitation['organization_id'], 'organization_name' => $organization['name'] ?? '', ]; } /** * Отклонение приглашения */ public function declineInvitation(string $token): array { $invitation = $this->orgUserModel->findByInviteToken($token); if (!$invitation) { return [ 'success' => false, 'message' => 'Приглашение не найдено или уже обработано', ]; } $deleted = $this->orgUserModel->declineInvitation($invitation['id']); return [ 'success' => $deleted, 'message' => $deleted ? 'Приглашение отклонено' : 'Ошибка при отклонении приглашения', ]; } /** * Отзыв приглашения (отправителем) */ public function cancelInvitation(int $invitationId, int $organizationId): array { $invitation = $this->orgUserModel ->where('id', $invitationId) ->where('organization_id', $organizationId) ->where('status', OrganizationUserModel::STATUS_PENDING) ->first(); if (!$invitation) { return [ 'success' => false, 'message' => 'Приглашение не найдено', ]; } $deleted = $this->orgUserModel->cancelInvitation($invitationId); return [ 'success' => $deleted, 'message' => $deleted ? 'Приглашение отозвано' : 'Ошибка при отзыве приглашения', ]; } /** * Повторная отправка приглашения */ public function resendInvitation(int $invitationId, int $organizationId): array { $invitation = $this->orgUserModel ->where('id', $invitationId) ->where('organization_id', $organizationId) ->where('status', OrganizationUserModel::STATUS_PENDING) ->first(); if (!$invitation) { return [ 'success' => false, 'message' => 'Приглашение не найдено', ]; } // Генерируем новый токен $newToken = $this->generateToken(); $newExpiresAt = date('Y-m-d H:i:s', strtotime('+7 days')); $this->orgUserModel->update($invitationId, [ 'invite_token' => $newToken, 'invite_expires_at' => $newExpiresAt, 'invited_at' => date('Y-m-d H:i:s'), ]); // Получаем email пользователя $user = $this->userModel->find($invitation['user_id']); if (!$user) { return [ 'success' => false, 'message' => 'Пользователь не найден', ]; } // Формируем ссылку $organization = $this->orgModel->find($organizationId); $inviteLink = $this->baseUrl . '/invitation/accept/' . $newToken; // Отправляем email $sent = $this->sendInvitationEmail( $user['email'], $organization['name'], $invitation['role'], $inviteLink ); return [ 'success' => $sent, 'message' => $sent ? 'Приглашение отправлено повторно' : 'Ошибка отправки', 'invite_link' => $inviteLink, ]; } /** * Генерация уникального токена */ protected function generateToken(): string { do { $token = bin2hex(random_bytes(32)); $exists = $this->orgUserModel->where('invite_token', $token)->first(); } while ($exists); return $token; } /** * Создание "теневого" пользователя для новых приглашенных */ protected function createShadowUser(string $email): int { $token = bin2hex(random_bytes(32)); $tokenExpiresAt = date('Y-m-d H:i:s', strtotime('+24 hours')); return $this->userModel->insert([ 'email' => $email, 'name' => '', // Заполнится при регистрации 'password' => null, // Без пароля до регистрации 'email_verified' => 0, 'verification_token' => $token, 'token_expires_at' => $tokenExpiresAt, 'created_at' => date('Y-m-d H:i:s'), ]); } /** * Привязка теневого пользователя к реальному */ protected function bindShadowUser(int $organizationId, int $userId): void { // Находим теневую запись по email пользователя $user = $this->userModel->find($userId); if ($user && empty($user['password'])) { // Обновляем все pending приглашения этого пользователя $this->orgUserModel ->where('user_id', null) ->where('status', OrganizationUserModel::STATUS_PENDING) ->set(['user_id' => $userId]) ->update(); } } /** * Отправка email с приглашением */ protected function sendInvitationEmail(string $email, string $orgName, string $role, string $inviteLink): bool { $roleLabels = [ 'owner' => 'Владелец', 'admin' => 'Администратор', 'manager' => 'Менеджер', 'guest' => 'Гость', ]; $roleLabel = $roleLabels[$role] ?? $role; $emailService = service('email'); $emailService->setTo($email); $emailService->setSubject('Приглашение в организацию ' . $orgName); $message = <<

Приглашение в Бизнес.Точка

Вас приглашают присоединиться к организации {$orgName}

Ваша роль: {$roleLabel}

Нажмите кнопку ниже, чтобы принять или отклонить приглашение:

Принять приглашение

Если кнопка не работает, скопируйте ссылку и откройте в браузере:

Ссылка действительна 7 дней.

HTML; $emailService->setMessage($message); return $emailService->send(); } }