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();
// Создаем запись приглашения
$invitationData = [
'organization_id' => $organizationId,
'user_id' => $userId, // NULL для новых пользователей
'role' => $role,
'status' => OrganizationUserModel::STATUS_PENDING,
'invite_token' => $inviteToken,
'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();
$this->orgUserModel->update($invitationId, [
'invite_token' => $newToken,
'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));
return $this->userModel->insert([
'email' => $email,
'name' => '', // Заполнится при регистрации
'password' => null, // Без пароля до регистрации
'email_verified' => 0,
'verification_token' => $token,
'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}
Нажмите кнопку ниже, чтобы принять или отклонить приглашение:
Принять приглашение
Если кнопка не работает, скопируйте ссылку и откройте в браузере:
{$inviteLink}
Ссылка действительна 48 часов.
HTML;
$emailService->setMessage($message);
return $emailService->send();
}
}