Несколько ящиков у одного логина, общие ящики, кто ответил, вход ящик*логин
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
- V7: users.is_shared, mailbox_access (MEMBER от админа/Jambotron, ATTACHED по паролю ящика), messages.answered_by
- /api/mailboxes: мои ящики с непрочитанными, сессия другого ящика (JWT с claim by), подключить по паролю
(LoginGuard), убрать из меню
- общий ящик: без своего входа (веб/SMTP/IMAP), участники открывают; «прочитано» общее; ответ из веба → answered_by
- SMTP/IMAP: ящик*логин + пароль логина (как master user в Dovecot)
- открытый чужой ящик (общий, подключённый, OPEN админа): профиль и пароль не меняются (403)
- админ: создать shared, участники ящика (PUT /{id}/members); служебное API: PUT /shared/{address};
выключение аккаунта убирает и из общих ящиков
- UI: MAILBOXES в левом меню (переключение, ⊕ подключить, × убрать), shared/участники в настройках,
«Answered by» у письма
- 88 тестов; живой прогон на Postgres 17 (апгрейд V6→V7 на данных), скриншоты UI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,10 @@ public class AuthenticationController {
|
||||
return delay.isZero() ? answer : answer.delayElement(delay);
|
||||
}
|
||||
guard.success(ip, email);
|
||||
if (user.isShared()) {
|
||||
return Mono.just(ResponseEntity.badRequest().body(Map.of("error",
|
||||
"This is a shared mailbox: log in with your own mailbox and open it there")));
|
||||
}
|
||||
if (!user.isActive()) {
|
||||
return Mono.just(ResponseEntity.badRequest().body(Map.of("error", "User is deactivated")));
|
||||
}
|
||||
@@ -138,7 +142,7 @@ public class AuthenticationController {
|
||||
|
||||
@PutMapping("/me")
|
||||
public Mono<ProfileResponse> updateMe(Mono<Authentication> auth, @Valid @RequestBody Mono<ProfileUpdateRequest> body) {
|
||||
return user(auth).zipWith(body)
|
||||
return own(auth).zipWith(body)
|
||||
.flatMap(pair -> userService.updateDisplayName(pair.getT1(), pair.getT2().displayName()))
|
||||
.flatMap(this::profile);
|
||||
}
|
||||
@@ -147,7 +151,7 @@ public class AuthenticationController {
|
||||
public Mono<Map<String, String>> changePassword(Mono<Authentication> auth, @Valid @RequestBody Mono<PasswordChangeRequest> body,
|
||||
ServerHttpRequest http) {
|
||||
String ip = clientIp(http);
|
||||
return user(auth).zipWith(body)
|
||||
return own(auth).zipWith(body)
|
||||
.flatMap(pair -> {
|
||||
User user = pair.getT1();
|
||||
// Someone with a stolen session must not guess the current password either
|
||||
@@ -186,6 +190,15 @@ public class AuthenticationController {
|
||||
admins.isAdmin(user.getEmail())));
|
||||
}
|
||||
|
||||
/** The session's own mailbox — not a shared / attached / administrator-opened one: no profile changes there. */
|
||||
private Mono<User> own(Mono<Authentication> auth) {
|
||||
return auth.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED)))
|
||||
.flatMap(a -> com.mailserver.security.Actor.opened(a)
|
||||
? Mono.error(new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||
"This mailbox is opened from " + com.mailserver.security.Actor.login(a) + " — change it in Settings → MAILBOXES"))
|
||||
: user(Mono.just(a)));
|
||||
}
|
||||
|
||||
private Mono<User> user(Mono<Authentication> auth) {
|
||||
return auth.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED)))
|
||||
.flatMap(a -> userService.findByEmail(a.getName()))
|
||||
|
||||
Reference in New Issue
Block a user