DEV-079.1 добавляет безопасный backend-сценарий приглашения зарегистрированного
пользователя в рабочее пространство Project. После принятия пользователь
становится Collaborator этого проекта. TeamMember, участник Application,
подписчик проекта и Collaborator остаются разными сущностями.
React-интерфейс будет добавлен отдельным этапом DEV-079.2. В этом PR не добавляются email-приглашения, токены, сроки действия и приглашения незарегистрированных пользователей.
Существующая модель invites.Invite уже хранит связь Project и
зарегистрированного CustomUser, роль, специализацию, сообщение и tri-state
is_accepted. Angular передает числовой user id, извлеченный из ссылки на
профиль, в POST /invites/, получает активные приглашения через
GET /invites/, принимает и отклоняет их отдельными action endpoints, а
DELETE /invites/<id>/ использует как отзыв.
Legacy-контракт /invites/ сохранен. Новый workspace API использует ту же
сущность, но не требует от React знания legacy-полей project, user и
is_accepted.
В Invite добавлены:
invited_by— зарегистрированный отправитель;is_revoked— отзыв без физического удаления;resolved_at— время принятия, отклонения или отзыва.
Публичный status вычисляется без удаления legacy is_accepted:
pending—is_accepted=null,is_revoked=false;accepted—is_accepted=true;declined—is_accepted=false;revoked—is_revoked=true.
Допустимые переходы:
pending -> accepted
pending -> declined
pending -> revoked
Повторный переход завершенного приглашения возвращает 409. Миграция заполняет
invited_by текущим лидером проекта, переносит дату обработки legacy-записей и,
если в старых данных есть несколько pending-записей одной пары Project/User,
оставляет активной только новейшую. Остальные сохраняются как revoked.
На уровне БД действуют:
- partial unique constraint для одной pending-записи на
project + user; - check constraint, запрещающий одновременно
is_revoked=trueи принятое или отклоненное значениеis_accepted; - индексы списков по пользователю/проекту и lifecycle-полям.
Все endpoints требуют аутентификацию.
GET /projects/<project_id>/workspace/invitations/
POST /projects/<project_id>/workspace/invitations/Список содержит историю только указанного проекта. Доступ имеют лидер проекта, staff и superuser.
Payload создания:
{
"recipient_id": 42,
"role": "Разработчик",
"specialization": "Backend",
"message": "Присоединяйтесь к проекту"
}Обязателен только recipient_id. Project берется из URL, отправитель — из
аутентифицированного пользователя. Неизвестные и read-only поля отклоняются.
GET /projects/workspace/invitations/incoming/Возвращает только историю текущего получателя; pending-приглашения идут первыми.
POST /projects/workspace/invitations/<invitation_id>/accept/
POST /projects/workspace/invitations/<invitation_id>/decline/
POST /projects/<project_id>/workspace/invitations/<invitation_id>/revoke/Action payload должен быть пустым. Принять или отклонить приглашение может только получатель. Отозвать pending-приглашение может лидер соответствующего Project, staff или superuser.
Коды ответа: 201 для создания, 200 для списков и успешных переходов, 400
для невалидных полей или получателя, 403 для видимого Project без права
управления, безопасный 404 для скрытого Project/чужого приглашения и 409 для
активного дубля либо повторного перехода статуса.
Пример ответа:
{
"id": 7,
"project": {
"id": 10,
"name": "Проект",
"draft": true,
"is_public": false
},
"sender": {
"id": 1,
"first_name": "Анна",
"last_name": "Иванова",
"avatar": null
},
"recipient": {
"id": 42,
"first_name": "Иван",
"last_name": "Петров",
"avatar": null
},
"status": "pending",
"role": "Разработчик",
"specialization": "Backend",
"message": "Присоединяйтесь к проекту",
"created_at": "2026-08-06T12:00:00Z",
"processed_at": null,
"updated_at": "2026-08-06T12:00:00Z"
}| Действие | Лидер | Collaborator | Получатель | Посторонний | Staff |
|---|---|---|---|---|---|
| Список проекта | Да | Нет | Нет | Нет | Да |
| Создание | Да | Нет | Нет | Нет | Да |
| Входящие | Только свои | Только свои | Только свои | Только свои | Только свои |
| Accept/decline | Только если получатель | Только если получатель | Да | Нет | Только если получатель |
| Revoke | Да | Нет | Нет | Нет | Да |
Нельзя пригласить лидера, существующего Collaborator, неактивного или
несуществующего пользователя. Для legacy Project, напрямую связанного с
PartnerProgramProject, получатель должен быть участником этой программы — это
повторяет действующий invariant Collaborator.clean().
Приватный Project скрывается от постороннего через 404; пользователь, который
видит Project, но не управляет им, получает 403. Ответы не содержат email,
телефон, анкету Application или данные Submission.
Создание, принятие, отклонение и отзыв выполняются в transaction.atomic с
блокировками Project и Invite. Принятие и создание Collaborator — одна
транзакция. Partial unique constraint закрывает гонку двух создающих запросов.
- приглашаются только существующие активные пользователи по
recipient_id; - отдельный безопасный поиск кандидатов не добавлен;
- email, уведомления, invite links и expiration отсутствуют;
- React UI относится к DEV-079.2;
- Angular продолжает использовать legacy
/invites/; TeamInvite,TeamMember, Application, Submission, подписки, цели и достижения не изменяются.